Last active
January 5, 2020 11:51
-
-
Save IanSSenne/19de69449e10f9b63ed678f4e0a33f11 to your computer and use it in GitHub Desktop.
workers are fun
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<title>Document</title> | |
</head> | |
<body style="background-color: gray;"> | |
<label>image file:</label> <br /> | |
<input type="file" id="imageLoader" name="imageLoader" /><br /> | |
<label>chunk size:</label> <br /> | |
<input type="number" min="8" max="1024" value="256" id="chunkSize" name="chunkSize" /><br /> | |
<canvas id="imageCanvas"></canvas> | |
<script type="module"> | |
import * as lib from "./lib.js"; window.Workerify = lib.default; | |
var imageLoader = document.getElementById('imageLoader'); | |
imageLoader.addEventListener('change', handleImage, false); | |
var canvas = document.getElementById('imageCanvas'); | |
var ctx = canvas.getContext('2d'); | |
const [modifyImage, terminate, WorkerifyWorkerHost] = Workerify(function (data) { | |
for (let i = 0; i < data.length; i += 4) { | |
const r = data[i + 0]; | |
const g = data[i + 1]; | |
const b = data[i + 2]; | |
const a = data[i + 3]; | |
data[i + 0] = 255 - r; | |
data[i + 1] = 255 - g; | |
data[i + 2] = 255 - b; | |
data[i + 3] = a; | |
} | |
return data; | |
}, { maxWorkers: 5, timeout: 60000, idleTime: 100 }); | |
window.modifyImage = modifyImage; | |
window.terminate = terminate; | |
window.WorkerifyWorkerHost = WorkerifyWorkerHost; | |
async function handleImage(e) { | |
const chunkSize = +document.getElementById("chunkSize").value; | |
var reader = new FileReader(); | |
reader.onload = async function (event) { | |
var img = new Image(); | |
img.onload = async function () { | |
canvas.width = img.width; | |
canvas.height = img.height; | |
ctx.drawImage(img, 0, 0); | |
for (let y = 0; y < canvas.height; y += chunkSize) { | |
for (let x = 0; x < canvas.width; x += chunkSize) { | |
let data = Array.from(ctx.getImageData(x, y, chunkSize, chunkSize).data); | |
modifyImage(data).then( | |
raw => { | |
console.log("put data in chunk ", x / chunkSize, y / chunkSize); | |
ctx.putImageData(new ImageData(new Uint8ClampedArray(Array.from(raw)), chunkSize, chunkSize), x, y); | |
} | |
); | |
} | |
} | |
} | |
img.src = event.target.result; | |
} | |
reader.readAsDataURL(e.target.files[0]); | |
} | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const WorkerifyDefaultOptions = { | |
maxWorkers: 1, | |
timeout: 60000, | |
idleTime: 60000 * 5 | |
}; | |
function textToUrl(arr) { | |
const str = arr.filter((v) => typeof v === "string").map((_, i) => { | |
return `// Start Workerify SubModule ${i}\n${_}\n// End Workerify SubModule ${i}`; | |
}).join("\n"); | |
const blob = new Blob([str]); | |
const url = URL.createObjectURL(blob); | |
return url; | |
} | |
class WorkerifyWorker { | |
constructor(url, onTaskComplete, id, preserve, options) { | |
this.id = id; | |
this.url = url; | |
this.onTaskComplete = onTaskComplete; | |
this.Worker = null; | |
this.isWorking = false; | |
this.lastCallId = null; | |
this.asyncCallback = null; | |
this.preserved = preserve; | |
this.options = options | |
this.timeout = options.timeout; | |
this.idleTime = options.idleTime; | |
if (this.preserved) this.populateWorker(); | |
} | |
destroy() { | |
if (this.Worker) { | |
this.Worker.terminate(); | |
this.Worker = null; | |
} | |
} | |
populateWorker() { | |
if (this.Worker === null) { | |
this.Worker = new Worker(this.url); | |
this.Worker.onmessage = ({ data }) => { | |
if (!this.asyncCallback) debugger; | |
this.asyncCallback(JSON.parse(data).value, null); | |
} | |
this.Worker.onerror = (err) => { | |
if (this.asyncCallback) { | |
this.asyncCallback(null, err); | |
} | |
} | |
} | |
} | |
call(args) { | |
this.isWorking = true; | |
if (!this.Worker) { | |
this.populateWorker(); | |
} | |
return new Promise((resolve, reject) => { | |
if (this.lastCallId != null) { | |
clearTimeout(this.lastCallId); | |
} | |
if (!this.preserved) this.lastCallId = setTimeout(() => { | |
this.lastCallId = null; | |
this.destroy(); | |
}, this.timeout); | |
if (this.idleTimeId != null) clearTimeout(this.idleTimeId); | |
this.asyncCallback = (value, err) => { | |
this.asyncCallback = null; | |
if (err) { | |
reject(err); | |
} else { | |
resolve(value); | |
} | |
this.isWorking = false; | |
if (!this.preserved) this.idleTimeId = setTimeout(() => { | |
this.idleTimeId = null; | |
this.destroy(); | |
}, this.idleTime); | |
this.onTaskComplete(this); | |
}; | |
this.Worker.postMessage(JSON.stringify(args)); | |
}); | |
} | |
} | |
class WorkifyManager { | |
constructor(func, options) { | |
this.func = func; | |
this.options = options; | |
this.url = textToUrl([`const func = ${this.func}`, `globalThis.onmessage=async ({data})=>{ | |
try{ | |
let res = func(...JSON.parse(data)); | |
if(res instanceof Promise)res = await res; | |
postMessage(JSON.stringify({value:res,err:null})); | |
}catch(e){ | |
postMessage(JSON.stringify({value:null,err:e})); | |
} | |
}`]); | |
this.Workers = []; | |
this.TaskQueue = []; | |
this.inactive = false; | |
for (let i = 0; i < this.options.maxWorkers; i++) { | |
this.Workers.push(new WorkerifyWorker(this.url, this.onTaskComplete.bind(this), i, i == 0, options)); | |
} | |
Object.freeze(this.Workers); | |
} | |
onTaskComplete(worker) { | |
if (this.TaskQueue[0]) { | |
const Task = this.TaskQueue.shift(); | |
worker.call(Task.args).then(Task.resolve).catch(Task.reject); | |
} | |
} | |
call(args) { | |
if (this.inactive) { | |
throw new Error("unable to call Workerify function after destruction has occured"); | |
} | |
for (let i = 0; i < this.Workers.length; i++) { | |
if (!this.Workers[i].isWorking) { | |
return this.Workers[i].call(args) | |
} | |
} | |
let taskSuccess = null, taskFail = null; | |
const TaskPromise = new Promise((resolve, reject) => { | |
taskSuccess = (val) => resolve(val); | |
taskFail = (val) => reject(val); | |
}) | |
this.TaskQueue.push({ args, resolve: taskSuccess, reject: taskFail }); | |
return TaskPromise; | |
} | |
destroy() { | |
this.inactive = true; | |
for (let i = 0; i < this.Workers.length; i++) { | |
this.Workers[i].destroy(); | |
} | |
this.Workers = []; | |
} | |
} | |
export default function Workerify(func, options) { | |
const ComputedOptions = Object.assign(WorkerifyDefaultOptions, options); | |
const Manager = new WorkifyManager(func, ComputedOptions); | |
const call = (...args) => Manager.call(args); | |
return [call, Manager.destroy.bind(Manager), Manager]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment