Znatiželjan slučaj ispitivanja performansi setTimeout (0)

(Za potpuni učinak, čitajte hrapavim glasom dok ste okruženi oblakom dima)

Sve je započelo sivog jesenjeg dana. Nebo je bilo oblačno, puhao je vjetar i netko mi je rekao da to setTimeout(0)u prosjeku stvara kašnjenje od 4 ms. Tvrdili su da je to potrebno vrijeme za vraćanje povratnog poziva iz hrpe, u red povratnih poziva i ponovno vraćanje u stog. Mislila sam da zvuči riblje (ovo je ono malo što me zamišljate crno-bijelo s cigaretom u ustima). S obzirom na to da cjevovod prikazivanja treba raditi svakih 16 ms kako bi se omogućile glatke animacije, 4 ms činilo mi se kao dugo vrijeme. Vrlo dugo.

Nekoliko naivnih testova u programima s console.time()potvrdilo je. Prosječno kašnjenje tijekom 20 vožnji bilo je oko 1,5 ms. Naravno, 20 serija nije dovoljna veličina uzorka, ali sada sam imao smisla dokazati. Htio sam pokrenuti testove na većoj skali koji bi mi mogli dati točniji odgovor. Tada bih, naravno, mogao otići i to mahnuti licu u lice da dokažem da su pogriješili.

Zašto inače radimo to što radimo?

Tradicionalna metoda

Odmah sam se našao u vrućoj vodi. Da bih izmjerio koliko je vremena potrebno setTimeout(0)za pokretanje, trebala mi je funkcija koja:

  • snimio trenutni trenutak
  • pogubljen setTimeout
  • zatim odmah izašao kako bi stog bio čist i planirani povratni poziv mogao pokrenuti i izračunati vremensku razliku
  • i trebala sam tu funkciju za pokretanje dovoljno velikog broja puta kako bi izračuni bili statistički značajni

Ali konstrukcija za ovo - for-loop - ne bi uspjela. Budući da for-loop ne uklanja stek dok ne izvrši svaku petlju, povratni poziv se ne bi pokrenuo odmah. Ili, da se stavi u kod, dobili bismo ovo:

Ovdje je problem bio inherentan - ako bih htio pokrenuti setTimeoutviše puta automatski, to bih morao učiniti iz drugog konteksta. Ali, sve dok bih trčao iz drugog konteksta, uvijek bi postojalo dodatno kašnjenje od trenutka kada sam pokrenuo test do trenutka izvršenja povratnog poziva.

Naravno da bih ga mogao zasuti poput nekih ovih beskorisnih detektiva, napisati funkciju koja radi ono što trebam, a zatim je kopirati i zalijepiti 10 000 puta. Naučio bih ono što sam želio znati, ali smaknuće bi bilo daleko od gracioznog. Da sam ovo htio utrljati u tuđe lice, radije bih to učinio na drugi način.

Tada je došlo do mene.

Revolucionarna metoda

Mogao bih koristiti web radnika.

Web zaposlenici rade na drugoj niti. Dakle, ako setTimeoutlogiku smjestim u web radnika, mogao bih je nazvati više puta. Svaki poziv bi stvorio vlastiti kontekst izvršenja, pozivanja setTimeouti neposrednog izlaska iz funkcije kako bi se povratni poziv mogao izvršiti. Radovao sam se što ću raditi s web radnicima.

Bilo je vrijeme da se prebacim na moj pouzdani Uzvišeni tekst.

Počeo sam samo testirati vode. S ovim kodom u main.js:

Ovdje je malo vodovoda za pripremu za stvarni test, ali u početku sam se samo htio osigurati mogu li ispravnu komunikaciju s web radnikom. Dakle, ovo je bilo početno worker.js:

I premda je djelovalo kao šarm - dalo je rezultate koje sam trebao očekivati, ali nije:

Budući da sam bio toliko naviknut na sinkronicitet u JS-u, nisam mogao a da me ovo ne iznenadi. Prvi trenutak kad sam to vidio, moj mozak je registrirao grešku. No, budući da svaka petlja postavlja novog web radnika i oni rade asinkrono, logično je da se brojevi neće ispisivati ​​redom.

Možda me iznenadilo, ali djelovalo je kako se očekivalo. Mogao bih nastaviti s testom.

