Skip to content

Commit 5cd2d70

Browse files
committed
refactor: move fixture generation to worker
1 parent d994795 commit 5cd2d70

File tree

4 files changed

+267
-75
lines changed

4 files changed

+267
-75
lines changed

packages/idb-cache-app/src/App.tsx

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "./App.css";
22
import { IDBCache } from "@instructure/idb-cache";
33
import { useCallback, useRef, useState, useEffect } from "react";
4-
import { deterministicHash, generateTextOfSize } from "./utils";
4+
import { deterministicHash } from "./utils";
55
import { Button } from "@instructure/ui-buttons";
66
import { Metric } from "@instructure/ui-metric";
77
import { View } from "@instructure/ui-view";
@@ -17,6 +17,7 @@ import {
1717
WrappedFlexItem,
1818
} from "./components/WrappedFlexItem";
1919
import { Test } from "./components/Test";
20+
import { generateTextOfSize } from "./fixtures";
2021

2122
// For demonstration/testing purposes.
2223
// Do *not* store cacheKey to localStorage in production.
@@ -155,22 +156,28 @@ const App = () => {
155156
saveContentKey(key);
156157

157158
const start1 = performance.now();
158-
const paragraphs = Array.from({ length: DEFAULT_NUM_ITEMS }, (_, index) =>
159-
generateTextOfSize(itemSize, `${key}-${index}`),
160-
);
161-
const end1 = performance.now();
162-
setTimeToGenerate(end1 - start1);
159+
try {
160+
const paragraphs = await Promise.all(
161+
Array.from({ length: DEFAULT_NUM_ITEMS }, (_, index) =>
162+
generateTextOfSize(itemSize, `${key}-${index}`),
163+
),
164+
);
165+
const end1 = performance.now();
166+
setTimeToGenerate(end1 - start1);
167+
168+
const start2 = performance.now();
169+
170+
for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) {
171+
await cache.setItem(`item-${key}-${i}`, paragraphs[i]);
172+
}
163173

164-
const start2 = performance.now();
174+
const end2 = performance.now();
175+
setSetItemTime(end2 - start2);
165176

166-
for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) {
167-
await cache.setItem(`item-${key}-${i}`, paragraphs[i]);
177+
setHash1(deterministicHash(paragraphs.join("")));
178+
} catch (error) {
179+
console.error("Error during text generation and storage:", error);
168180
}
169-
170-
const end2 = performance.now();
171-
setSetItemTime(end2 - start2);
172-
173-
setHash1(deterministicHash(paragraphs.join("")));
174181
}, [itemSize]);
175182

176183
const retrieveAndDecrypt = useCallback(async () => {
@@ -180,21 +187,25 @@ const App = () => {
180187
return;
181188
}
182189

183-
const results: Array<string | null> = [];
184-
const start = performance.now();
190+
try {
191+
const results: Array<string | null> = [];
192+
const start = performance.now();
185193

186-
for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) {
187-
const result = await cache.getItem(`item-${contentKey}-${i}`);
188-
results.push(result);
189-
}
194+
for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) {
195+
const result = await cache.getItem(`item-${contentKey}-${i}`);
196+
results.push(result);
197+
}
190198

191-
const end = performance.now();
192-
setGetItemTime(end - start);
193-
setHash2(
194-
results.filter((x) => x).length > 0
195-
? deterministicHash(results.join(""))
196-
: null,
197-
);
199+
const end = performance.now();
200+
setGetItemTime(end - start);
201+
setHash2(
202+
results.filter((x) => x).length > 0
203+
? deterministicHash(results.join(""))
204+
: null,
205+
);
206+
} catch (error) {
207+
console.error("Error during text retrieval and decryption:", error);
208+
}
198209
}, [contentKey]);
199210

