From e11355193b948a155e943b09d8b726ff8ba2187a Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Thu, 9 Apr 2015 14:01:02 +0200 Subject: [PATCH 1/6] Now using webworker to compute current colors --- package.json | 2 +- .../model/frame/AsyncCachedFrameProcessor.js | 52 +++++++++++++ src/js/model/frame/CachedFrameProcessor.js | 1 + src/js/service/CurrentColorsService.js | 64 ++++++++-------- .../palette/reader/PaletteImageReader.js | 2 +- src/js/utils/Job.js | 29 ++++++++ src/js/worker/framecolors/FrameColors.js | 32 ++++++++ .../worker/framecolors/FrameColorsWorker.js | 66 +++++++++++++++++ src/js/worker/hash/Hash.js | 2 +- src/js/worker/hash/HashWorker.js | 4 +- src/piskel-script-list.js | 5 ++ test/js/utils/JobTest.js | 73 +++++++++++++++++++ 12 files changed, 295 insertions(+), 37 deletions(-) create mode 100644 src/js/model/frame/AsyncCachedFrameProcessor.js create mode 100644 src/js/utils/Job.js create mode 100644 src/js/worker/framecolors/FrameColors.js create mode 100644 src/js/worker/framecolors/FrameColorsWorker.js create mode 100644 test/js/utils/JobTest.js diff --git a/package.json b/package.json index dbed7f37..f522d822 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "karma": "0.12.17", "karma-chrome-launcher": "^0.1.4", "karma-phantomjs-launcher": "^0.1.4", - "karma-jasmine": "^0.2.0", + "karma-jasmine": "^0.3.5", "nodewebkit": "~0.10.1" }, "window": { diff --git a/src/js/model/frame/AsyncCachedFrameProcessor.js b/src/js/model/frame/AsyncCachedFrameProcessor.js new file mode 100644 index 00000000..508c63a0 --- /dev/null +++ b/src/js/model/frame/AsyncCachedFrameProcessor.js @@ -0,0 +1,52 @@ +(function () { + var ns = $.namespace('pskl.model.frame'); + + ns.AsyncCachedFrameProcessor = function (cacheResetInterval) { + ns.CachedFrameProcessor.call(this, cacheResetInterval); + }; + + + pskl.utils.inherit(ns.AsyncCachedFrameProcessor, ns.CachedFrameProcessor); + + /** + * Retrieve the processed frame from the cache, in the (optional) namespace + * If the first level cache is empty, attempt to clone it from 2nd level cache. If second level cache is empty process the frame. + * @param {pskl.model.Frame} frame + * @param {String} namespace + * @return {Object} the processed frame + */ + ns.AsyncCachedFrameProcessor.prototype.get = function (frame, callback, namespace) { + var processedFrame = null; + namespace = namespace || this.defaultNamespace; + + if (!this.cache_[namespace]) { + this.cache_[namespace] = {}; + } + + var cache = this.cache_[namespace]; + + var firstCacheKey = frame.getHash(); + if (cache[firstCacheKey]) { + processedFrame = cache[firstCacheKey]; + } else { + var framePixels = JSON.stringify(frame.getPixels()); + var secondCacheKey = pskl.utils.hashCode(framePixels); + if (cache[secondCacheKey]) { + processedFrame = this.outputCloner(cache[secondCacheKey], frame); + cache[firstCacheKey] = processedFrame; + } else { + this.frameProcessor(frame, this.onFrameProcessorComplete.bind(this, callback, cache, firstCacheKey, secondCacheKey)); + } + } + + if (processedFrame) { + callback(processedFrame); + } + }; + + ns.AsyncCachedFrameProcessor.prototype.onFrameProcessorComplete = function (callback, cache, firstCacheKey, secondCacheKey, processedFrame) { + cache[secondCacheKey] = processedFrame; + cache[firstCacheKey] = processedFrame; + callback(processedFrame); + } +})(); \ No newline at end of file diff --git a/src/js/model/frame/CachedFrameProcessor.js b/src/js/model/frame/CachedFrameProcessor.js index 8a164be6..9b680d76 100644 --- a/src/js/model/frame/CachedFrameProcessor.js +++ b/src/js/model/frame/CachedFrameProcessor.js @@ -17,6 +17,7 @@ this.cacheResetInterval = cacheResetInterval || DEFAULT_CLEAR_INTERVAL; this.frameProcessor = DEFAULT_FRAME_PROCESSOR; this.outputCloner = DEFAULT_OUTPUT_CLONER; + this.defaultNamespace = DEFAULT_NAMESPACE; window.setInterval(this.clear.bind(this), this.cacheResetInterval); }; diff --git a/src/js/service/CurrentColorsService.js b/src/js/service/CurrentColorsService.js index fa953224..2ffa299a 100644 --- a/src/js/service/CurrentColorsService.js +++ b/src/js/service/CurrentColorsService.js @@ -7,7 +7,7 @@ this.cache = {}; this.currentColors = []; - this.cachedFrameProcessor = new pskl.model.frame.CachedFrameProcessor(); + this.cachedFrameProcessor = new pskl.model.frame.AsyncCachedFrameProcessor(); this.cachedFrameProcessor.setFrameProcessor(this.getFrameColors_.bind(this)); this.colorSorter = new pskl.service.color.ColorSorter(); @@ -44,20 +44,37 @@ var colors = this.cache[historyIndex]; if (colors) { this.setCurrentColors(colors); + } else { + this.updateCurrentColors_(); } }; ns.CurrentColorsService.prototype.updateCurrentColors_ = function () { var layers = this.piskelController.getLayers(); var frames = layers.map(function (l) {return l.getFrames();}).reduce(function (p, n) {return p.concat(n);}); - var colors = {}; - frames.forEach(function (f) { - var frameColors = this.cachedFrameProcessor.get(f); - Object.keys(frameColors).slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED).forEach(function (color) { - colors[color] = true; - }); - }.bind(this)); + this.currentJob = new pskl.utils.Job({ + items : frames, + args : { + colors : {} + }, + process : function (frame, callback) { + return this.cachedFrameProcessor.get(frame, callback); + }.bind(this), + onProcessEnd : function (frameColors) { + var colors = this.args.colors; + Object.keys(frameColors).slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED).forEach(function (color) { + colors[color] = true; + }); + }, + onComplete : this.updateCurrentColorsReady_.bind(this) + }); + + this.currentJob.start(); + }; + + ns.CurrentColorsService.prototype.updateCurrentColorsReady_ = function (args) { + var colors = args.colors; // Remove transparent color from used colors delete colors[Constants.TRANSPARENT_COLOR]; @@ -69,30 +86,13 @@ this.setCurrentColors(currentColors); }; - ns.CurrentColorsService.prototype.getFrameColors_ = function (frame) { - var frameColors = {}; - frame.forEachPixel(function (color, x, y) { - var hexColor = this.toHexString_(color); - frameColors[hexColor] = true; - }.bind(this)); - return frameColors; - }; + ns.CurrentColorsService.prototype.getFrameColors_ = function (frame, processorCallback) { + var frameColorsWorker = new pskl.worker.framecolors.FrameColors(frame, + function (event) {processorCallback(event.data.colors);}, + function () {}, + function (event) {processorCallback({});} + ); - ns.CurrentColorsService.prototype.toHexString_ = function (color) { - if (color === Constants.TRANSPARENT_COLOR) { - return color; - } else { - color = color.replace(/\s/g, ''); - var hexRe = (/^#([a-f0-9]{3}){1,2}$/i); - var rgbRe = (/^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/i); - if (hexRe.test(color)) { - return color.toUpperCase(); - } else if (rgbRe.test(color)) { - var exec = rgbRe.exec(color); - return pskl.utils.rgbToHex(exec[1] * 1, exec[2] * 1, exec[3] * 1); - } else { - console.error('Could not convert color to hex : ', color); - } - } + frameColorsWorker.process(); }; })(); \ No newline at end of file diff --git a/src/js/service/palette/reader/PaletteImageReader.js b/src/js/service/palette/reader/PaletteImageReader.js index f5953bc9..dcd8c5ca 100644 --- a/src/js/service/palette/reader/PaletteImageReader.js +++ b/src/js/service/palette/reader/PaletteImageReader.js @@ -14,7 +14,7 @@ }; ns.PaletteImageReader.prototype.onImageLoaded_ = function (image) { - var imageProcessor = new pskl.worker.ImageProcessor(image, + var imageProcessor = new pskl.worker.imageprocessor.ImageProcessor(image, this.onWorkerSuccess_.bind(this), this.onWorkerStep_.bind(this), this.onWorkerError_.bind(this)); diff --git a/src/js/utils/Job.js b/src/js/utils/Job.js new file mode 100644 index 00000000..de9043e1 --- /dev/null +++ b/src/js/utils/Job.js @@ -0,0 +1,29 @@ +(function () { + var ns = $.namespace('pskl.utils'); + + ns.Job = function (cfg) { + this.args = cfg.args; + this.items = cfg.items; + + this.process = cfg.process; + this.onProcessEnd = cfg.onProcessEnd; + this.onComplete = cfg.onComplete; + + this.completed_ = 0; + }; + + ns.Job.prototype.start = function () { + this.items.forEach(function (item, index) { + this.process(item, this.processCallback.bind(this, index)); + }.bind(this)) + }; + + ns.Job.prototype.processCallback = function (index, args) { + this.completed_++; + this.onProcessEnd(args, index); + + if (this.completed_ === this.items.length) { + this.onComplete(this.args); + } + } +})(); \ No newline at end of file diff --git a/src/js/worker/framecolors/FrameColors.js b/src/js/worker/framecolors/FrameColors.js new file mode 100644 index 00000000..1f318850 --- /dev/null +++ b/src/js/worker/framecolors/FrameColors.js @@ -0,0 +1,32 @@ +(function () { + var ns = $.namespace('pskl.worker.framecolors'); + + ns.FrameColors = function (frame, onSuccess, onStep, onError) { + this.serializedFrame = JSON.stringify(frame.pixels); + + this.onStep = onStep; + this.onSuccess = onSuccess; + this.onError = onError; + + this.worker = pskl.utils.WorkerUtils.createWorker(ns.FrameColorsWorker, 'frame-colors'); + this.worker.onmessage = this.onWorkerMessage.bind(this); + }; + + ns.FrameColors.prototype.process = function () { + this.worker.postMessage({ + serializedFrame : this.serializedFrame + }); + }; + + ns.FrameColors.prototype.onWorkerMessage = function (event) { + if (event.data.type === 'STEP') { + this.onStep(event); + } else if (event.data.type === 'SUCCESS') { + this.onSuccess(event); + this.worker.terminate(); + } else if (event.data.type === 'ERROR') { + this.onError(event); + this.worker.terminate(); + } + }; +})(); \ No newline at end of file diff --git a/src/js/worker/framecolors/FrameColorsWorker.js b/src/js/worker/framecolors/FrameColorsWorker.js new file mode 100644 index 00000000..1a55fa2d --- /dev/null +++ b/src/js/worker/framecolors/FrameColorsWorker.js @@ -0,0 +1,66 @@ +(function () { + var ns = $.namespace('pskl.worker.framecolors'); + + if (Constants.TRANSPARENT_COLOR !== 'rgba(0, 0, 0, 0)') { + throw 'Constants.TRANSPARENT_COLOR, please update FrameColorsWorker'; + } + + ns.FrameColorsWorker = function () { + + var TRANSPARENT_COLOR = 'rgba(0, 0, 0, 0)'; + + var toHexString_ = function(color) { + if (color === TRANSPARENT_COLOR) { + return color; + } else { + color = color.replace(/\s/g, ''); + var hexRe = (/^#([a-f0-9]{3}){1,2}$/i); + var rgbRe = (/^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/i); + if (hexRe.test(color)) { + return color.toUpperCase(); + } else if (rgbRe.test(color)) { + var exec = rgbRe.exec(color); + return rgbToHex(exec[1] * 1, exec[2] * 1, exec[3] * 1); + } + } + }; + + var rgbToHex = function (r, g, b) { + return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); + }; + + var componentToHex = function (c) { + var hex = c.toString(16); + return hex.length == 1 ? "0" + hex : hex; + }; + + var getFrameColors = function (frame) { + var frameColors = {}; + for (var x = 0 ; x < frame.length ; x ++) { + for (var y = 0 ; y < frame[x].length ; y++) { + var color = frame[x][y]; + var hexColor = toHexString_(color); + frameColors[hexColor] = true; + } + } + return frameColors; + }; + + this.onmessage = function(event) { + try { + var data = event.data; + var frame = JSON.parse(data.serializedFrame); + var colors = getFrameColors(frame); + this.postMessage({ + type : 'SUCCESS', + colors : colors + }); + } catch (e) { + this.postMessage({ + type : 'ERROR', + message : e.message + }); + } + }; + }; +})(); \ No newline at end of file diff --git a/src/js/worker/hash/Hash.js b/src/js/worker/hash/Hash.js index ce3e7c4a..494eedd3 100644 --- a/src/js/worker/hash/Hash.js +++ b/src/js/worker/hash/Hash.js @@ -8,7 +8,7 @@ this.onSuccess = onSuccess; this.onError = onError; - this.worker = pskl.utils.WorkerUtils.createWorker(ns.HashWorker, 'hash-builder'); + this.worker = pskl.utils.WorkerUtils.createWorker(ns.HashWorker, 'hash'); this.worker.onmessage = this.onWorkerMessage.bind(this); }; diff --git a/src/js/worker/hash/HashWorker.js b/src/js/worker/hash/HashWorker.js index 90872b93..35830fa4 100644 --- a/src/js/worker/hash/HashWorker.js +++ b/src/js/worker/hash/HashWorker.js @@ -1,7 +1,7 @@ (function () { - var ns = $.namespace('pskl.worker'); + var ns = $.namespace('pskl.worker.hash'); - ns.HashBuilder = function () { + ns.HashWorker = function () { var hashCode = function(str) { var hash = 0; if (str.length !== 0) { diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index 02a73b0f..368f80fd 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -24,6 +24,7 @@ "js/utils/FileUtilsDesktop.js", "js/utils/FrameTransform.js", "js/utils/FrameUtils.js", + "js/utils/Job.js", "js/utils/LayerUtils.js", "js/utils/ImageResizer.js", "js/utils/PixelUtils.js", @@ -58,6 +59,7 @@ "js/model/Layer.js", "js/model/piskel/Descriptor.js", "js/model/frame/CachedFrameProcessor.js", + "js/model/frame/AsyncCachedFrameProcessor.js", "js/model/Palette.js", "js/model/Piskel.js", @@ -188,6 +190,8 @@ "js/devtools/init.js", // Workers + "js/worker/framecolors/FrameColorsWorker.js", + "js/worker/framecolors/FrameColors.js", "js/worker/hash/HashWorker.js", "js/worker/hash/Hash.js", "js/worker/imageprocessor/ImageProcessorWorker.js", @@ -195,6 +199,7 @@ // Application controller and initialization "js/app.js", + // Bonus features !! "js/snippets.js" ]; \ No newline at end of file diff --git a/test/js/utils/JobTest.js b/test/js/utils/JobTest.js new file mode 100644 index 00000000..41f57eac --- /dev/null +++ b/test/js/utils/JobTest.js @@ -0,0 +1,73 @@ +describe("Job for // async", function() { + + beforeEach(function() {}); + afterEach(function() {}); + + it("completes synchronous job", function() { + // when + var isComplete = false; + var result = null; + // then + var job = new pskl.utils.Job({ + items : [0,1,2,3,4], + args : { + store : [] + }, + process : function (item, callback) { + callback(item+5) + }, + onProcessEnd : function (value, index) { + this.args.store[index] = value; + }, + onComplete : function (args) { + isComplete = true; + result = args.store; + } + }); + + job.start(); + + // verify + expect(isComplete).toBe(true); + expect(result).toEqual([5,6,7,8,9]); + }); + + describe("async", function () { + // when + var isComplete = false; + var result = null; + + beforeEach(function(done) { + // then + var job = new pskl.utils.Job({ + items : [0,1,2,3,4], + args : { + store : [] + }, + process : function (item, callback) { + setTimeout(function (item, callback) { + callback(item+5); + }.bind(this, item, callback), 100 - (item * 20)); + }, + onProcessEnd : function (value, index) { + console.log('Processed ', index); + this.args.store[index] = value; + }, + onComplete : function (args) { + isComplete = true; + result = args.store; + done(); + } + }); + + job.start(); + }); + it("completes asynchronous job", function() { + // verify + expect(isComplete).toBe(true); + expect(result).toEqual([5,6,7,8,9]); + }); + + }) + +}); \ No newline at end of file From 522006f67a3f80c119ab4e5c0bacf836b2cd6e80 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Thu, 9 Apr 2015 16:37:29 +0200 Subject: [PATCH 2/6] wip : replace Job by promises --- src/js/lib/q.js | 1986 +++++++++++++++++ .../model/frame/AsyncCachedFrameProcessor.js | 13 +- src/js/service/CurrentColorsService.js | 33 +- src/piskel-script-list.js | 3 + 4 files changed, 2011 insertions(+), 24 deletions(-) create mode 100644 src/js/lib/q.js diff --git a/src/js/lib/q.js b/src/js/lib/q.js new file mode 100644 index 00000000..710fdf4b --- /dev/null +++ b/src/js/lib/q.js @@ -0,0 +1,1986 @@ +// vim:ts=4:sts=4:sw=4: +/*! + * + * Copyright 2009-2012 Kris Kowal under the terms of the MIT + * license found at http://github.com/kriskowal/q/raw/master/LICENSE + * + * With parts by Tyler Close + * Copyright 2007-2009 Tyler Close under the terms of the MIT X license found + * at http://www.opensource.org/licenses/mit-license.html + * Forked at ref_send.js version: 2009-05-11 + * + * With parts by Mark Miller + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +(function (definition) { + "use strict"; + + // This file will function properly as a