Tag Archives: html5

webmastering

Multithreading in web browser (Web Workers + jQuery)

This post is a kind of expe­ri­ment. It’s not a strict trans­la­tion of my previous post writ­ten in polish. I’m just curious how „uncle Google” will behave and if he send me some visi­tors :-) Notice that english is my second langu­age, so you will have to bear some my langu­age mista­kes. In case this project succe­edes, I will write some more english stuff.

I’m going to write about one of HTML5 new featu­res — Web Workers. These are inten­ded to provide some heavy compu­ta­tions in the back­gro­und and — what is most impor­tant — web brow­ser rema­ins respon­sive. It means that you won’t expe­rience any slowe­ness. When Web Worker ends its job, it noti­ces brow­ser about the outcome. The point is to use user’s CPU which is rarely full used in oppo­site to server’s.

By the way, multi­th­re­ading in brow­ser is not a big deal, Face­book uses it to provide you instant messa­ging or wall refre­shing without relo­ading the whole page.

Offi­cial draft of Web Workers API informs that workers have many restric­tions, such as no access to DOM or (in our case) jQuery object. You may think of it as a closed envi­ron­ment. Howe­ver you are able to commu­ni­cate in both ways via JSON and post­Mes­sage() function.

Take a look at our goal. We can control spaw­ning of new Workers, it state update inte­rvals and the moment of termi­na­tion. To ensure that our brow­ser supports Web Workers, we’ll use nice library specia­li­zed in chec­king a lot of modern HTML featu­res — Moder­nizr. In both cases user reads alert message if he can use workers or not.

Basicly we need two files: worker and main html file. Below is Java­Script code of timeworker.js:

var workerNo;
 
onmessage = function(e) {
    workerNo = e.data.workerNo;
    changeInterval(e.data.timer);
}
 
function changeInterval(interval) {
    setInterval("gimmeTime()",interval);
}
 
function gimmeTime() {
    postMessage({
        time:new Date().toUTCString(),
        workerNo:workerNo
    });
}
  1. Field wor­kerNo conta­ins worker id — inte­ger 1 or greater.
  2. onmes­sage is message recie­ved from the brow­ser. Notice that we can handle recie­ved data via e.data, not e alone!
  3. chan­ge­In­te­rval is func­tion which self calls gimme­Time() func­tion with given para­me­ter inte­rval (in microsecs).
  4. gim­me­Time sends brow­ser JSON message with its id and current time via post­Mes­sage() function.

Now it’s time for workersdemoeng.html (I’m listing only JS code):

var workers = new Array();
var counter = 0;
var bindedWorkers = '';
 
if (Modernizr.webworkers) {
    alert("Your browser supports Web Workers technology, you're free to test it!");
}
else {
    alert('Your browser does NOT support Web Workers technology. Install yourself a decent browser (FF, Chrome, Opera[?])!');
}
 
function addNotification(workerno) {
    var msg = $('<p>').appendTo('#notif');
    msg.text('Worker '+workerno+' updated state');
    msg.fadeOut(1500);
}
 
function fireUpWorker() {
    ++counter;
    var workerDiv = $('<div>').attr('id','workerdiv-'+counter).appendTo('#counters');
    var par = $('<p>').attr('id','worker-'+counter).appendTo(workerDiv);
    $('<input />').attr({
        type:'submit',
        value:'terminate',
        onClick:'terminateWorker('+counter+');return false;'
    }).appendTo(workerDiv);
    workers[counter] = new Worker('timeworker.js');
    var timer = $('#intrv').val();
    workers[counter].postMessage({timer:timer,workerNo:counter});
    bindWorkers();
}
 
function terminateWorker(workerNo) {
    workers[workerNo].terminate();
    $('#worker-'+workerNo).text('terminated!');
    $('#workerdiv-'+workerNo).fadeOut(5000);
}
 