200211
const cleanup = useCallback(async () => {
@@ -204,10 +215,14 @@ const App = () => {
204215
return;
205216
}
206217

207-
const start = performance.now();
208-
await cache.cleanup();
209-
const end = performance.now();
210-
setCleanupTime(end - start);
218+
try {
219+
const start = performance.now();
220+
await cache.cleanup();
221+
const end = performance.now();
222+
setCleanupTime(end - start);
223+
} catch (error) {
224+
console.error("Error during cache cleanup:", error);
225+
}
211226
}, []);
212227

213228
const count = useCallback(async () => {
@@ -217,11 +232,15 @@ const App = () => {
217232
return;
218233
}
219234

220-
const start = performance.now();
221-
const count = await cache.count();
222-
const end = performance.now();
223-
setCountTime(end - start);
224-
setItemCount(count);
235+
try {
236+
const start = performance.now();
237+
const count = await cache.count();
238+
const end = performance.now();
239+
setCountTime(end - start);
240+
setItemCount(count);
241+
} catch (error) {
242+
console.error("Error during cache count:", error);
243+
}
225244
}, []);
226245

227246
const clear = useCallback(async () => {
@@ -231,11 +250,15 @@ const App = () => {
231250
return;
232251
}
233252

234-
const start = performance.now();
235-
await cache.clear();
236-
localStorage.removeItem("keyCounter");
237-
const end = performance.now();
238-
setClearTime(end - start);
253+
try {
254+
const start = performance.now();
255+
await cache.clear();
256+
localStorage.removeItem("keyCounter");
257+
const end = performance.now();
258+
setClearTime(end - start);
259+
} catch (error) {
260+
console.error("Error during cache clear:", error);
261+
}
239262
}, []);
240263

