Det nysgerrige tilfælde af performance test setTimeout (0)

(For fuld effekt, læs med en husky stemme, mens du er omgivet af en sky af røg)

Det hele begyndte på en grå efterårsdag. Himlen var overskyet, vinden blæste, og nogen fortalte mig, at det setTimeout(0)i gennemsnit skaber en forsinkelse på 4 ms. De hævdede, at det er den tid, det tager at pope tilbagekaldet fra stakken, ind i tilbagekaldskøen og tilbage på stakken igen. Jeg troede, det lød fiskeagtigt (dette er den smule du forestiller mig i sort / hvid med en cigar i munden). I betragtning af at gengivelsesrørledningen skal køre hver 16. ms for at muliggøre glatte animationer, syntes 4 ms for mig at være lang tid. Meget lang tid.

Et par naive tests i devtools med console.time()bekræftede det. Den gennemsnitlige forsinkelse på tværs af 20 kørsler var ca. 1,5 ms. Selvfølgelig gør 20 kørsler ikke en tilstrækkelig stikprøvestørrelse, men nu havde jeg et punkt at bevise. Jeg ønskede at køre tests i større skala, der kunne give mig et mere præcist svar. Jeg kunne så naturligvis gå og bøje det i min kollegas ansigt for at bevise, at de tog fejl.

Hvorfor gør vi ellers det, vi gør?

Den traditionelle metode

Straks befandt jeg mig i varmt vand. For at måle, hvor lang tid det tog setTimeout(0)at køre, havde jeg brug for en funktion, der:

  • tog et øjebliksbillede af den aktuelle tid
  • henrettet setTimeout
  • forlod derefter straks, så stakken ville være klar, og den planlagte tilbagekaldelse kunne køre og beregne tidsforskellen
  • og jeg havde brug for denne funktion til at køre et tilstrækkeligt stort antal gange, så beregningerne var statistisk meningsfulde

Men go-to-konstruktionen til dette - for-loop - ville ikke fungere. Fordi for-loop ikke rydder stakken, før den har udført hver loop, vil tilbagekaldet ikke køre med det samme. Eller for at sætte det i kode, ville vi få dette:

Problemet her var iboende - hvis jeg ville køre setTimeoutflere gange automatisk, skulle jeg gøre det inden for en anden sammenhæng. Men så længe jeg løb fra en anden kontekst, ville der altid være en ekstra forsinkelse fra det tidspunkt, hvor jeg startede testen til det tidspunkt, hvor tilbagekaldet blev udført.

Selvfølgelig kunne jeg slumre det som nogle af disse god-for-ingenting-detektiver, skrive en funktion, der gør hvad jeg har brug for, og derefter kopiere og indsætte den 10.000 gange. Jeg ville lære, hvad jeg ville vide, men henrettelsen ville være langt fra yndefuld. Hvis jeg ville gnide dette i andres ansigt, ville jeg meget hellere gøre det på en anden måde.

Så kom det til mig.

Den revolutionerende metode

Jeg kunne bruge en webarbejder.

Webarbejdere kører på en anden tråd. Så hvis jeg placerer setTimeoutlogikken i en webarbejder, kunne jeg kalde det flere gange. Hvert opkald ville skabe sin egen eksekveringskontekst, ringe setTimeoutog straks afslutte funktionen, så tilbagekaldet kunne udføres. Jeg havde set frem til at arbejde sammen med webarbejdere.

Det var tid til at skifte til min pålidelige sublime tekst.

Jeg startede bare med at teste vandet. Med denne kode i main.js:

Noget VVS her for at forberede den faktiske test, men oprindeligt ville jeg bare sikre mig, at jeg kunne kommunikere ordentligt med webarbejderen. Så dette var begyndelsen worker.js:

Og mens det fungerede som en charme - det producerede resultater, som jeg burde have forventet, men ikke var:

At være så vant til synkronicitet i JS, kunne jeg ikke lade være med at blive overrasket over dette. Det første øjeblik, jeg så det, registrerede min hjerne en fejl. Men da hver sløjfe opretter en ny webarbejder, og de kører asynkront, er det fornuftigt, at tallene ikke bliver udskrevet i rækkefølge.

Det har måske overrasket mig, men det fungerede som forventet. Jeg kunne gå videre med testen.

