Jõudluskontrolli kurioosne juhtum setTimeout (0)

(Täieliku efekti saavutamiseks lugege husky häälega, kui suitsupilv on ümbritsetud)

Kõik algas hallist sügispäevast. Taevas oli pilves, tuul puhus ja keegi ütles mulle, et see setTimeout(0)tekitab keskmiselt 4 ms viivituse. Nad väitsid, et see on aeg, mis kulub tagasihelistamise pakkimiseks virnast tagasi helistamise järjekorda ja uuesti virna. Ma arvasin, et see kõlab kalakindlalt (see on natuke see, mida sa mind mustvalgel ette kujutad, sigar suus). Arvestades, et renderdustorustik peab sujuvate animatsioonide võimaldamiseks töötama iga 16 ms järel, tundus mulle 4 ms pikk aeg. Väga kaua.

Mõned naiivsed testid devtoolides console.time()kinnitasid seda. Keskmine viivitus 20 sõidu ajal oli umbes 1,5 ms. Muidugi ei anna 20 käiku piisavat valimimahtu, kuid nüüd oli mul tõestada mõte. Tahtsin korraldada teste suuremas mahus, et saaksin täpsema vastuse. Ma võiksin siis muidugi minna ja lehvitada sellega oma kolleegi näol, et tõestada, et nad eksisid.

Miks me muidu teeme seda, mida teeme?

Traditsiooniline meetod

Kohe sattusin kuuma vette. Selleks, et mõõta, kui kaua kulus setTimeout(0)selle käivitamiseks, vajasin funktsiooni, mis:

  • tegi hetkepildi praegusest ajast
  • hukati setTimeout
  • siis väljus kohe, et virn oleks selge ja ajastatud tagasihelistamine saaks käia ning ajavahe välja arvutada
  • ja mul oli vaja, et see funktsioon töötaks piisavalt palju kordi, et arvutused oleksid statistiliselt mõttekad

Kuid selle ehitamine - for-loop - ei tööta. Kuna for-loop ei korista virna enne, kui see on iga tsükli käivitanud, ei käivitaks tagasihelistamine kohe. Või koodi sisestamiseks saaksime selle:

Siinne probleem oli omane - kui ma tahaksin setTimeoutmitu korda automaatselt käivitada , peaksin seda tegema teise konteksti seest. Kuid seni, kuni ma jooksin teise konteksti alt, oleks katse alustamisest kuni tagasihelistamise hetkeni alati täiendav viivitus.

Muidugi võiksin ma seda slummida nagu mõned neist mittemidagiütlevatest detektiividest, kirjutada funktsiooni, mis teeb seda, mida mul vaja on, ja seejärel kopeerida ja kleepida see 10 000 korda. Ma õpiksin seda, mida tahtsin teada, kuid hukkamine poleks kaugeltki graatsiline. Kui ma kavatseksin seda kellelegi teisele näkku hõõruda, siis pigem teeksin seda teistmoodi.

Siis jõudis see minuni.

Revolutsiooniline meetod

Ma saaksin kasutada veebitöötajat.

Veebitöötajad jooksevad erineva lõimega. Niisiis, kui panen setTimeoutloogika veebitöötajasse, saaksin seda mitu korda helistada. Iga kõne loob oma täitmiskonteksti, helistab setTimeoutja väljub koheselt funktsioonist, nii et tagasihelistamist saaks käivitada. Olin oodanud, et saaksin koos veebitöötajatega mõnda tööd teha.

Oli aeg minna üle oma usaldusväärsele ülevale tekstile.

Hakkasin lihtsalt veekogusid katsetama. Selle koodiga main.js:

Mõned torustikud siin, et tegelikuks testiks valmistuda, kuid esialgu tahtsin lihtsalt veenduda, et saan veebitöötajaga korralikult suhelda. Nii et see oli esialgne worker.js:

Ja kuigi see töötas nagu võlu - see andis tulemusi, mida oleksin pidanud ootama, kuid ei olnud:

Olles JS-i sünkroonsusega nii harjunud, ei suutnud ma seda üllatada. Esimesel hetkel, kui seda nägin, registreeris mu aju vea. Kuid kuna iga silmus loob uue veebitöötaja ja nad töötavad asünkroonselt, on mõistlik, et numbreid ei trükita järjekorras.

See võis mind üllatada, kuid töötas ootuspäraselt. Ma võiksin testiga edasi minna.

Tahtsin, et veebitöötaja onmessagefunktsioon registreeruks t0, helistaks setTimeoutja seejärel kohe väljuks, et virna ei blokeeriks. Pärast väärtuse määramist võiksin tagasihelistamisse lisada täiendavaid funktsioone t1. Lisasin oma postMessagetagasihelistamisse, nii et see ei blokeeri virna:

