Skip to content

Instantly share code, notes, and snippets.

@IanSSenne
Last active January 5, 2020 11:51
Show Gist options
  • Save IanSSenne/19de69449e10f9b63ed678f4e0a33f11 to your computer and use it in GitHub Desktop.
Save IanSSenne/19de69449e10f9b63ed678f4e0a33f11 to your computer and use it in GitHub Desktop.
workers are fun
<!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>
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