Skip to content

Commit

Permalink
Use a worker pool library. Closes #7
Browse files Browse the repository at this point in the history
  • Loading branch information
rosslh committed Feb 4, 2024
1 parent 6733499 commit 0c294b0
Show file tree
Hide file tree
Showing 29 changed files with 242 additions and 263 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

- [Mandelbrot set code - <code>mandelbrot/src/lib.rs</code>](mandelbrot/src/lib.rs)
- [Rust tests - <code>mandelbrot/src/lib_test.rs</code>](mandelbrot/src/lib_test.rs)
- [Web Worker - <code>client/app/worker.js</code>](client/app/worker.js)
- [Web Worker - <code>client/app/worker.ts</code>](client/app/worker.ts)
- [Leaflet tile generation - <code>client/app/main.ts</code>](client/app/main.ts)

## Features
Expand Down
Empty file modified clean.sh
100644 → 100755
Empty file.
58 changes: 35 additions & 23 deletions client/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,51 @@ module.exports = {
env: {
browser: true,
es6: true,
node: true
node: true,
},
extends: [
'plugin:editorconfig/all',
'eslint:recommended',
"plugin:editorconfig/all",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
"plugin:@typescript-eslint/recommended",
],
overrides: [{
files: ["*.js"],
rules: {
"@typescript-eslint/no-var-requires": "off"
}
}],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'editorconfig'],
overrides: [
{
files: ["*.js"],
rules: {
"@typescript-eslint/no-var-requires": "off",
},
},
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "editorconfig"],
root: true,
rules: {
"brace-style": [1, "1tbs"],
eqeqeq: [2, "always", {
"null": "ignore"
}],
"no-console": [1, {
allow: ["warn", "error"]
}],
eqeqeq: [
2,
"always",
{
null: "ignore",
},
],
"no-console": [
1,
{
allow: ["warn", "error"],
},
],
"no-implicit-coercion": 2,
"no-param-reassign": 1,
"no-var": 2,
"prefer-const": 1,
semi: 1,
"@typescript-eslint/no-unused-vars": [2, {
argsIgnorePattern: "^_"
}],
"no-unused-vars": "off"
}
"@typescript-eslint/no-unused-vars": [
2,
{
argsIgnorePattern: "^_",
},
],
"no-unused-vars": "off",
},
};
136 changes: 46 additions & 90 deletions client/app/main.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import "./static";
import { stringify } from "./utils";
import * as debounce from "debounce";
import * as L from "leaflet";
import { saveAs } from "file-saver";
import domToImage from "dom-to-image";

interface WorkerContainer {
worker: Worker;
activeJobs: number;
ready: boolean;
}
import { EsThreadPool, EsThread } from "threads-es/controller";