Hvad jeg ønskede er, at webarbejderens onmessagefunktion skal registrere t0, ringe setTimeoutog derefter straks afslutte for ikke at blokere stakken. Jeg kunne dog sætte ekstra funktionalitet inde i tilbagekaldet, efter at jeg har indstillet værdien af t1. Jeg tilføjede min postMessagetil tilbagekald, så det blokerer ikke stakken:

Og her er main.jskoden:

Denne version har et problem.

Selvfølgelig - da jeg er ny hos webarbejdere, havde jeg først ikke overvejet det. Men når flere kørsler af funktionen fortsatte med at udskrive 0, regnede jeg med, at noget ikke var rigtigt.

Da jeg udskrev summerne indefra, onmessagefik jeg mit svar. Hovedfunktionen bevægede sig synkront og ventede ikke på, at beskeden fra medarbejderen skulle vende tilbage, så den beregnede gennemsnittet, inden webarbejderen var færdig.

En hurtig og beskidt løsning er at tilføje en tæller og kun beregne, når tælleren har nået den maksimale værdi. Så her er det nyemain.js:

Og her er resultaterne:

main(10): 0.1

main(100) : 1.41

main(1000) : 13.082

Åh. Min. Nå, det er ikke godt, er det? Hvad sker der her?

Jeg ofrede præstationstest for at få et kig ind. Jeg logger nu, t0og t1 når de er oprettet, bare for at se, hvad der foregår der.

Og resultaterne:

Det viser sig også, at min forventning om at t1blive beregnet umiddelbart efter t0blev vildledt. Grundlæggende betyder det faktum, at intet om webarbejdere er synkront, at mine mest grundlæggende antagelser om, hvordan min kode opfører sig, bare ikke stemmer. Det er en vanskelig blind plet at se.

Ikke kun det, men selv de resultater, jeg fik for, main(10)og main(100)som oprindeligt gjorde mig meget glade og selvtilfredse, var ikke noget, jeg kunne stole på.

Webarbejdernes asynkronicitet gør dem også til en upålidelig proxy for, hvordan tingene opfører sig i vores almindelige stak. Så mens måling af ydeevne setTimeouthos en webarbejder giver nogle interessante resultater, er det ikke resultater, der besvarer vores spørgsmål.

Lærebogsmetoden

Jeg var frustreret ... kunne jeg virkelig ikke finde en vanilje JS-løsning, som både ville være elegant og bevise min kollega forkert?

Og så indså jeg - der var noget, jeg kunne gøre, men jeg kunne ikke lide det.

Jeg kunne ringe setTimeoutrekursivt.

Nu når jeg ringer, kalder maindet testRunnerhvilke mål t0og planlægger derefter tilbagekaldet. Tilbagekaldet kører derefter straks, beregner t1og ringer derefter op testRunnerigen, indtil det når det ønskede antal opkald.

Resultaterne af denne kode var særligt overraskende. Her er nogle udskrifter af main(10)og main(1000):

Resultaterne er markant forskellige, når der kaldes til funktionen 1.000 gange sammenlignet med at kalde den 10 gange. Jeg har prøvet dette gentagne gange og har stort set fået de samme resultater med at main(10)komme ind på 3-4 ms og main(1000)toppe 5 ms.

For at være ærlig er jeg ikke sikker på, hvad der sker her. Jeg søgte efter et svar, men kunne ikke finde nogen rimelig forklaring. Hvis du læser dette og har et veluddannet gæt om, hvad der foregår - vil jeg meget gerne høre fra dig i kommentarerne.

Den velprøvede metode

Et eller andet sted bag i sindet vidste jeg altid, at det ville komme til dette ... Prangende ting er rart for dem, der kan få dem, men prøvet og sandt vil altid være der i den ende. Selvom jeg forsøgte at undgå det, vidste jeg altid, at dette var en mulighed. setInterval.

Denne kode gør tricket med noget brutal kraft. setIntervalkører funktionen gentagne gange og venter 50 ms mellem hver kørsel for at sikre, at stakken er klar. Dette er uelegant, men tester nøjagtigt hvad jeg havde brug for.

Og resultaterne var også lovende. Tider synes at matche min oprindelige forventning - under 1,5 ms.

Endelig kunne jeg lægge denne sag i seng. Jeg havde haft nogle op- og nedture, og min andel af uventede resultater, men i sidste ende var kun en ting vigtig - jeg havde bevist, at en anden udvikler tog fejl! Det var godt nok for mig.

Vil du lege med denne kode? tjek det her: //github.com/NettaB/setTimeout-test