Merge pull request #1496 from RoboSats/fix-robohash-generator

Fix Robohash generation on web
This commit is contained in:
KoalaSat 2024-09-23 22:53:44 +00:00 committed by GitHub
commit 98a16e633a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 61 additions and 77 deletions

View File

@ -1,7 +1,11 @@
interface Task { interface Task {
robohash: Robohash; robohash: Robohash;
resolves: Array<(result: string) => void>; }
rejects: Array<(reason?: Error) => void>;
interface RoboWorker {
id: number;
worker: Worker;
busy: boolean;
} }
interface Robohash { interface Robohash {
@ -10,71 +14,25 @@ interface Robohash {
cacheKey: string; cacheKey: string;
} }
interface RoboWorker {
worker: Worker;
busy: boolean;
}
class RoboGenerator { class RoboGenerator {
private assetsCache: Record<string, string> = {}; private assetsCache: Record<string, string> = {};
private readonly workers: RoboWorker[] = []; private readonly workers: RoboWorker[] = [];
private readonly queue: Task[] = []; private readonly taskQueue: Task[] = [];
private numberOfWorkers: number = 8;
private waitingForLibrary: boolean = true;
private resolves: Record<string, ((result: string) => void)[]> = {};
private rejects: Record<string, ((reason?: Error) => void)[]> = {};
constructor() { constructor() {
// limit to 8 workers for (let i = 0; i < this.numberOfWorkers; i++) {
const numCores = 8; this.workers.push(this.createWorker(i));
for (let i = 0; i < numCores; i++) {
const worker = new Worker(new URL('./robohash.worker.ts', import.meta.url));
worker.onmessage = this.assignTasksToWorkers.bind(this);
this.workers.push({ worker, busy: false });
} }
}
private assignTasksToWorkers(): void { setTimeout(() => {
const availableWorker = this.workers.find((w) => !w.busy); this.waitingForLibrary = false;
}, 1000);
if (availableWorker) {
const task = this.queue.shift();
if (task) {
availableWorker.busy = true;
availableWorker.worker.postMessage(task.robohash);
// Clean up the event listener and free the worker after receiving the result
const cleanup = (): void => {
availableWorker.worker.removeEventListener('message', completionCallback);
availableWorker.busy = false;
};
// Resolve the promise when the task is completed
const completionCallback = (event: MessageEvent): void => {
if (event.data.cacheKey === task.robohash.cacheKey) {
const { cacheKey, imageUrl } = event.data;
// Update the cache and resolve the promise
this.assetsCache[cacheKey] = imageUrl;
cleanup();
task.resolves.forEach((f) => {
f(imageUrl);
});
}
};
availableWorker.worker.addEventListener('message', completionCallback);
// Reject the promise if an error occurs
availableWorker.worker.addEventListener('error', (error) => {
cleanup();
task.rejects.forEach((f) => {
f(new Error(error.message));
});
});
}
}
} }
public generate: (hash: string, size: 'small' | 'large') => Promise<string> = async ( public generate: (hash: string, size: 'small' | 'large') => Promise<string> = async (
@ -86,8 +44,7 @@ class RoboGenerator {
return this.assetsCache[cacheKey]; return this.assetsCache[cacheKey];
} else { } else {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
let task = this.queue.find((t) => t.robohash.cacheKey === cacheKey); let task = this.taskQueue.find((task) => task.robohash.cacheKey === cacheKey);
if (!task) { if (!task) {
task = { task = {
robohash: { robohash: {
@ -95,19 +52,48 @@ class RoboGenerator {
size, size,
cacheKey, cacheKey,
}, },
resolves: [],
rejects: [],
}; };
this.queue.push(task);
} }
task.resolves.push(resolve); this.resolves[cacheKey] = [...(this.resolves[cacheKey] ?? []), resolve];
task.rejects.push(reject); this.rejects[cacheKey] = [...(this.rejects[cacheKey] ?? []), reject];
this.assignTasksToWorkers(); this.addTask(task);
}); });
} }
}; };
createWorker = (id: number): RoboWorker => {
const worker = new Worker(new URL('./robohash.worker.ts', import.meta.url));
worker.onmessage = (event) => {
const { cacheKey, imageUrl } = event.data;
// Update the cache and resolve the promise
this.assetsCache[cacheKey] = imageUrl;
this.resolves[cacheKey].forEach((f) => {
f(imageUrl);
});
if (this.taskQueue.length > 0) {
const nextTask = this.taskQueue.shift();
this.workers[id].busy = true;
this.workers[id].worker.postMessage(nextTask);
} else {
this.workers[id].busy = false;
}
};
return { id, worker, busy: false };
};
addTask = (task: any) => {
const availableWorker = this.workers.find((w) => !w.busy);
if (availableWorker && !this.waitingForLibrary) {
availableWorker.worker.postMessage(task);
} else {
this.taskQueue.push(task);
}
};
} }
export const robohash = new RoboGenerator(); export const robohash = new RoboGenerator();

View File

@ -1,17 +1,15 @@
import { async_generate_robohash } from 'robo-identities-wasm'; import { async_generate_robohash } from 'robo-identities-wasm';
// Listen for messages from the main thread self.onmessage = async (event) => {
self.addEventListener('message', (event) => { if (!event.data) return;
void (async () => { try {
const { hash, size, cacheKey } = event.data; const { hash, size, cacheKey } = event.data.robohash;
// Generate the image using async_image_base // Generate the image using async_image_base
const t0 = performance.now();
const avatarB64: string = await async_generate_robohash(hash, size === 'small' ? 80 : 256); const avatarB64: string = await async_generate_robohash(hash, size === 'small' ? 80 : 256);
const imageUrl = `data:image/png;base64,${avatarB64}`; const imageUrl = `data:image/png;base64,${avatarB64}`;
const t1 = performance.now();
console.log(`Avatar generated in: ${t1 - t0} ms`);
// Send the result back to the main thread // Send the result back to the main thread
self.postMessage({ cacheKey, imageUrl }); self.postMessage({ cacheKey, imageUrl });
})(); } catch (error) {
}); console.error('Wasm error:', error);
}
};