interface MandelbrotConfig {
[key: string]: number | string | boolean;
Expand All @@ -34,13 +28,6 @@ interface CheckboxInput {
map: MandelbrotMap;
}

interface MessageFromWorker {
data: {
image: Uint8ClampedArray;
coords: string;
};
}

interface Done {
(error: null, tile: HTMLCanvasElement): void;
}
Expand All @@ -51,54 +38,9 @@ const config: MandelbrotConfig = {
colorScheme: "turbo",
reverseColors: false,
};

class WorkerManager {
workers: Array<WorkerContainer> = [];

constructor() {
this.resetWorkers();
}

createWorker() {
const w: WorkerContainer = {
worker: new Worker("./worker.js"),
activeJobs: 0,
ready: false,
};
const workerReadyHandler = (e: MessageEvent) => {
if (e.data.ready) {
w.ready = true;
w.worker.removeEventListener("message", workerReadyHandler);
}
};
w.worker.addEventListener("message", workerReadyHandler);
return w;
}

async resetWorkers() {
for (const { worker } of this.workers) {
worker.terminate();
}
const numWorkers = Math.min(navigator.hardwareConcurrency || 4, 64);
this.workers = [...new Array(numWorkers)].map(() => this.createWorker());
while (!this.workers.every((w) => w.ready)) {
await new Promise((resolve) => setTimeout(resolve, 300));
}
}

getLeastActiveWorker() {
return this.workers
.filter((w) => w.ready)
.reduce(
(leastActive, worker) =>
worker.activeJobs < leastActive.activeJobs ? worker : leastActive,
this.workers[0]
);
}
}

class MandelbrotLayer extends L.GridLayer {
tileSize: number;
_map: MandelbrotMap;

constructor() {
super({
Expand Down Expand Up @@ -157,41 +99,31 @@ class MandelbrotLayer extends L.GridLayer {
canvas.height = this.getTileSize().y;

const mappedCoords = this.getMappedCoords(coords);
const coordsString = stringify(mappedCoords);

Object.entries({ ...coords, ...mappedCoords }).forEach(([key, value]) => {
canvas.dataset[key] = String(value);
});

const selectedWorker = workerManager.getLeastActiveWorker();

selectedWorker.activeJobs += 1;
const tileRetrievedHandler = ({ data }: MessageFromWorker) => {
if (data.coords === coordsString) {
selectedWorker.worker.removeEventListener(
"message",
tileRetrievedHandler
);
selectedWorker.activeJobs = Math.max(selectedWorker.activeJobs - 1, 0);
this._map.pool
?.queue((thread) =>
thread.methods.getTile({
coords: mappedCoords,
maxIterations: config.iterations,
exponent: config.exponent,
tileSize: this.getTileSize().x,
colorScheme: config.colorScheme,
reverseColors: config.reverseColors,
})
)
.then((result) => {
const imageData = new ImageData(
Uint8ClampedArray.from(data.image),
Uint8ClampedArray.from(result),
this.getTileSize().x,
this.getTileSize().y
);
context.putImageData(imageData, 0, 0);
done(null, canvas);
}
};

selectedWorker.worker.addEventListener("message", tileRetrievedHandler);
selectedWorker.worker.postMessage({
coords: mappedCoords,
maxIterations: config.iterations,
exponent: config.exponent,
tileSize: this.getTileSize().x,
colorScheme: config.colorScheme,
reverseColors: config.reverseColors,
});
});

return canvas;
}
Expand All @@ -211,14 +143,20 @@ class MandelbrotMap extends L.Map {
mapId: string;
defaultPosition: [number, number];
defaultZoom: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pool: EsThreadPool<any>;

constructor(mapId: string) {
constructor({ htmlId: mapId }: { htmlId: string }) {
super(mapId, {
attributionControl: false,
maxZoom: 32,
zoomAnimationThreshold: 32,
});

this.createPool().then(() => {
this.refresh(false);
this.mandelbrotLayer.refresh();
});
this.mapId = mapId;
this.mandelbrotLayer = new MandelbrotLayer().addTo(this);
this.defaultPosition = [0, 0];
Expand All @@ -227,8 +165,27 @@ class MandelbrotMap extends L.Map {
this.mandelbrotLayer.refresh();
}

async createPool() {
if (this.pool) {
await this.pool.terminate();
}

this.pool = await EsThreadPool.Spawn(
() =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
EsThread.Spawn<any>(
new Worker(new URL("./worker.ts", import.meta.url), {
type: "module",
})
),
{
size: navigator.hardwareConcurrency || 4,
}
);
}

async refresh(resetView = false) {
await workerManager.resetWorkers();
await this.createPool();
if (resetView) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any)._resetView(this.defaultPosition, this.defaultZoom);
Expand Down Expand Up @@ -351,8 +308,7 @@ function handleInputs(map: MandelbrotMap) {
});
}

const workerManager = new WorkerManager();
workerManager.resetWorkers().then(() => {
const map = new MandelbrotMap("leaflet-map");
handleInputs(map);
const map = new MandelbrotMap({
htmlId: "leaflet-map",
});
handleInputs(map);
File renamed without changes.
11 changes: 0 additions & 11 deletions client/app/utils.js

This file was deleted.

34 changes: 0 additions & 34 deletions client/app/worker.js

This file was deleted.

42 changes: 42 additions & 0 deletions client/app/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { exposeApi } from "threads-es/worker";
import("../../mandelbrot/pkg/index").then((wasm) => {
wasm.init();

const workerApi = {
getTile: ({
coords,
maxIterations,
exponent,
tileSize,
colorScheme,
reverseColors,
}: {
coords: {
re_min: number;
re_max: number;
im_min: number;
im_max: number;
};
maxIterations: number;
exponent: number;
tileSize: number;
colorScheme: string;
reverseColors: boolean;
}) => {
const data = wasm.get_tile(
coords.re_min,
coords.re_max,
coords.im_min,
coords.im_max,
maxIterations,
exponent,
tileSize,
colorScheme,
reverseColors
);
return data;
},
};

exposeApi(workerApi);
});
Loading

0 comments on commit 0c294b0

Please sign in to comment.