241264
return (
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { uuid } from "./utils";
2+
3+
interface WorkerMessage {
4+
requestId: string;
5+
targetSizeInBytes: number;
6+
seed: string;
7+
}
8+
9+
interface WorkerResponse {
10+
requestId: string;
11+
text: string;
12+
}
13+
14+
/**
15+
* Generates the Worker code as a string using the Function.prototype.toString() strategy.
16+
* This ensures that the Worker code is self-contained and not transformed by the bundler.
17+
* The worker code is written as a function and then converted to a string.
18+
*/
19+
function generateTextOfSizeWorkerCode(): string {
20+
const workerFunction = () => {
21+
// Define types for internal worker usage
22+
interface WorkerMessage {
23+
requestId: string;
24+
targetSizeInBytes: number;
25+
seed: string;
26+
}
27+
28+
interface WorkerResponse {
29+
requestId: string;
30+
text: string;
31+
}
32+
33+
/**
34+
* Utility function to convert a seed string into a numerical hash.
35+
*
36+
* @param str - The seed string to hash.
37+
* @returns A numerical hash derived from the input string.
38+
*/
39+
function hashCode(str: string): number {
40+
let hash = 0;
41+
for (let i = 0; i < str.length; i++) {
42+
hash = (hash * 31 + str.charCodeAt(i)) >>> 0; // Ensure unsigned 32-bit integer
43+
}
44+
return hash;
45+
}
46+
47+
/**
48+
* Seeded pseudo-random number generator using Linear Congruential Generator (LCG).
49+
*
50+
* @param seed - The seed string to initialize the generator.
51+
* @returns A function that generates a pseudo-random number between 0 (inclusive) and 1 (exclusive).
52+
*/
53+
function seededRandom(seed: string): () => number {
54+
let state: number = hashCode(seed);
55+
const a: number = 1664525;
56+
const c: number = 1013904223;
57+
const m: number = 2 ** 32;
58+
59+
/**
60+
* Generates the next pseudo-random number in the sequence.
61+
*
62+
* @returns A pseudo-random number between 0 (inclusive) and 1 (exclusive).
63+
*/
64+
function random(): number {
65+
state = (a * state + c) >>> 0; // Update state with LCG formula
66+
return state / m;
67+
}
68+
69+
return random;
70+
}
71+
72+
/**
73+
* Calculates the byte size of a string using UTF-8 encoding.
74+
*
75+
* @param str - The string whose byte size is to be calculated.
76+
* @returns The byte size of the input string.
77+
*/
78+
function calculateByteSize(str: string): number {
79+
return new TextEncoder().encode(str).length;
80+
}
81+
82+
/**
83+
* Listener for messages from the main thread.
84+
* Generates a deterministic random text string based on the provided seed and target size.
85+
*/
86+
self.onmessage = (event: MessageEvent): void => {
87+
const data: WorkerMessage = event.data;
88+
const { requestId, targetSizeInBytes, seed } = data;
89+
90+
const rand: () => number = seededRandom(seed);
91+
const estimatedChars: number = Math.ceil(targetSizeInBytes);
92+
const charArray: string[] = new Array(estimatedChars);
93+
94+
for (let i = 0; i < estimatedChars; i++) {
95+
// Generate a random printable ASCII character (codes 33 to 126)
96+
charArray[i] = String.fromCharCode(33 + Math.floor(rand() * 94));
97+
}
98+
99+
let result: string = charArray.join("");
100+
101+
// Ensure the generated result matches the exact target size
102+
while (calculateByteSize(result) > targetSizeInBytes) {
103+
result = result.slice(0, -1);
104+
}
105+
106+
const response: WorkerResponse = { requestId, text: result };
107+
// Send the generated text back to the main thread
108+
postMessage(response);
109+
};
110+
};
111+
112+
// Convert the worker function to a string and invoke it immediately
113+
return `(${workerFunction.toString()})();`;
114+
}
115+
116+
/**
117+
* Creates a Web Worker from a given code string by converting it to a Blob URL.
118+
*
119+
* @param code The Worker code as a string.
120+
* @returns A new Worker instance.
121+
*/
122+
function createWorkerFromCode(code: string): Worker {
123+
const blob: Blob = new Blob([code], { type: "application/javascript" });
124+
const blobURL: string = URL.createObjectURL(blob);
125+
return new Worker(blobURL);
126+
}
127+
128+
/**
129+
* Asynchronously generates a deterministic random text string of a specified byte size
130+
* by offloading the task to a Web Worker. Supports multiple concurrent requests using requestId.
131+
*
132+
* @param targetSizeInBytes The desired byte size of the generated string.
133+
* @param seed Optional seed for the random number generator. Defaults to "default".
134+
* @returns A Promise that resolves to the generated string.
135+
*/
136+
export async function generateTextOfSize(
137+
targetSizeInBytes: number,
138+
seed = "default"
139+
): Promise<string> {
140+
return new Promise<string>((resolve, reject) => {
141+
const requestId: string = uuid();
142+
143+
// Generate the worker code and create a new worker
144+
const workerCode: string = generateTextOfSizeWorkerCode();
145+
const worker: Worker = createWorkerFromCode(workerCode);
146+
147+
/**
148+
* Handler for messages from the worker.
149+
* Resolves the promise if the response matches the requestId.
150+
*/
151+
const handleMessage = (event: MessageEvent): void => {
152+
const data: WorkerResponse = event.data;
153+
if (data.requestId === requestId) {
154+
resolve(data.text);
155+
cleanup();
156+
}
157+
};
158+
159+
/**
160+
* Handler for errors from the worker.
161+
* Rejects the promise and cleans up the worker.
162+
*/
163+
const handleError = (error: ErrorEvent): void => {
164+
reject(error);
165+
cleanup();
166+
};
167+
168+
/**
169+
* Cleans up event listeners and terminates the worker.
170+
*/
171+
const cleanup = (): void => {
172+
worker.removeEventListener("message", handleMessage);
173+
worker.removeEventListener("error", handleError);
174+
worker.terminate();
175+
};
176+
177+
// Attach event listeners
178+
worker.addEventListener("message", handleMessage);
179+
worker.addEventListener("error", handleError);
180+
181+
// Send the message with the requestId
182+
const message: WorkerMessage = { requestId, targetSizeInBytes, seed };
183+
worker.postMessage(message);
184+
});
185+
}

0 commit comments

Comments
 (0)