function bindWorkers() {
    bindedWorkers += "workers["+counter+"].onmessage = function(event) {";
    bindedWorkers += "$('#worker-"+counter+"').text('Worker '+event.data.workerNo+' date : '+event.data.time);";
    bindedWorkers += "addNotification(event.data.workerNo);";
    bindedWorkers += "};";
    eval(bindedWorkers);
}
  1. We have 3 global fields: array wor­kers which holds all active workers, workers coun­ter and bin­de­dWor­kers which conta­ins ugly JS code to be execu­ted by eval() — the only way I know to force brow­ser to listen all workers, not only the last one created.
  2. add­No­ti­fi­ca­tion shows short 1,5 sec. noti­fi­ca­tion about worker state update.
  3. fireU­pWor­ker is the main func­tion. Firstly it creates a new div, and then para­graph and termi­nate button. Newly created worker rece­ives JSON message about its number and state update inte­rval. At the end, bin­dWor­kers() is executed.
  4. ter­mi­na­te­Wor­ker firstly kills certain wor­ker by worker.terminate(), and then hides div rela­ted to it.
  5. Finally the func­tion I’m least satis­fied of: bin­dWor­kers(). It handles what brow­ser should do with the rece­ived message from a worker. It works analo­gi­cally like the worker’s one: thro­ugh onmes­sage and e.data. To bin­de­dWor­kers string we’re appen­ding the code and execu­ting by eval(). The worst part is when we termi­nate some workers, this string conta­ins useless data (we could cut this out by some rege­xps, but I’m lazy ;-) ).

I have a big requ­est for the JS geeks: how to run onmes­sage code in a loop and without eval?

P.S.: It seems that I have found a bug in Chrome brow­ser: an attempt to add onclick attri­bute by jQuery didn’t work. You should use onClick instead :-)

webmastering

Wielowątkowość w przeglądarce (Web Workers + jQuery)

Przed nami ciekawe czasy w tech­no­lo­gii WWW. Powoli wcho­dzi HTML5 wpro­wa­dza­jąc całko­wi­cie nową jakość. Póki co strony w HTML5 mają charak­ter bardziej poka­zowy niż użyteczny. Ja jako maniak tech­no­lo­gii musia­łem zacząć się z tym wszyst­kim bawić. Na pierw­szy ogień idzie tech­no­lo­gia Web Workers [dalej WW].

WW to próba wpro­wa­dze­nia wielo­wąt­ko­wo­ści w prze­glą­darce WWW. W zało­że­niu zada­nia, które wyma­gają dużej mocy obli­cze­nio­wej wrzu­cane są do takiego workera. Gdy sobie skoń­czy liczyć, powia­da­mia prze­glą­darkę, a ta wyświe­tla wynik. Chodzi o to, że użyt­kow­nik strony nieko­niecz­nie ma ochotę cier­pieć zwol­nie­nie pracy prze­glą­darki w czasie kiedy proce­sor jest obcią­żony (ładnie się to nazywa respon­syw­ność przeglądarki).

Zresztą to nie jest jakaś super nowość. Z wątkami w tle spoty­ka­cie się na co dzień prze­glą­da­jąc Face­bo­oka (powia­do­mie­nia kto jest na czacie, nowe posty na tablicy bez prze­ła­do­wy­wa­nia itd.) Oficjalne API WW ma spore obostrze­nia. Worker to jakby zamknięte środo­wi­sko. Nie mamy dostępu do np. obiek­tów strony macie­rzy­stej (DOM, jQuery). Więcej pole­cam poczy­tać u Marco­osa.

Dzisiaj zrobimy coś, czego nie widzia­łem nigdzie w Inter­ne­cie póki co. Spró­bu­jemy dyna­micz­nie gene­ro­wać nowe wątki, kaso­wać je, a nawet regu­lo­wać często­tli­wość ich odpo­wie­dzi. Wszystko to będzie oczy­wi­ście ładnie okra­szone biblio­teką jQuery. Siedzia­łem nad tym pół niedzieli, więc uważajcie!

Najpierw rzuć­cie okiem na efekt finalny, a poni­żej wyja­śnie­nia co ciekaw­szych rzeczy.

Pierw­sze co robimy to spraw­dzamy czy prze­glą­darka użyt­kow­nika w ogóle obsłu­guje WW. Korzy­stamy z biblio­teki Moder­nizr i wyświe­tlamy alert informujący.

Druga sprawa to kod samego workera (timeworker.js):

var workerNo;
 
onmessage = function(e) {
    workerNo = e.data.workerNo;
    changeInterval(e.data.timer);
}
 
function changeInterval(interval) {
    setInterval("gimmeTime()",interval);
}
 
function gimmeTime() {
    postMessage({
        time:new Date().toUTCString(),
        workerNo:workerNo
    });
}
  1. Pole workerNo zawiera iden­ty­fi­ka­tor workera — liczbę od 1 w górę.
  2. onmes­sage to komu­ni­kat odebrany od prze­glą­darki. Zwra­camy uwagę na to, że do odebra­nych danych dobie­ramy się poprzez e.data, a nie samo e!
  3. chan­ge­In­te­rval to funk­cja usta­wia­jąca samo­wy­wo­ła­nie okre­ślo­nej funk­cji cyklicz­nie co podany czas w ms.
  4. gimme­Time to funk­cja wysy­ła­jące prze­glą­darce JSON z aktu­al­nym czasem i swoim iden­ty­fi­ka­to­rem za pomocą funk­cji post­Mes­sage().

