Skip to content

Commit 06224b1

Browse files
committed
refactor: move fixture generation to worker
1 parent d994795 commit 06224b1

File tree

4 files changed

+270
-75
lines changed

4 files changed

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

0 commit comments

Comments
 (0)