code related to parallelization
code related to visualization
| import type { Neko as DesktopWindow } from "https://deno.land/x/neko/core/Neko.ts"; | |
| import type * as skia from "https://deno.land/x/skia_canvas/mod.ts"; | |
| export default class Canvas { | |
| #canvas?: skia.Canvas | HTMLCanvasElement; | |
| #window?: DesktopWindow; | |
| #img?: typeof skia.Image; | |
| #ctx?: Context2D; | |
| static async new( | |
| { title, width, height } = {} as { | |
| title?: string; | |
| width: number; | |
| height: number; | |
| }, | |
| ) { | |
| if (!("window" in globalThis)) throw "Canvas must be in Main thread"; | |
| const self = new Canvas(Canvas._guard); | |
| if ("Deno" in globalThis) { | |
| const [{ Neko }, { createCanvas }, { Image }] = await Promise.all( | |
| [ | |
| "neko/core/Neko.ts", | |
| "skia_canvas/src/canvas.ts", | |
| "skia_canvas/src/image.ts", | |
| ].map((mod) => import(`https://deno.land/x/${mod}`)), | |
| ); | |
| self.#img = Image; | |
| self.#canvas = createCanvas(width, height); | |
| self.#window = new Neko({ title, width, height, resize: true }); | |
| } else { | |
| document.body.append( | |
| self.#canvas = (document.querySelector("canvas:not(#memviz)") ?? | |
| document.createElement("canvas")) as HTMLCanvasElement, | |
| ); | |
| if (!self.#canvas.hasAttribute("width")) self.#canvas.width = width; | |
| if (!self.#canvas.hasAttribute("height")) self.#canvas.height = height; | |
| } | |
| return self; | |
| } | |
| async loadImage(path: string | URL) { | |
| if ("Deno" in globalThis) return this.#img!.load(path); | |
| return createImageBitmap(await fetch(path).then((as) => as.blob())); | |
| } | |
| #cursor = { x: NaN, y: NaN }; | |
| #onmousemove?: (e: MouseEvent) => void; | |
| get mouse() { | |
| const p = this.#cursor; | |
| if ("Deno" in globalThis) [p.x, p.y] = this.#window!.mousePosition; | |
| else if (!this.#onmousemove) { | |
| addEventListener( | |
| "mousemove", | |
| this.#onmousemove = (e) => (p.x = e.x, p.y = e.y), | |
| ); | |
| } | |
| return p; | |
| } | |
| reset() { | |
| this.#ctx?.clearRect(0, 0, this.#canvas!.width, this.#canvas!.height); | |
| this.#ctx?.resetTransform(); | |
| } | |
| draw(fn: (ctx: Context2D) => void) { | |
| this.reset(); | |
| fn(this.#ctx ??= this.getContext("2d")); | |
| if ("Deno" in globalThis) { | |
| this.#window!.setFrameBuffer( | |
| (this.#canvas as skia.Canvas).pixels, | |
| this.#canvas!.width, | |
| this.#canvas!.height, | |
| ); | |
| } | |
| } | |
| getContext(type: "2d") { | |
| return this.#ctx ??= this.#canvas!.getContext(type, { | |
| desynchronized: true, | |
| })!; | |
| } | |
| private static readonly _guard = Symbol(); // to bad typesccrypt can't do typeof MemViz.#guard | |
| constructor(guard: typeof Canvas._guard) { | |
| if (guard !== Canvas._guard) throw null; | |
| } | |
| } | |
| type Context2D = CanvasRenderingContext2D | skia.CanvasRenderingContext2D; |
| { | |
| "compilerOptions": { | |
| "lib": [ | |
| "dom", | |
| "dom.iterable", | |
| "dom.asynciterable", | |
| "deno.ns", | |
| "deno.unstable" | |
| ] | |
| } | |
| } |
| <canvas/> | |
| <canvas id=memviz/> |
| #!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable --allow-ffi | |
| // Copyright 2023 Fahmi Akbar Wildana <[email protected]> | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| import MemViz from "./memviz.ts"; | |
| import Canvas from "./canvas.ts"; | |
| import { footer, header } from "./string.ts"; | |
| import * as random from "./random.ts"; | |
| import { cpus } from "https://deno.land/std/node/os.ts"; | |
| import { loop, malloc, WorkerThread } from "./utils.ts"; | |
| /////////////////////////////////////////////////////////////////////////////👆 0 | |
| //👇 tweakable | |
| const capacity = { min: 100, max: 200 }, | |
| instance = 10, | |
| worker = cpus().length - 1; | |
| class Position { | |
| static TypedArray = Uint16Array; | |
| static each = 2 * Position.TypedArray.BYTES_PER_ELEMENT; //👈 | |
| x: InstanceType<typeof Position.TypedArray>; | |
| y: InstanceType<typeof Position.TypedArray>; | |
| constructor(public cap: number) { | |
| this.x = make(Position.TypedArray, this.cap); | |
| this.y = make(Position.TypedArray, this.cap); | |
| } | |
| color = random.color(); | |
| avatar?: Awaited<ReturnType<Canvas["loadImage"]>> | true; | |
| } | |
| let make: ReturnType<typeof malloc>; | |
| let buffer: ArrayBufferLike; | |
| /////////////////////////////////////////////////////////////////////////////👆 1 | |
| const width = 800, height = 600, size = { particle: 2, avatar: 32 }; //👈 tweakable | |
| const radius = 100; | |
| const printMemoryLayout = true; | |
| let zoo: Position[], | |
| capacities: number[] | Uint8Array = [], | |
| border: number, | |
| inc = 0, | |
| mouse = { x: width / 2, y: height / 2 }, | |
| isFollowCursor: true | undefined, | |
| actor: WorkerThread | undefined; | |
| const script = import.meta.url; | |
| let actorID: number | undefined; | |
| if ("window" in globalThis) { | |
| const byteLength = Position.each * capacity.max * instance; //👈 console.group(...header("Main thread")); | |
| console.time("full setup"); | |
| if (worker) { | |
| actor = new WorkerThread(script, new SharedArrayBuffer(byteLength)); | |
| } | |
| make = malloc(buffer = worker ? actor!.buffer : new ArrayBuffer(byteLength)); | |
| setup(); // create zoo | |
| if (worker) { // tell worker about the memory layout of zoo | |
| actor!.run(new Uint8Array(capacities = zoo!.map((it) => it.cap)).buffer); | |
| } | |
| console.timeEnd("full setup"); | |
| // setTimeout(() => actor!.relayMessage(["no shake"]), 1e2); // debug: freeze particle | |
| console.time("prepare to draw"); | |
| const memviz = await MemViz.new(buffer); | |
| const canvas = await Canvas.new({ width, height }); | |
| console.timeEnd("prepare to draw"); | |
| console.time("fetch avatar"); | |
| Promise.all( | |
| zoo!.map((it, i) => | |
| canvas.loadImage(random.avatar.elonmusk()).then((img) => { | |
| it.avatar = img; | |
| incRect += 2; | |
| actor.relayMessage(["avatar ready", i]); | |
| }) | |
| ), | |
| ).then(() => actor.relayMessage(["follow cursor"])); | |
| console.timeEnd("fetch avatar"); | |
| console.groupEnd(); | |
| console.info(...footer("Main")); | |
| loop(() => { | |
| mouse = canvas.mouse; | |
| if (worker) actor!.relayMessage(["mousemove", mouse]); | |
| canvas.draw((ctx) => { | |
| const m = mouse; | |
| for (const { cap, x, y, color, avatar } of zoo) { | |
| ctx.fillStyle = color; | |
| for (let i = cap; i--;) { | |
| border = avatar ? size.avatar : size.particle + inc; | |
| const p = { x: Atomics.load(x, i), y: Atomics.load(y, i) }; | |
| ctx.translate(p.x, p.y); | |
| if (avatar && !is(p).inside(radius, m)) { | |
| ctx.rotate( | |
| Math.atan2(m.y - (p.y + border / 2), m.x - (p.x + border / 2)), | |
| ); | |
| ctx.drawImage(avatar, 0, 0, border, border); | |
| } else { | |
| ctx.fillRect(0, 0, border, border); | |
| } | |
| ctx.resetTransform(); | |
| } | |
| } | |
| }); | |
| memviz.draw(); | |
| }); | |
| } else { | |
| console.group(...header("Worker thread")); | |
| console.time("full setup"); | |
| const shared = await WorkerThread.selfSpawn(script, worker - 1); | |
| actorID = shared.id; | |
| make = malloc(buffer = shared.memory); | |
| capacities = new Uint8Array(shared.layout); | |
| setup(); // create zoo | |
| console.timeEnd("full setup"); | |
| console.groupEnd(); | |
| console.info(...footer(`Worker[${actorID}]`)); | |
| shared.onrelay = (data) => { | |
| if (!Array.isArray(data)) return; | |
| switch (data[0]) { | |
| case "mousemove": | |
| mouse = data[1]; | |
| break; | |
| case "no shake": | |
| isFollowCursor = true; | |
| break; | |
| case "avatar ready": | |
| border = size.avatar; | |
| zoo[data[1]].avatar = true; | |
| break; | |
| } | |
| }; | |
| loop(update); | |
| } | |
| /////////////////////////////////////////////////////////////////////////////👆 2 | |
| // WARNING: declaring global variables from here then capture it in setup() or update() will not works 😏 | |
| function setup() { | |
| zoo = Array.from( | |
| { length: instance }, | |
| (_, i) => | |
| new Position( | |
| "window" in globalThis | |
| ? random.int(capacity) //👈 set random capacity in the main thread | |
| : capacities.at(i)!, //👈 set capactiy in the worker thread from data passed by the parent thread | |
| ), | |
| ); // NOTE: Position is like DataView for buffer but based on memory layout when in worker thread | |
| // center all entities when setup in main thread | |
| if ("window" in globalThis) { | |
| for (const { x, y } of zoo) { | |
| x.fill(width / 2); | |
| y.fill(height / 2); | |
| } | |
| } | |
| if (printMemoryLayout) printMemTable(); | |
| } | |
| function update() { | |
| const clamp = (...$: number[]) => Math.min(Math.max($[1], $[0]), $[2]); | |
| const pad = border ??= size.particle * 2, vw = width - pad, vh = height - pad; | |
| if (isFollowCursor) return; // avatar looking at mouse pointer while wiggle | |
| for (const { cap, x, y, avatar } of zoo) { | |
| // for (let i= cap - 1; i--;) { | |
| let len = Math.ceil(cap / (worker || 1)) - 1; | |
| for (const offset = len * (actorID ?? 0); len--;) { // eratic particles | |
| const i = offset + len; | |
| const p = { x: Atomics.load(x, i), y: Atomics.load(y, i) }; | |
| const inside = is(p).inside(radius, mouse); | |
| if (avatar && !inside) continue; // freeze | |
| // eratic particles | |
| const rx = random.int(-inside, +inside); | |
| const ry = random.int(-inside, +inside); | |
| Atomics.store(x, i, clamp(pad, p.x + rx, vw)); | |
| Atomics.store(y, i, clamp(pad, p.y + ry, vh)); | |
| } | |
| } | |
| } | |
| /////////////////////////////////////////////////////////////////////////////👆 3 | |
| function is(p: { x: number; y: number }) { | |
| return { | |
| inside: (r: number, m: { x: number; y: number }) => | |
| (p.x - m.x + r / 2) ** 2 + (p.y - m.y - r / 2) ** 2 < r ** 2, | |
| }; | |
| } | |
| function printMemTable() { | |
| console.time("print memory layout"); | |
| const table = zoo.flatMap(( | |
| { x, y, cap }, | |
| ) => [{ | |
| malloc: cap * Position.each, | |
| }, { | |
| "|addr|👉": "|x|", | |
| start: x.byteOffset, | |
| end: x.byteOffset + x.byteLength, | |
| }, { | |
| "|addr|👉": "|y|", | |
| start: y.byteOffset, | |
| end: y.byteOffset + x.byteLength, | |
| }] | |
| ), //@ts-ignore: typescript can't infer `group.malloc` | |
| allocated = table.reduce((total, group) => total += group.malloc ?? 0, 0), | |
| available = buffer.byteLength - allocated; | |
| console.info("> every number are in bytes"); | |
| console.table(table); | |
| console.table({ "memory buffer": buffer.byteLength, allocated, available }); | |
| console.timeEnd("print memory layout"); | |
| } |
| import type { Neko as DesktopWindow } from "https://deno.land/x/neko/core/Neko.ts"; | |
| export default class MemViz { | |
| #width?: number; | |
| #height?: number; | |
| #framebuffer?: Uint8Array; | |
| #window?: DesktopWindow; | |
| static async new(buffer: ArrayBufferLike, title = "memviz") { | |
| if (!("window" in globalThis)) throw "MemViz must be in Main thread"; | |
| const self = new MemViz(MemViz._guard); | |
| self.#height = self.#width = Math.floor(Math.sqrt(buffer.byteLength) / 2); | |
| if ("Deno" in globalThis) { | |
| self.#window = new (await import("https://deno.land/x/neko/core/Neko.ts")) | |
| .Neko({ | |
| width: self.#width, | |
| height: self.#height, | |
| title, | |
| // resize: true, | |
| topmost: true, | |
| }); | |
| self.#framebuffer = new Uint8Array(buffer); | |
| } else { | |
| self.#img = new ImageData( | |
| new Uint8ClampedArray(buffer), | |
| self.#width, | |
| self.#height, | |
| ); | |
| const canvas: HTMLCanvasElement = | |
| document.querySelector("canvas#memviz") ?? | |
| document.createElement("canvas"); | |
| self.#ctx = canvas.getContext("2d")!; | |
| document.body.append(canvas); | |
| } | |
| return self; | |
| } | |
| draw() { | |
| if ("Deno" in globalThis) { | |
| this.#window!.setFrameBuffer( | |
| this.#framebuffer!, | |
| this.#width, | |
| this.#height, | |
| ); | |
| } else this.#ctx!.putImageData(this.#img!, 0, 0); | |
| } | |
| #ctx?: CanvasRenderingContext2D; | |
| #img?: ImageData; | |
| private static readonly _guard = Symbol(); // to bad typesccrypt can't do typeof MemViz.#guard | |
| constructor(guard: typeof MemViz._guard) { | |
| if (guard !== MemViz._guard) throw null; | |
| } | |
| } |
| // import {} from "https://esm.sh/@dice-roller/rpg-dice-roller"; | |
| import { faker } from "https://esm.sh/@faker-js/faker"; | |
| const { round, random: rand } = Math; | |
| export function int(min: number, max: number): number; | |
| export function int($: { min: number; max: number }): number; | |
| export function int( | |
| min: { min: number; max: number } | number, | |
| max?: number, | |
| ) { | |
| if (typeof min !== "number") (max = min.max, min = min.min); | |
| return round(min + rand() * (max! - min)); | |
| } | |
| export function color(c?: string): string { | |
| const { color: { human } } = faker, { reserved } = color; | |
| return reserved.has(c = human()) || c === "black" | |
| ? color(c) | |
| : (reserved.add(c), c); | |
| } | |
| color.reserved = new Set<string>(); | |
| export function avatar(url?: string): string { | |
| const { image: { avatar: ipfs } } = faker, { reserved } = avatar; | |
| return reserved.has(url = ipfs()) ? avatar(url) : (reserved.add(url), url); | |
| } | |
| avatar.reserved = new Set<string>(); | |
| avatar.elonmusk = Object.assign(function (i?: number): string { | |
| const { elonmusk } = avatar, { at, reserved } = elonmusk, rand = int; | |
| return reserved.has(i = rand(0, hash.elonmusk.length - 1)) | |
| ? elonmusk(i) | |
| : (reserved.add(i), at(i)); | |
| }, { | |
| reserved: new Set<number>(), | |
| at: (index: number) => dir.elonmusk + hash.elonmusk[index] + "_400x400.jpg", | |
| }); | |
| const hash = { | |
| elonmusk: [ | |
| "3VYnFrzx", | |
| "DgO3WL4S", | |
| "FS8-70Ie", | |
| "IY9Gx6Ok", | |
| "Nyn1HZWF", | |
| "RJ5EI7PS", | |
| "Xz3fufYY", | |
| "Y4s_eu5O", | |
| "dHw9JcrK", | |
| "foUrqiEw", | |
| "itVSA7l0", | |
| "mXIcYtIs", | |
| "my_Sigxw", | |
| ], | |
| }; | |
| const dir = { | |
| elonmusk: | |
| "https://cloudflare-ipfs.com/ipfs/QmXtX4hzEH3bbb69Sr2MD5B7GLCzktbK1mFG8hM39cWpuZ/", | |
| }; |
| const tcol = Deno.consoleSize().columns; | |
| export const spacer = (n: number, char: string) => | |
| Array(Math.floor(n)).fill(char).join(""); | |
| export function breakline(open: string, title: string, close: string) { | |
| const line = liner(spacer((tcol - title.length - 2) / 2, "─")); | |
| return [line([open]), title, line([, close])]; | |
| } | |
| export const liner = (sep: string) => | |
| ( | |
| [open = "", close = ""]: [open?: string, close?: string], | |
| s = open || close, | |
| ) => open + sep.slice(0, -s.length) + close; | |
| export const header = (title: string) => breakline("┌─", title, "─┐"); | |
| export const footer = (title: string) => breakline("└─", title, "─┘"); |
| // Copyright 2023 Fahmi Akbar Wildana <[email protected]> | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| export function malloc(buffer: ArrayBufferLike) { | |
| let prevType: BYTES_PER_ELEMENT = 1, count = 0, prevSize = 0; | |
| // IIFE | |
| return <T extends TypedArrayConstructor>(TypedArray: T, size: number) => | |
| (($) => (prevType = $.BYTES_PER_ELEMENT, $ as InstanceType<T>))( | |
| new TypedArray(buffer, prevType * prevSize * count++, prevSize = size), //👈 | |
| ); | |
| } | |
| export class WorkerThread extends Worker { | |
| constructor( | |
| public url: string, | |
| public buffer: SharedArrayBuffer, | |
| public id: number = 0, | |
| ) { | |
| super(url, { type: "module" }); | |
| this.postMessage([id, this.buffer]); // share memory buffer with the/another worker thread | |
| } | |
| /** tell worker to run while send info about the memory layout */ | |
| run(layout: ArrayBufferLike, config?: { copy: true }) { | |
| this.postMessage(layout, config?.copy ? undefined : { transfer: [layout] }); | |
| } | |
| relayMessage(message: unknown, options?: StructuredSerializeOptions) { | |
| this.postMessage(["relay", message, ...options ? [options] : []]); | |
| } | |
| // concurrent? 🤔 | |
| static selfSpawn(url: string, limit = 1) { | |
| return new Promise<MayRelay>((resolve) => { | |
| const $ = {} as MayRelay; | |
| self.addEventListener("message", ({ data }) => { | |
| if (data[1] instanceof SharedArrayBuffer) { | |
| [$.id, $.memory] = data; | |
| if ($.id < limit) { | |
| $.relay = new WorkerThread(url, $.memory, ($.id = +$.id) + 1); | |
| } | |
| } else if ($.memory && data instanceof ArrayBuffer) { | |
| $.layout = data; | |
| resolve($); | |
| if ($.relay) $.relay.run($.layout, { copy: true }); | |
| } else if (($.relay || $.id === limit) && data[0] === "relay") { | |
| $.onrelay?.(data[1]); | |
| $.relay?.relayMessage(data[1], data[2]); | |
| } | |
| }); | |
| }); | |
| } | |
| // await-ing will wait Shared memory before returning | |
| static get self(): Promise<SharedData> { | |
| return new Promise((resolve) => { | |
| let memory: SharedArrayBuffer, id: number; | |
| self.onmessage = ({ data }) => { | |
| if (data[1] instanceof SharedArrayBuffer) [id, memory] = data; | |
| else if (memory && data instanceof ArrayBuffer) { | |
| resolve({ id, memory, layout: data }); | |
| } | |
| }; | |
| }); | |
| } | |
| } | |
| export function loop(fn: () => void) { | |
| if ("Deno" in globalThis) { | |
| const chan = new MessageChannel(); | |
| chan.port1.postMessage(null); | |
| chan.port2.onmessage = () => { | |
| fn(); | |
| chan.port1.postMessage(null); | |
| }; | |
| chan.port2.start(); | |
| } else {requestAnimationFrame(function loop() { | |
| fn(); | |
| requestAnimationFrame(loop); | |
| });} | |
| } | |
| interface MayRelay extends SharedData { | |
| relay: WorkerThread; | |
| onrelay?: (data: Parameters<typeof structuredClone>[0]) => void; | |
| } | |
| interface SharedData { | |
| id: number; | |
| memory: SharedArrayBuffer; | |
| layout: ArrayBuffer; | |
| } | |
| type BYTES_PER_ELEMENT = number; | |
| type TypedArrayConstructor = | |
| | Uint8ClampedArrayConstructor | |
| | Uint8ArrayConstructor | |
| | Uint16ArrayConstructor | |
| | Uint32ArrayConstructor | |
| | BigUint64ArrayConstructor | |
| | Int8ArrayConstructor | |
| | Int16ArrayConstructor | |
| | Int32ArrayConstructor | |
| | BigInt64ArrayConstructor | |
| | Float32ArrayConstructor | |
| | Float64ArrayConstructor; |