Teraz czas na kod workersdemo.html (podaję sam kod Java­Script bo szkoda miejsca).

var workers = new Array();
var licznik = 0;
var bindedWorkers = '';
 
if (Modernizr.webworkers) {
    alert('Twoja przeglądarka wspiera Web Workers, możesz śmiało testować!');
}
else {
    alert('Twoja przeglądarka NIE wspiera Web Workers, demo nie będzie działało. Zainstaluj sobie prawdziwą przeglądarkę (FF, Chrome, Opera[?])!');
}
 
function addNotification(workerno) {
    var msg = $('<p>').appendTo('#notif');
    msg.text('Worker '+workerno+' updated state');
    msg.fadeOut(1500);
}
 
function fireUpWorker() {
    ++licznik;
    var workerDiv = $('<div>').attr('id','workerdiv-'+licznik).appendTo('#liczniki');
    var par = $('<p>').attr('id','worker-'+licznik).appendTo(workerDiv);
    $('<input />').attr({
        type:'submit',
        value:'terminate',
        onClick:'terminateWorker('+licznik+');return false;'
    }).appendTo(workerDiv);
    workers[licznik] = new Worker('timeworker.js');
    var timer = $('#intrv').val();
    workers[licznik].postMessage({timer:timer,workerNo:licznik});
    bindWorkers();
}
 
function terminateWorker(workerNo) {
    workers[workerNo].terminate();
    $('#worker-'+workerNo).text('terminated!');
    $('#workerdiv-'+workerNo).fadeOut(5000);
}
 
function bindWorkers() {
    bindedWorkers += "workers["+licznik+"].onmessage = function(event) {";
    bindedWorkers += "$('#worker-"+licznik+"').text('Worker '+event.data.workerNo+' date : '+event.data.time);";
    bindedWorkers += "addNotification(event.data.workerNo);";
    bindedWorkers += "};";
    eval(bindedWorkers);
}
  1. Mamy 3 pola globalne: tablicę workers trzy­ma­jącą wszyst­kich aktyw­nych worke­rów, licz­nik worke­rów i binde­dWor­kers zawie­ra­jący brzydki kod Java­Script do wyko­na­nia poprzez eval() — jedyny znany mi sposób na zmusze­nie prze­glą­darki żeby słuchała wszyst­kich aktyw­nych worke­rów, a nie tylko ostat­niego dodanego.
  2. addNo­ti­fi­ca­tion wyrzuca użyt­kow­ni­kowi komu­ni­kat o odebra­niu wiado­mo­ści od workera, znika po 1,5 sek.
  3. fireU­pWor­ker (nie chciało mi się wysi­lać na lepszą nazwę ;-) ) to główna funk­cja. Najpierw tworzy nowe elementy HTML: div, w nim para­graf na komu­ni­kat z workera i przy­cisk zabi­ja­jący. Nowo stwo­rzo­nemu worke­rowi wysy­łam komu­ni­kat w JSON co jaki czas ma się odświe­żać i jaki ma numer. Na koniec wywo­łuję funk­cję bindWor­kers().
  4. termi­na­te­Wor­ker najpierw zabija okre­ślo­nego workera poprzez worker.terminate(), a następ­nie ukrywa zwią­za­nego z nim diva.
  5. Na koniec funk­cja, z której jestem najmniej zado­wo­lony: bindWor­kers(). Zajmuje się usta­wia­niem co prze­glą­darka ma robić w przy­padku otrzy­ma­nia komu­ni­katu od workera. Działa to analo­gicz­nie jak poprzed­nio. Poprzez onmes­sagee.data. Do pola binde­dWor­kers doda­jemy kod w postaci ciągu i urucha­miamy evalem.

Tutaj mam prośbę dla znaw­ców. Doda­wa­nie onmes­sage w pętli dla poszcze­gól­nych worke­rów osobno nie dzia­łało. Jak zała­twić problem bez używa­nia evala?

P.S.: Znala­złem chyba buga w Chrome: Próby doda­nia atry­butu onclick poprzez jQuery nic nie dają. Należy używać onClick :-)