1 //** Start JS Lint Settings ** 2 /*globals self,Worker,Blob,rtn*/ 3 /*jslint vars:true, devel:true, browser:true, evil:true*/ 4 //** End JS Lint Settings ** 5 6 /* 7 * Title: WebHamsters 8 * Description: Javascript library to add multi-threading support to javascript by exploiting concurrent web workers 9 * Author: Austin K. Smith 10 * Contact: austin@asmithdev.com 11 * Copyright: 2015 Austin K. Smith - austin@asmithdev.com 12 * License: Artistic License 2.0 13 */ 14 var hamsters = { 15 version: '2.7', 16 debug: false, 17 cache: false, 18 maxThreads: Math.ceil((navigator.hardwareConcurrency || 1) * 1.25), 19 tools: {}, 20 wheel: { 21 legacy: false, 22 queue: { 23 running: [], 24 pending: [] 25 }, 26 tasks: [], 27 errors: [], 28 setup: {} 29 } 30 }; 31 32 /** 33 * @description: Initializes and sets up library functionality 34 * @method wakeUp 35 * @return 36 */ 37 hamsters.wheel.wakeUp = function() { 38 "use strict"; 39 40 /** 41 * @description: Detect Internet Explorer by Version IE10 and below 42 * @method isIE 43 * @param {integer} version 44 * @return CallExpression 45 */ 46 hamsters.tools.isIE = function(version) { 47 return (new RegExp('msie' + (!isNaN(version) ? ('\\s'+version) : ''), 'i').test(navigator.userAgent)); 48 }; 49 50 /** 51 * Description 52 * @description: Detect browser support for web workers 53 * @method isLegacy 54 * @return 55 */ 56 hamsters.wheel.setup.isLegacy = function(callback) { 57 if(!window.Worker || navigator.userAgent.indexOf('Kindle/3.0') !== -1 || navigator.userAgent.indexOf('Mobile/8F190') !== -1 || navigator.userAgent.indexOf('IEMobile') !== -1 || hamsters.tools.isIE(10)) { 58 hamsters.wheel.legacy = true; 59 } else if(navigator.userAgent.toLowerCase().indexOf('firefox') !== -1) { 60 window.firefox = window.firefox || true; 61 } 62 if(callback) { 63 callback(hamsters.wheel.legacy); 64 } 65 }; 66 67 /** 68 * @description: Return browser support for library 69 * @method isLegacy 70 * @return MemberExpression 71 */ 72 hamsters.isLegacy = function() { 73 return hamsters.wheel.legacy; 74 }; 75 76 /** 77 * @description: Method for checking wheel error log 78 * @method checkErrors 79 * @return ObjectExpression 80 */ 81 hamsters.checkErrors = function() { 82 var errors = hamsters.wheel.errors || []; 83 return { 84 'msg': 'There are currently ' + errors.length + ' errors captured in the wheel', 85 'total': errors.length, 86 'errors': errors 87 }; 88 }; 89 90 /** 91 * @description: Splits an array into equal sized subarrays for individual workers 92 * @constructor 93 * @method splitArray 94 * @param {array} array - incoming array to be split 95 * @param {integer} n - total subarrays 96 * @return ArrayExpression 97 */ 98 hamsters.tools.splitArray = function(array, n) { 99 if(array.length && !array.slice) { 100 array = hamsters.wheel.normalizeArray(array); 101 } 102 var tasks = []; 103 var i = 0; 104 if(array) { 105 var len = array.length; 106 var size = Math.ceil(len/n); 107 while (i < len) { 108 tasks.push(array.slice(i, i += size)); 109 } 110 return tasks; 111 } 112 return []; 113 }; 114 115 /** 116 * @description: Generates a worker which generates an array of random numbers for testing 117 * @constructor 118 * @function randomArray 119 * @method randomArray 120 * @param {integer} count - array size 121 * @param {function} callback - callback when array ready 122 * @return 123 */ 124 hamsters.tools.randomArray = function(count, callback) { 125 if(!count || !callback) { 126 hamsters.wheel.errors.push({ 127 'msg': 'Unable to generate random array, missing required params' 128 }); 129 return; 130 } 131 var params = { 132 'count': count 133 }; 134 hamsters.run(params, function() { 135 var total = params.count; 136 var i = 0; 137 while(i < total) { 138 rtn.data[rtn.data.length] = Math.round(Math.random() * (100 - 1) + 1); 139 i+=1; 140 } 141 }, function(output) { 142 if(callback) { 143 callback(output); 144 } 145 }, 1, false, 'Int32', false); 146 }; 147 148 /** 149 * Description 150 * @method checkCache 151 * @param {string} fn 152 * @param {array} input 153 * @param {string} dataType 154 * @return 155 */ 156 hamsters.wheel.checkCache = function(hash, dataType) { 157 var item; 158 for (var i = 0, len = sessionStorage.length; i < len; i+=1) { 159 item = JSON.parse(sessionStorage[i]); 160 if(item && item['#'] === hash && item.dT === dataType) { 161 var rtn = item.oP; 162 if(dataType && !hamsters.wheel.legacy) { 163 rtn = hamsters.wheel.processDataType(dataType, item.oP); 164 } 165 return rtn; 166 } 167 } 168 }; 169 170 /** 171 * Description 172 * @method memoize 173 * @param {string} fn 174 * @param {array} input 175 * @param {array} output 176 * @param {string} dataType 177 * @return 178 */ 179 hamsters.wheel.memoize = function(fn, input, output, dataType) { 180 var hash = hamsters.wheel.hashResult({'func': fn, 'dT': dataType, 'input': input}); 181 if(hamsters.wheel.checkCache(hash, dataType)) { 182 return; 183 } 184 try { 185 sessionStorage.setItem(sessionStorage.length, JSON.stringify({'#': hash, 'dT': dataType, 'oP': output})); 186 } catch(eve) { 187 if(eve.name === 'QuotaExceededError') { 188 sessionStorage.clear(); 189 try { 190 sessionStorage.setItem(sessionStorage.length, JSON.stringify({'#': hash, 'dT': dataType, 'oP': output})); 191 } catch(e) { //Do nothing, can't cache this result..too large 192 return; 193 } 194 } 195 } 196 }; 197 198 /** 199 * Description 200 * @method compareArrays 201 * @param {array} array1 202 * @param {array} array2 203 * @return CallExpression 204 */ 205 hamsters.wheel.compareArrays = function (array1, array2) { 206 if (array1.length !== array2.length) { 207 return false; 208 } 209 return array1.every(function (el, i) { 210 return (el === array2[i]); 211 }); 212 }; 213 214 /** 215 * @description: Setups dom objects for web worker use with library boilerplate 216 * @constructor 217 * @function populateElements 218 * @method populateElements 219 * @param {integer} count 220 * @param {function} callback - optional callback once thread boilerplates setup 221 * @return 222 */ 223 hamsters.wheel.setup.populateElements = function(count, callback) { 224 for (var i = 0, len = count; i < len; i+=1) { 225 hamsters.wheel.setup.getOrCreateElement(i); 226 } 227 if(callback) { 228 callback.call(); 229 } 230 }; 231 232 /** 233 * @description: Setups dom objects for web worker use with library boilerplate 234 * @constructor 235 * @method getOrCreateElement 236 * @param {integer} id - thread # to populate 237 * @return script 238 */ 239 hamsters.wheel.setup.getOrCreateElement = function(id) { 240 var script = (document.getElementById('hamster'+id) || null); 241 if(!script) { 242 var work = hamsters.wheel.giveHamsterWork(); 243 script = document.createElement('script'); 244 script.type = ('javascript/worker'); 245 script.id = ('hamster'+id); 246 script.text = '(' + String(work) + '());'; 247 document.getElementsByTagName('head')[0].appendChild(script); 248 return script; 249 } 250 return script; 251 }; 252 253 /** 254 * @description: Creates boiler plate logic for worker thread 255 * @constructor 256 * @method giveHamsterWork 257 * @return work 258 */ 259 hamsters.wheel.giveHamsterWork = function() { 260 var work = function() { 261 var params; 262 /** 263 * Description 264 * @method respond 265 * @param {object} rtn 266 * @param {string} msg 267 * @return 268 */ 269 var respond = function(rtn, msg) { 270 if(params.dataType && params.dataType !== "na") { 271 var output = processDataType(params.dataType, rtn.data); 272 rtn.data = output.buffer; 273 rtn.dataType = params.dataType; 274 self.postMessage({ 275 'results': rtn || null, 276 'msg': msg || '' 277 }, [output.buffer]); 278 } else { 279 self.postMessage({ 280 'results': rtn || null, 281 'msg': msg || '' 282 }); 283 } 284 }; 285 /** 286 * Description 287 * @method processDataType 288 * @param {string} dataType 289 * @param {array} buffer 290 * @return arr 291 */ 292 var processDataType = function(dataType, buffer) { 293 if (dataType === 'uint32') { 294 return new Uint32Array(buffer); 295 } 296 if (dataType === 'uint16') { 297 return new Uint16Array(buffer); 298 } 299 if (dataType === 'uint8') { 300 return new Uint8Array(buffer); 301 } 302 if (dataType === 'uint8clamped') { 303 return new Uint8ClampedArray(buffer); 304 } 305 if (dataType === 'int32') { 306 return new Int32Array(buffer); 307 } 308 if (dataType === 'int16') { 309 return new Int16Array(buffer); 310 } 311 if (dataType === 'int8') { 312 return new Int8Array(buffer); 313 } 314 if (dataType === 'float32') { 315 return new Float32Array(buffer); 316 } 317 if (dataType === 'float64') { 318 return new Float64Array(buffer); 319 } 320 return buffer; 321 }; 322 323 /** 324 * Description 325 * @method onmessage 326 * @param {object} e 327 * @return 328 */ 329 self.onmessage = function(e) { 330 var rtn = { 331 'success': true, 332 'data': [] 333 }; 334 params = e.data; 335 if(typeof params === 'string') { 336 params = JSON.parse(e.data); 337 } 338 if(params.dataType && params.dataType !== "na" && typeof params.array === 'object') { 339 params.array = processDataType(params.dataType, params.array); 340 } 341 if(params.fn) { 342 var fn = eval('('+params.fn+')'); 343 if(fn && typeof fn === 'function') { 344 fn(); 345 respond(rtn); 346 } else { 347 rtn.success = false; 348 rtn.error = 'Missing function'; 349 rtn.msg = 'Error encounted check errors for details'; 350 respond(rtn); 351 } 352 } 353 }; 354 }; 355 return work; 356 }; 357 358 /** 359 * @description: Sorts an array of objects based on incoming property param 360 * @constructor 361 * @method sort 362 * @param {string} property - property to sort by 363 * @return FunctionExpression 364 */ 365 hamsters.wheel.sort = function(arr, order) { 366 if(order === 'desc') { 367 return Array.prototype.sort.call(arr, function(a, b) { 368 return b - a; 369 }); 370 } 371 if(order === 'asc') { 372 return Array.prototype.sort.call(arr, function(a, b) { 373 return a - b; 374 }); 375 } 376 if(order === 'ascAlpha') { 377 return arr.sort(); 378 } 379 if(order === 'descAlpha') { 380 return arr.reverse(); 381 } 382 }; 383 384 /** 385 * @description: Takes an incoming sequential function and automatically splits the work across as many defined threads for paralell execution 386 * @constructor 387 * @method run 388 * @param {object} params - incoming params object for task 389 * @param {function} fn - Sequential function to execute 390 * @param {function} callback - task callback when all threads complete 391 * @param {integer} workers - total number of threads to use 392 * @param {boolean} aggregate - boolean aggregate individual thread outputs into final array 393 * @param {string} dataType 394 * @return 395 */ 396 hamsters.run = function(params, fn, callback, workers, aggregate, dataType, memoize, order) { 397 if(!params || !fn) { 398 return 'Error processing for loop, missing params or function'; 399 } 400 var taskid = hamsters.wheel.tasks.length; 401 workers = workers || hamsters.maxThreads; 402 hamsters.wheel.newTask(taskid, workers, order, dataType, fn, callback); 403 var task = hamsters.wheel.tasks[taskid]; 404 callback = (callback || null); 405 var hamsterfood = {'array':[]}; 406 hamsterfood.fn = fn.toString(); 407 if(dataType) { 408 dataType = dataType.toLowerCase(); 409 } else { 410 dataType = "na"; 411 } 412 if(hamsters.cache && params.array && params.array.length !== 0) { 413 memoize = memoize || true; 414 var hash = hamsters.wheel.hashResult({'func': fn, 'dT': "na", 'input': params.array}); 415 var result = hamsters.wheel.checkCache(hash, dataType); 416 if(result && callback) { 417 setTimeout(function() { 418 callback(result); 419 hamsters.wheel.tasks[taskid] = null; //Clean up our task, not needed any longer 420 }, 4); 421 return; 422 } 423 } 424 var key; 425 for(key in params) { 426 if(params.hasOwnProperty(key)) { 427 if(key !== 'array') { 428 hamsterfood[key] = params[key]; 429 } 430 } 431 } 432 hamsterfood.dataType = dataType || null; 433 var workArray = params.array || null; 434 if(params.array && task.threads !== 1) { 435 workArray = hamsters.tools.splitArray(params.array, task.threads); //Divide our array into equal array sizes 436 } 437 var i = 0; 438 while(i < task.threads) { 439 if(workArray && task.threads !== 1) { 440 hamsters.wheel.newWheel(workArray[i], hamsterfood, aggregate, callback, taskid, i, null, null, memoize); 441 } else { 442 hamsters.wheel.newWheel(workArray, hamsterfood, aggregate, callback, taskid, i, null, null, memoize); 443 } 444 i+=1; 445 } 446 }; 447 448 hamsters.wheel.newTask = function(taskid, workers, order, dataType, fn, callback) { 449 hamsters.wheel.tasks.push({ 450 'id': taskid, 451 'workers': [], 452 'count': 0, 453 'threads': workers, 454 'input': [], 455 'dataType': dataType || null, 456 'fn': fn || null, 457 'output': [], 458 'order': order || null, 459 'callback': callback 460 }); 461 }; 462 463 hamsters.wheel.trackInput = function(task, inputArray, thread, taskid, hamsterfood) { 464 task.input.push({ 465 'input': inputArray, 466 'workerid': thread, 467 'taskid': taskid, 468 'params': hamsterfood, 469 'start': new Date().getTime() 470 }); 471 }; 472 473 hamsters.wheel.poolThread = function(queue, inputArray, hamsterfood, threadid, callback, taskid, aggregate, memoize) { 474 queue.pending.push({ 475 'memoize': memoize, 476 'input': inputArray, 477 'params': hamsterfood, 478 'workerid': threadid, 479 'callback': callback, 480 'taskid': taskid, 481 'aggregate': aggregate 482 }); 483 }; 484 485 hamsters.wheel.trackThread = function(task, queue, thread) { 486 task.workers[task.workers.length] = thread; //Keep track of threads scoped to current task 487 queue.running[queue.running.length] = thread; //Keep track of all currently running threads 488 }; 489 490 /** 491 * @description: Simulates threading for execution on devices that don't support workers 492 * @constructor 493 * @method legacyProcessor 494 * @param {object} food - Input params object 495 * @param {array} inputArray - Input array 496 * @param {function} callback - Callback function to return response 497 * @return 498 */ 499 hamsters.wheel.legacyProcessor = function(food, inputArray, callback) { 500 setTimeout(function() { 501 var params = food; 502 var rtn = { 503 'success': true, 504 'data': [] 505 }; 506 var respond = function(rtn) { 507 if(callback) { 508 callback(rtn); // Return legacy output 509 } 510 }; 511 if(params.fn) { 512 params.array = inputArray; 513 var fn = eval('('+params.fn+')'); 514 if(fn && typeof fn === 'function') { 515 try { 516 fn(); 517 respond(rtn); 518 } catch(exception) { 519 rtn.success = false; 520 rtn.error = exception; 521 rtn.msg = 'Error encounted check errors for details'; 522 respond(rtn); 523 } 524 } 525 } 526 }, 4); //4ms delay (HTML5 spec minimum), simulate threading 527 }; 528 529 /** 530 * @description: Creates web worker thread 531 * @constructor 532 * @method createHamster 533 * @param {integer} thread - Thread # 534 * @return ObjectExpression 535 */ 536 hamsters.wheel.createHamster = function(thread) { 537 var hamster = hamsters.wheel.setup.getOrCreateElement(thread); 538 var blob = hamsters.wheel.createBlob(hamster.textContent); 539 var uri = window.URL.createObjectURL(blob); 540 return {'worker': new Worker(uri), 'dataBlob': blob, 'blobUri': uri}; 541 }; 542 543 /** 544 * @description: Sends ajax request 545 * @constructor 546 * @method sendRequest 547 * @param {string} type - 'GET' || 'POST' 548 * @param {string} url - Url to connect to 549 * @param {string} responseType - 'ArrayBuffer','','blob','document','json','text' 550 * @param {string} callback - Callback to return response 551 * @return 552 */ 553 hamsters.wheel.sendRequest = function(type, url, responseType, callback) { 554 var xhr = new XMLHttpRequest(); 555 xhr.open(type, url); 556 xhr.responseType = responseType; 557 /** 558 * Description 559 * @method onload 560 * @return 561 */ 562 xhr.onload = function() { 563 callback(this.response); 564 }; 565 xhr.send(); 566 }; 567 568 /** 569 * @description: Creates array buffer by issuing a fake ajax request with response of arraybuffer 570 * @constructor 571 * @method fetchArrayBuffer 572 * @param {string} string - input params object 573 * @param {function} callback - callback function to return buffer to 574 * @return 575 */ 576 hamsters.wheel.fetchArrayBuffer = function(string, callback) { 577 var url = window.URL.createObjectURL(hamsters.wheel.createBlob(string)); 578 hamsters.wheel.sendRequest('GET', url, 'arraybuffer', function(arrayBuffer) { 579 if(callback) { 580 callback(arrayBuffer); 581 } 582 }); 583 }; 584 585 /** 586 * @description: Creates dataBlob for worker generation 587 * @constructor 588 * @method createBlob 589 * @param {string} textContent - Web worker boiler plate 590 * @return blob 591 */ 592 hamsters.wheel.createBlob = function(textContent) { 593 if(Blob) { 594 return new Blob([textContent], {type: 'application/javascript'}); 595 } 596 var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; 597 if(BlobBuilder) { //Fallback for browsers that don't support blob constructor 598 var blob = new BlobBuilder(); 599 blob.append([textContent], {type: 'application/javascript'}); 600 blob = blob.getBlob(); 601 return blob; 602 } 603 }; 604 605 /** 606 * @description: Aggregates individual hamster outputs into a single array 607 * @constructor 608 * @method aggregate 609 * @param {array} input - incoming array of subarrays 610 * @param {string} dataType 611 * @return output 612 */ 613 hamsters.tools.aggregate = function(input, dataType) { 614 if(!input) { 615 console.error("Missing array"); 616 return; 617 } 618 if(input.length > 20) { 619 return input; 620 } 621 if(dataType) { 622 return hamsters.wheel.aggregateTypedArrays(input, dataType); 623 } 624 return input.reduce(function(a, b) { 625 return a.concat(b); 626 }); 627 }; 628 629 /** 630 * @description: Get our nested output values from each task, return array of subarrays 631 * @constructor 632 * @method getOutput 633 * @param {array} output - incoming task output 634 * @param {boolean} aggregate 635 * @param {string} dataType 636 * @return rtn 637 */ 638 hamsters.wheel.getOutput = function(output, aggregate, dataType) { 639 if(aggregate) { 640 return hamsters.tools.aggregate(output, dataType); 641 } else { 642 return output; 643 } 644 }; 645 646 /** 647 * @description: Process next item in queue 648 * @constructor 649 * @method processQueue 650 * @param {object} hamster - Most recently finished web worker, for reuse 651 * @param {blob} dataBlob 652 * @return 653 */ 654 hamsters.wheel.processQueue = function(hamster, dataBlob) { 655 var item = hamsters.wheel.queue.pending.shift(); //Get and remove first item from queue 656 if(item) { 657 hamsters.wheel.newWheel(item.input, item.params, item.aggregate, item.callback, item.taskid, item.workerid, hamster, dataBlob, item.memoize); //Assign most recently finished thread to queue item 658 } 659 }; 660 661 /** 662 * @description: Cleans up memory used by dataBlob 663 * @constructor 664 * @method terminateHamster 665 * @param {object} dataBlob - dataBlob to free from memory, critical for IE11 support 666 * @return 667 */ 668 hamsters.wheel.terminateHamster = function(dataBlob) { 669 if(dataBlob) { 670 if(dataBlob.blob.close) { 671 dataBlob.blob.close(); 672 } else if(dataBlob.blob.msClose) { 673 dataBlob.blob.msClose(); 674 } else if(dataBlob.blob.slice) { 675 dataBlob.blob = dataBlob.blob.slice(0,0); 676 } 677 window.URL.revokeObjectURL(dataBlob.uri); 678 } 679 }; 680 681 /** 682 * @description: Handle response from worker thread, setup error handling 683 * @constructor 684 * @method trainHamster 685 * @param {integer} id - global wheel threadid 686 * @param {boolean} aggregate - boolean aggregate individual thread outputs into final array 687 * @param {function} callback - task callback when all hamsters complete 688 * @param {integer} taskid - global wheel task id 689 * @param {integer} workerid - worker wheel threadid 690 * @param {object} hamster - web worker 691 * @param {blob} dataBlob 692 * @return 693 */ 694 hamsters.wheel.trainHamster = function(id, aggregate, callback, taskid, workerid, hamster, dataBlob, memoize) { 695 696 /** 697 * @description: Runs when a hamster (thread) finishes it's work 698 * @constructor 699 * @method onmessage 700 * @param {object} e - Web Worker event object 701 * @return 702 */ 703 hamster.onmessage = function(e) { 704 var queue = hamsters.wheel.queue; 705 if(queue.pending.length === 0) { 706 setTimeout(function() { 707 hamster.terminate(); //Kill the thread only if no items waiting to run (20-22% performance improvement observed during testing, repurposing threads vs recreating them) 708 hamsters.wheel.terminateHamster(dataBlob); 709 }, 4); 710 } 711 queue.running.splice(queue.running.indexOf(id), 1); //Remove thread from running pool 712 var task = hamsters.wheel.tasks[taskid]; 713 if(!task) { 714 hamsters.wheel.errors = hamsters.wheel.errors.concat({ 715 'timeStamp': e.timeStamp, 716 'msg': 'Error, unable to match thread to task, throwing exception', 717 'taskid': taskid, 718 'workerid': workerid, 719 'aggregate': aggregate, 720 'callback': callback 721 }); 722 console.error('Fatal Exception, unable to match thread #'+workerid+' to task #'+ taskid + ', cannot continue. Check errors for more details'); 723 return; 724 } 725 task.workers.splice(task.workers.indexOf(workerid), 1); //Remove thread from task running pool 726 var results = e.data.results; 727 if(results.dataType && typeof results.data === 'object') { 728 results.data = hamsters.wheel.processDataType(results.dataType, results.data); 729 } 730 task.output[workerid] = results.data; 731 if(task.workers.length === 0 && task.count === task.threads) { 732 var output = hamsters.wheel.getOutput(task.output, aggregate, results.dataType); 733 if(callback) { 734 if(hamsters.debug) { 735 console.info('Execution Complete! Elapsed: ' + ((e.timeStamp - task.input[0].start)/1000) + 's'); 736 } 737 if(task.order) { 738 callback(hamsters.wheel.sort(output, task.order)); 739 } else { 740 callback(output); 741 } 742 hamsters.wheel.tasks[taskid] = null; //Clean up our task, not needed any longer 743 if(hamsters.cache && memoize !== false) { 744 var inputArray = task.input[0].input; 745 if(output.length > 0 && !results.dataType) { 746 setTimeout(hamsters.wheel.memoize(task.fn, inputArray, output, 'na'), 4); 747 } else if(output.length > 0 && results.dataType) { 748 setTimeout(hamsters.wheel.memoize(task.fn, inputArray, hamsters.wheel.normalizeArray(output), results.dataType), 4); 749 } 750 } 751 } 752 } else if(hamsters.debug === 'verbose') { 753 console.info('Hamster #' + id + ' finished ' + '@ ' + e.timeStamp); 754 } 755 if(queue.pending.length !== 0) { 756 hamsters.wheel.processQueue(hamster, dataBlob); 757 } 758 }; 759 760 /** 761 * @description: Setup error handling 762 * @constructor 763 * @method onerror 764 * @param {object} e - Web Worker event object 765 * @return 766 */ 767 hamster.onerror = function(e) { 768 hamster.terminate(); //Kill the thread 769 var msg = 'Error Hamster #' + id + ': Line ' + e.lineno + ' in ' + e.filename + ': ' + e.message; 770 hamsters.wheel.errors = hamsters.wheel.errors.concat({ 771 'msg': msg 772 }); 773 console.error(msg); 774 }; 775 }; 776 777 /** 778 * @description: Normalizes typed array into normal array 779 * @constructor 780 * @method normalizeArray 781 * @param {object} input - typedArray input 782 * @return arr 783 */ 784 hamsters.wheel.normalizeArray = function(input) { 785 var arr = []; 786 for (var n = 0, len = input.length; n < len; n++) { 787 arr.push(input[n]); 788 } 789 return arr; 790 }; 791 792 /** 793 * Description 794 * @method aggregateTypedArrays 795 * @param {array} input 796 * @param {string} dataType 797 * @return output 798 */ 799 hamsters.wheel.aggregateTypedArrays = function(input, dataType) { 800 var output; 801 var length = 0; 802 var offset = 0; 803 for (var i = 0, len = input.length; i < len; i+=1) { 804 length += input[i].length; 805 } 806 output = hamsters.wheel.processDataType(dataType, length); 807 for (var n = 0, len2 = input.length; n < len2; n+=1) { 808 output.set(input[n], offset); 809 offset += input[n].length; 810 } 811 return output; 812 }; 813 814 /** 815 * @description: Converts array buffer or normal array into a typed array 816 * @constructor 817 * @method processDataType 818 * @param {string} dataType - dataType config param 819 * @param {object} buffer - buffer object or normal array 820 * @return arr 821 */ 822 hamsters.wheel.processDataType = function(dataType, buffer) { 823 if (dataType === 'uint32') { 824 return new Uint32Array(buffer); 825 } 826 if (dataType === 'uint16') { 827 return new Uint16Array(buffer); 828 } 829 if (dataType === 'uint8') { 830 return new Uint8Array(buffer); 831 } 832 if (dataType === 'uint8clamped') { 833 return new Uint8ClampedArray(buffer); 834 } 835 if (dataType === 'int32') { 836 return new Int32Array(buffer); 837 } 838 if (dataType === 'int16') { 839 return new Int16Array(buffer); 840 } 841 if (dataType === 'int8') { 842 return new Int8Array(buffer); 843 } 844 if (dataType === 'float32') { 845 return new Float32Array(buffer); 846 } 847 if (dataType === 'float64') { 848 return new Float64Array(buffer); 849 } 850 return buffer; 851 }; 852 853 hamsters.wheel.generateHash = function(string) { 854 string = String(string); 855 var hash = 0; 856 for (var i = string.length - 1; i >= 0; i--) { //Shift 5 bits 857 hash += (((hash << 5) - hash) + string.charCodeAt(i)) & 0xFFFFFFFF; 858 } 859 return hash; 860 }; 861 862 hamsters.wheel.hashResult = function(obj) { 863 var result = 0; 864 for(var key in obj) { 865 if(obj.hasOwnProperty(key)) { 866 if(typeof obj[key] === 'object' && obj[key].length && !obj[key].slice) { 867 result += hamsters.wheel.generateHash(key + String(hamsters.wheel.normalizeArray(obj[key]))); 868 } else { 869 result += hamsters.wheel.generateHash(key + obj[key]); 870 } 871 } 872 } 873 return result; 874 }; 875 876 /** 877 * @description: Sends message to worker thread to invoke execution 878 * @constructor 879 * @method feedHamster 880 * @param {object} hamster - web worker 881 * @param {object} food - params object for worker 882 * @param {array} inputArray 883 * @return 884 */ 885 hamsters.wheel.feedHamster = function(hamster, food, inputArray) { 886 if(inputArray && food.dataType) { //Transferable object transfer if using typed array 887 food.array = hamsters.wheel.processDataType(food.dataType, inputArray); 888 } else if(inputArray) { 889 food.array = inputArray; 890 } 891 if(food.array && food.array.buffer) { 892 var buffer = food.array.buffer; 893 food.array = buffer; 894 hamster.postMessage(food, [buffer]); 895 } else if((window.chrome || window.firefox) && food.array.length <= 35000000) { //Stringify data for chrome/firefox on less than 35M array size, good performance boost 896 hamster.postMessage(JSON.stringify(food)); 897 } else { 898 hamster.postMessage(food); //Use structured cloning for Safari/IE or for data sets > 40M+ 899 } 900 }; 901 902 /** 903 * @description: Creates new worker thread with body of work to be completed 904 * @constructor 905 * @method newWheel 906 * @param {array} inputArray 907 * @param {object} hamsterfood - incoming params object for worker 908 * @param {boolean} aggregate - boolean aggregate individual thread outputs into final array 909 * @param {function} callback - task callback when all hamsters complete 910 * @param {integer} taskid - global wheel task id 911 * @param {integer} threadid - global wheel threadid 912 * @param {object} hamster - web worker 913 * @param {blob} dataBlob 914 * @return 915 */ 916 hamsters.wheel.setup.isLegacy(function(legacy) { 917 if(legacy) { 918 hamsters.wheel.newWheel = function(inputArray, hamsterfood, aggregate, callback, taskid, threadid, hamster, dataBlob, memoize) { 919 var task = hamsters.wheel.tasks[taskid]; 920 if(!task) { 921 hamsters.wheel.errors.push({ 922 'msg': 'Error, unable to match thread to task, throwing exception', 923 'params': hamsterfood, 924 'aggregate': aggregate, 925 'callback': callback 926 }); 927 console.error('Error, unable to match thread to task ' + taskid + ', throwing exception. Check errors for more details'); 928 return; 929 } 930 var debug = hamsters.debug; 931 hamsters.wheel.legacyProcessor(hamsterfood, inputArray, function(output) { 932 task.count++; //Thread finished 933 task.output[threadid] = output.data; 934 if(task.count === task.threads) { //Task complete get output and return 935 var rtn = hamsters.wheel.getOutput(task.output, aggregate, output.dataType); 936 if(callback) { 937 if(debug) { 938 console.info('Execution Complete! Elapsed: ' + ((new Date().getTime() - task.input[0].start)/1000) + 's'); 939 } 940 callback(rtn); 941 hamsters.wheel.tasks[taskid] = null; //Clean up our task, not needed any longer 942 if(hamsters.cache && memoize !== false) { 943 if(output.data.length > 0 && !output.dataType) { 944 setTimeout(hamsters.wheel.memoize(task.fn, task.input, output.data, 'na'), 4); 945 } else if(output.data.length > 0 && output.dataType) { 946 setTimeout(hamsters.wheel.memoize(task.fn, task.input, hamsters.wheel.normalizeArray(output.data), output.dataType), 4); 947 } 948 } 949 } 950 } 951 }); 952 return; 953 }; 954 } else { 955 hamsters.wheel.newWheel = function(inputArray, hamsterfood, aggregate, callback, taskid, threadid, hamster, dataBlob, memoize) { 956 var task = hamsters.wheel.tasks[taskid]; 957 if(!task) { 958 hamsters.wheel.errors.push({ 959 'msg': 'Error, unable to match thread to task, throwing exception', 960 'params': hamsterfood, 961 'aggregate': aggregate, 962 'callback': callback 963 }); 964 console.error('Error, unable to match thread to task ' + taskid + ', throwing exception. Check errors for more details'); 965 return; 966 } 967 var queue = hamsters.wheel.queue; 968 if(hamsters.maxThreads && hamsters.maxThreads <= queue.running.length) { 969 hamsters.wheel.poolThread(queue, inputArray, hamsterfood, threadid, callback, taskid, aggregate, memoize); 970 return; 971 } 972 var thread = (threadid || task.count); //Determine threadid depending on currently running threads 973 var debug = hamsters.debug; 974 if(debug || memoize) { 975 hamsters.wheel.trackInput(task, inputArray, thread, taskid, hamsterfood); 976 if(debug === 'verbose') { 977 console.info('Spawning Hamster #' + thread + ' @ ' + new Date().getTime()); 978 } 979 } 980 hamsters.wheel.trackThread(task, queue, thread); 981 var blobObject = (dataBlob || null); 982 var hamsterData; 983 if(!hamster) { 984 hamsterData = hamsters.wheel.createHamster(thread); 985 hamster = hamsterData.worker; 986 blobObject = {'blob': hamsterData.dataBlob, 'uri': hamsterData.blobUri}; 987 } 988 hamsters.wheel.trainHamster(thread, aggregate, callback, taskid, thread, hamster, blobObject, memoize); 989 hamsters.wheel.feedHamster(hamster, hamsterfood, inputArray); 990 task.count++; //Increment count, thread is running 991 }; 992 } 993 }); 994 hamsters.wheel.setup.populateElements(hamsters.maxThreads); 995 }; 996 //Wake 'em up 997 hamsters.wheel.wakeUp();