JavaScript timers – naše staré hodiny, bijí čtyři hodiny

Absolutním Cimrmanovým rýmem začínám další ze série článků o Javascriptu. V něm bych chtěl rozebrat pár postřehů při práci s časovači (timery) v JavaScriptu. Ty se používají k lecčemu – při jQuery animacích, zobrazování aktuálního času, periodickém dotazováním serveru atp. Intuitivně jsme vždycky tušili, že jejich časování nemusí být úplně přesné, ale přesto jsme hrubě podcenili význam pro aplikaci, pro kterou je aktuální čas zásadní.

Stáli jsme před relativně jednoduchým problémem. Odpočítávat čas do okamžiku T a vypočítávat slevu v ceně na základě času, který do okamžiku T zbývá. Samozřejmě všechny údaje (ať čas nebo cena) musely být u všech klientů naprosto stejné a musely se měnit každou vteřinu. Tento jednoduchý problém nás ale docela potrápil a proto vznikl tento článek, který by měl zachytit problémy a jejich řešení.

Všechny problémy byly způsobeny dvěma příčinami. Ta první je, že každý počítač klienta má odlišný čas a přes všechny synchronizace, které operační systémy provádí.. Nezanedbatelná část klientů má čas posunutý zásadním způsobem v řádech minut nebo i hodin (a to přesto, že se pohybujeme ve stejném časovém pásmu).

Druhou je nepřesnost časovačů v prohlížečích. Pro ilustraci jsem připravil jednoduchou testovací stránku, kterou si sami můžete spustit v různých prohlížečích. Především v těch, kde je horší implementace JavaScriptu (starší verze prohlížečů nebo Internet Explorer), můžeme během první minuty lehce nasbírat zpoždění i několika vteřin.

Timer tester

Vysvětlení důvodu, proč se tak děje se můžete dočíst v článku Johna Resiga, kde rozebírá princip toho jak časovače v prohlížečích fungují. Především je to dáno tím, že veškeré zpracování probíhá v jednom vlákně, takže v případě, že v danou chvíli prohlížeč vyřizuje jiný kus kódu (např. kód ošetřující kliknutí myši), spuštění časovače musí počkat (respektive se zafrontuje).

Z našich chyb jsme si vzali několik poučení

browser má vždy jiný čas než server

Nikdy nepracujte přímo s new Date() při vypočítávání zbývajícího času. Jediný způsob, jak synchronizovat čas všech klientů, je dostat na klienta aktuální serverový čas (i tak nebudete úplně přesní, protože mezi vložením času do HTTP response a zpracování HTTP response na klientovi nějaký čas – byť minimální – uběhne). Na klientu pak vypočtete odchylku systémového času klienta a systémového času serveru a o tuto odchylku upravujte všechna volání new Date() v logice klienta (nebo ještě lépe toto chování extrahujte do nějaké metody).

Příklad:

na periodu časovačů se při výpočtech nelze spolehnout

V logice na klientu nikdy nepočítejte s intervalem, který řídí spouštění aktualizační logiky, ale s aktuálním systémovým časem upraveným o odchylku. Přestože je to lákavé, odpočítávání času by nemělo fungovat tak, že si nastavíte timer na 1 sekundový interval a při zavolání metody z proměnné obsahující počet zbývajících sekund odečtete jedničku. Takto odpočet nebude vůbec přesný.

Řešením je přepsat aktualizační metodu tak, že vždy nanovo vypočte počet zbývajících sekund odečtením cílového času udaného serverem od aktuálního času ošetřeného o odchylku.

Závěrem

Když si ten článek po sobě čtu, tak si říkám: “Otec Fura objevil Ameriku”. Je zvláštní jak hodně dokáže být člověk i po těch letech naivní.

Reference

Podělte se s ostatními:

  • Digg
  • del.icio.us
  • Technorati
  • Diigo
  • DZone
  • FriendFeed
  • Google Bookmarks
  • LinkedIn
  • Reddit
  • RSS
  • StumbleUpon
  • Twitter

Související články:

  1. JavaScript Closures – překvapení Java programátora
Ohodnoťte článek:
Takovéhle články už radši ne!Nic nového pod sluncem.Průměr - obsahuje zajímavé střípky informací.Hodnotný článek - lecos nového jsem se dozvěděl.Skvělý článek - informace se mi dost hodí. (1 hlasů, průměrně: 5.00 z 5)
Loading ... Loading ...

5 Responses to “JavaScript timers – naše staré hodiny, bijí čtyři hodiny”

  1. NkD says:

    Ten zaver do kamene tesat. Takhle naivni jsem i pres svuj seniorsky vek docela casto. Sam pred sebou se omlouvam slovy “Po bitve je kazdej general”.

    Diky za clanek.

  2. Martin Jansa says:

    Nezouseli jste zmerit, jestli doba “mezi vložením času do HTTP response a zpracování HTTP response” odpovida zhruba polovine casu od odeslani requestu z klienta po zpracovani response? Pak by se to mozna mohlo ješte upravovat o tuto konstantu.

  3. Otec Fura says:

    Abych pravdu řekl, nezkoušeli. Současná přesnost je pro nás dostačující – rozdíly mezi uživateli nejsou už rozpoznatelné. Ale je to zajímavý nápad.

  4. Jakub Vrána says:

    O zobrazení serverového času u klienta jsem před časem také psal: http://php.vrana.cz/zobrazeni-serveroveho-casu.php

    Doplním jen, že pokud bychom chtěli vyřešit nepřesnost časovače, dalo by se místo setInterval zavolat vždy setTimeout s vypočteným časem (mírně nižším než 1 s).

  5. Otec Fura says:

    Podle mého názoru setTimeout trpí úplně stejnými nešvary jako setTimeout – princip je úplně stejný – jen setInterval se provádí opakovaně a setTimeout jedinkrát. Na vině je jednovláknovost prohlížeče, se kterou voláním jiné metody nic nezískáme. Každopádně díky za reakci.

  6. Otec Fura says:

    Pochopil jsem co jste měl na mysli. Spíš na to reaguji ve smyslu – přestože v daném okamžiku víte, že máte zpoždění 50ms a načasujete tedy další timer na 950ms místo 1000s nikde nemáte zaručeno, že se vám timer skutečně zavolá za 950ms a ne třeba až za 1300ms. Podle mého názoru tím daný problém neřešíte.

  7. Jakub Vrána says:

    Řeší to problém narůstajícího zpoždění.

  8. Otec Fura says:

    Narůstajícího zpoždění možná, nikoliv však přesné zobrazení času v konkrétním okamžiku. Navíc se mi zdá, že třeba Firefox podobnou logiku uplatňuje již na setInterval – když na testovacím skriptu sleduji zpoždění, tak pokud naroste, tak se po nějaké chvíli zase sníží. Ale možná je to jen klam.

    Koukal jsem na ten kód, co uvádíte, a nepovedlo se mi vyextrahovat, jak by se choval v případě, že by zpoždění přesáhlo celý sledovaný interval. Tzn. kdyby v okamžiku N bylo zpoždění 0ms a v okamžiku N+1 třebas +1300ms, přičemž námi sledovaná perioda je 1000ms. Pak byste se totiž dostal do situace, kdy byste potřeboval další timeout nastavit na -300ms, což nepůjde.

  9. Jakub Vrána says:

    Na konci článku to zmiňuji. Řeším to žertem, v praktické implementaci by to asi chtělo funkci zavolat ihned znovu.

Leave a Reply