Ono što sam želio je da se onmessagefunkcija mrežnog radnika registrira t0, pozove setTimeouti zatim odmah izađe kako ne bi blokirala stog. Međutim, mogao bih staviti dodatnu funkcionalnost unutar povratnog poziva nakon što postavim vrijednost t1. Dodao sam svoj postMessageu povratni poziv, tako da ne blokira hrpu:

I evo main.jskoda:

Ova verzija ima problem.

Naravno - budući da sam nova u web radnicima, u početku to nisam razmišljala. No, kad se višekratno pokretanje funkcije nastavljalo ispisivati 0, shvatio sam da nešto nije u redu.

Kad sam iznutra ispisao svote onmessage, dobio sam odgovor. Glavna se funkcija kretala sinkrono i nije čekala da se poruka radnika vrati, pa je izračunala prosjek prije nego što je web radnik završio.

Brzo i prljavo rješenje je dodavanje brojača i izračun samo kad brojač dosegne maksimalnu vrijednost. Dakle, evo novogmain.js:

I evo rezultata:

main(10): 0.1

main(100) : 1.41

main(1000) : 13.082

Oh. Moj. Pa, to nije sjajno, zar ne? Što se ovdje događa?

Žrtvovao sam testiranje performansi da bih pogledao unutra. Sada se prijavljujem t0i t1 kad se kreiraju, samo da vidim što se tamo događa.

I rezultati:

Ispostavilo se da je i moje očekivanje da ću t1biti izračunat odmah nakon toga t0bilo pogrešno. U osnovi činjenica da ništa o web radnicima nije sinkrono znači da moje najosnovnije pretpostavke o ponašanju mog koda jednostavno više ne vrijede. To je teško vidjeti slijepu točku.

I ne samo to, nego čak ni rezultati koje sam postigao main(10)i main(100)koji su me u početku bili jako sretni i samozadovoljni, nisu bili nešto na što bih se mogao osloniti.

Asinhronost web radnika također ih čini nepouzdanim proxyjem za to kako se stvari ponašaju u našem redovnom snopu podataka. Dakle, iako mjerenje performansi setTimeoutinternetskog radnika daje neke zanimljive rezultate, to nisu rezultati koji odgovaraju na naše pitanje.

Udžbenička metoda

Bio sam frustriran ... zar stvarno ne bih mogao pronaći vanilijevo JS rješenje koje bi ujedno bilo elegantno i dokazalo da moj kolega griješi?

A onda sam shvatila - mogao sam nešto učiniti, ali to mi se ne bi svidjelo.

Mogao bih setTimeoutrekurzivno nazvati .

Sad kad nazovem, nazvat mainće testRunnerkoje mjere, t0a zatim zakazati povratni poziv. Tada se povratni poziv pokreće odmah, izračunava, t1a zatim testRunnerponovno poziva , sve dok ne dosegne željeni broj poziva.

Rezultati ovog koda bili su posebno iznenađujući. Evo nekoliko ispisa main(10)i main(1000):

Rezultati se značajno razlikuju kada se funkcija pozove 1.000 puta u usporedbi s pozivom funkcije 10 puta. Pokušavao sam to više puta i postigao sam uglavnom iste rezultate, s main(10)ulaskom od 3-4 ms i main(1000)dosipanjem od 5 ms.

Da budem iskren, nisam siguran što se ovdje događa. Tražio sam odgovor, ali nisam mogao naći razumno objašnjenje. Ako ovo čitate i nagađate što se događa - volio bih čuti vaše komentare.

Iskušana i istinita metoda

Negdje u pozadini svog uma, uvijek sam znao da će doći do ovoga ... Blještave stvari su lijepe za one koji ih mogu dobiti, ali isprobane i istinite uvijek će biti tu na kraju. Iako sam to pokušavao izbjeći, uvijek sam znao da je to opcija. setInterval.

Ovaj kôd izvodi trik pomalo grubo. setIntervalizvodi funkciju uzastopno, čekajući 50 ms između svakog pokretanja, kako bi bio siguran da je stog čist. Ovo je ne elegantno, ali testiram točno ono što sam trebao.

A rezultati su također bili obećavajući. Čini se da se vremena podudaraju s mojim izvornim očekivanjima - ispod 1,5 ms.

Napokon bih mogao staviti ovaj slučaj u krevet. Imao sam uspona i padova i udio neočekivanih rezultata, ali na kraju je bilo važno samo jedno - dokazao sam da drugi programer griješi! To mi je bilo dovoljno dobro.

Želite li se poigrati s ovim kodom? pogledajte ovdje: //github.com/NettaB/setTimeout-test