Ja siin on main.jskood:

Sellel versioonil on probleem.

Muidugi - kuna ma olen veebitöötajate jaoks uus, ei olnud ma esialgu seda kaalunud. Kuid kui funktsiooni mitu käiku trükiti 0, arvasin, et midagi pole korras.

Kui printisin summad seestpoolt, onmessagesain oma vastuse. Põhifunktsioon liikus sünkroonselt edasi ja ei oodanud töötajalt sõnumi tagasitulekut, nii et see arvutas keskmise enne veebitöötaja tegemist.

Kiire ja määrdunud lahendus on loenduri lisamine ja arvutuse tegemine alles siis, kui loendur on jõudnud maksimaalse väärtuseni. Nii et siin on uusmain.js:

Ja siin on tulemused:

main(10): 0.1

main(100) : 1.41

main(1000) : 13.082

Oh. Minu. Noh, see pole ju tore? Mis siin toimub?

Ohverdasin jõudluskontrolli, et sisse vaadata. Ma login nüüd t0ja t1 kui need on loodud, siis lihtsalt selleks, et näha, mis seal toimub.

Ja tulemused:

Selgub, et ka minu ootus t1arvutamise järele vahetult pärast seda t0oli ekslik. Põhimõtteliselt tähendab asjaolu, et midagi veebitöötajate kohta pole sünkroonset, see, et minu kõige põhilisemad eeldused minu koodi käitumise kohta ei pea enam paika. Seda on raske pimeala näha.

Vähe sellest, isegi tulemused , mille eest sain main(10)ja main(100)mis algselt mind väga õnnelikuks ja enesekindlalt rõõmustasid, ei olnud sellised, millele saaksin loota.

Veebitöötajate asünkroonsus muudab nad ka ebausaldusväärseks puhuks, kuidas asjad meie tavalises virnas käituvad. Seega, kui setTimeoutveebitöötaja tulemuslikkuse mõõtmine annab huvitavaid tulemusi, ei vasta need meie küsimusele.

Õpiku meetod

Ma olin pettunud ... kas ma tõesti ei suutnud leida vanilje JS-i lahendust, mis oleks nii elegantne kui tõestaks mu kolleegi eksimist?

Ja siis sain aru - oli midagi, mida teha sain, aga see ei meeldinud mulle.

Ma võiksin helistada setTimeoutrekursiivselt.

Nüüd, kui ma helistan, helistab mainsee, testRunnermilliseid meetmeid mõõdab t0ja seejärel helistab tagasihelistamise. Seejärel käivitatakse tagasihelistamine, arvutatakse t1ja helistatakse testRunneruuesti, kuni see on jõudnud soovitud kõnede arvuni.

Selle koodi tulemused olid eriti üllatavad. Siin on mõned väljatrükid main(10)ja main(1000):

Funktsiooni 1000-kordse helistamise tulemused on oluliselt erinevad, võrreldes 10-kordse kutsumisega. Olen seda korduvalt proovinud ja saanud suures osas samu tulemusi, main(10)tulles 3–4 ms ja main(1000)ülaosaga 5 ms.

Kui aus olla, siis ma pole kindel, mis siin toimub. Otsisin vastust, kuid ei leidnud mõistlikku seletust. Kui loete seda ja teil on haritud arvamus selle kohta, mis toimub - tahaksin teid kuulda kommentaarides.

Proovitud ja õige meetod

Kuskil oma mõtteis teadsin alati, et see juhtub selleni ... Toretsevad asjad on toredad neile, kes neid saavad, kuid proovitud ja tõesed on ka siis alati olemas. Kuigi üritasin seda vältida, teadsin alati, et see on võimalus. setInterval.

See kood teeb triki mõnevõrra toore jõuga. setIntervalkäivitab funktsiooni korduvalt, oodates iga käigu vahel 50 ms, veendumaks, et virna on puhas. See on ebaõiglane, kuid testib täpselt seda, mida vajasin.

Ja ka tulemused olid paljutõotavad. Ajad näivad ühtivat minu algse ootusega - alla 1,5 ms.

Lõpuks sain selle juhtumi magama panna. Mul oli olnud mõned tõusud ja mõõnad ning minu osa ootamatutest tulemustest, kuid lõpuks oli oluline ainult üks asi - olin tõestanud, et teine ​​arendaja eksis! See oli minu jaoks piisavalt hea.

Kas soovite selle koodiga ringi mängida? vaadake seda siit: //github.com/NettaB/setTimeout-test