Skip to content

Commit 387df22

Browse files
committed
comlink and web worker example
1 parent 9e564a7 commit 387df22

8 files changed

Lines changed: 612 additions & 489 deletions

File tree

examples/web-worker-compute.ipynb

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Heavy Computation in a Web Worker\n",
8+
"\n",
9+
"The JavaScript kernel runs inside a **Web Worker** \u2014 a background thread separate from the browser's main UI thread.\n",
10+
"\n",
11+
"This means CPU-intensive code does not freeze the browser. While a heavy cell executes, you can still scroll the page, interact with other UI elements, and queue up the next cell.\n",
12+
"\n",
13+
"This notebook runs four progressively heavier benchmarks, all off the main thread:\n",
14+
"\n",
15+
"- **Sieve of Eratosthenes** \u2014 find all primes up to 200,000,000\n",
16+
"- **Monte Carlo \u03c0 estimation** \u2014 estimate \u03c0 with 200 million random samples\n",
17+
"- **Large array sort** \u2014 sort 20 million random floats\n",
18+
"- **Matrix multiplication** \u2014 multiply two 1024\u00d71024 matrices\n",
19+
"\n",
20+
"> **Try it:** start running the cells and try scrolling this page \u2014 it stays fully responsive throughout."
21+
]
22+
},
23+
{
24+
"cell_type": "markdown",
25+
"metadata": {},
26+
"source": [
27+
"## Setup\n",
28+
"\n",
29+
"A shared `time()` helper measures each computation and collects results for a summary visualization at the end."
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": null,
35+
"metadata": {},
36+
"outputs": [],
37+
"source": [
38+
"// Shared results log \u2014 persists across all cells in this notebook\n",
39+
"const results = [];\n",
40+
"\n",
41+
"// Runs fn(), logs elapsed time, and stores the result\n",
42+
"function time(label, fn) {\n",
43+
" const t0 = performance.now();\n",
44+
" const value = fn();\n",
45+
" const elapsed = +(performance.now() - t0).toFixed(1);\n",
46+
" results.push({ label, elapsed });\n",
47+
" console.log(`${label}: ${elapsed} ms`);\n",
48+
" return value;\n",
49+
"}"
50+
]
51+
},
52+
{
53+
"cell_type": "markdown",
54+
"metadata": {},
55+
"source": [
56+
"## Sieve of Eratosthenes\n",
57+
"\n",
58+
"A classic prime-finding algorithm: mark composite numbers by iterating over multiples of each prime found so far, then count what remains. Finding all primes up to 200 million requires marking hundreds of millions of entries in a typed array \u2014 a tight, cache-friendly inner loop with no pauses."
59+
]
60+
},
61+
{
62+
"cell_type": "code",
63+
"execution_count": null,
64+
"metadata": {},
65+
"outputs": [],
66+
"source": [
67+
"function sieve(limit) {\n",
68+
" const composite = new Uint8Array(limit + 1); // composite[i] = 1 means i is not prime\n",
69+
" for (let i = 2; i * i <= limit; i++) {\n",
70+
" if (!composite[i]) {\n",
71+
" for (let j = i * i; j <= limit; j += i) {\n",
72+
" composite[j] = 1;\n",
73+
" }\n",
74+
" }\n",
75+
" }\n",
76+
" let count = 0;\n",
77+
" for (let i = 2; i <= limit; i++) {\n",
78+
" if (!composite[i]) count++;\n",
79+
" }\n",
80+
" return count;\n",
81+
"}\n",
82+
"\n",
83+
"const primeLimit = 200_000_000;\n",
84+
"const primeCount = time(\n",
85+
" `Sieve of Eratosthenes (up to ${primeLimit.toLocaleString()})`,\n",
86+
" () => sieve(primeLimit)\n",
87+
");\n",
88+
"console.log(`Found ${primeCount.toLocaleString()} primes`);\n",
89+
"primeCount"
90+
]
91+
},
92+
{
93+
"cell_type": "markdown",
94+
"metadata": {},
95+
"source": [
96+
"## Monte Carlo \u03c0 Estimation\n",
97+
"\n",
98+
"Throw random darts at a unit square and count how many land inside the inscribed quarter-circle. The ratio converges to \u03c0/4. With 200 million samples the result is typically accurate to about 5 decimal places \u2014 and generating 400 million random numbers plus comparisons keeps the CPU busy for several seconds."
99+
]
100+
},
101+
{
102+
"cell_type": "code",
103+
"execution_count": null,
104+
"metadata": {},
105+
"outputs": [],
106+
"source": [
107+
"function estimatePi(samples) {\n",
108+
" let inside = 0;\n",
109+
" for (let i = 0; i < samples; i++) {\n",
110+
" const x = Math.random();\n",
111+
" const y = Math.random();\n",
112+
" if (x * x + y * y <= 1) inside++;\n",
113+
" }\n",
114+
" return (4 * inside) / samples;\n",
115+
"}\n",
116+
"\n",
117+
"const piSamples = 200_000_000;\n",
118+
"const piEstimate = time(\n",
119+
" `Monte Carlo \u03c0 (${piSamples.toLocaleString()} samples)`,\n",
120+
" () => estimatePi(piSamples)\n",
121+
");\n",
122+
"console.log(`\u03c0 \u2248 ${piEstimate.toFixed(8)}`);\n",
123+
"console.log(`error: ${Math.abs(piEstimate - Math.PI).toFixed(8)}`);\n",
124+
"piEstimate"
125+
]
126+
},
127+
{
128+
"cell_type": "markdown",
129+
"metadata": {},
130+
"source": [
131+
"## Sorting 20 Million Random Floats\n",
132+
"\n",
133+
"Fill a `Float64Array` with random values, then sort it. JavaScript engines use TimSort (a hybrid merge/insertion sort), giving O(n log n) comparisons. For 20 million elements that is roughly 486 million comparison operations. `TypedArray.sort()` sorts numerically by default \u2014 no comparator needed."
134+
]
135+
},
136+
{
137+
"cell_type": "code",
138+
"execution_count": null,
139+
"metadata": {},
140+
"outputs": [],
141+
"source": [
142+
"const sortSize = 20_000_000;\n",
143+
"\n",
144+
"const sortedArr = time(\n",
145+
" `Sort ${sortSize.toLocaleString()} random floats`,\n",
146+
" () => {\n",
147+
" const arr = new Float64Array(sortSize);\n",
148+
" for (let i = 0; i < sortSize; i++) arr[i] = Math.random();\n",
149+
" arr.sort();\n",
150+
" return arr;\n",
151+
" }\n",
152+
");\n",
153+
"\n",
154+
"console.log(`min: ${sortedArr[0].toFixed(8)}`);\n",
155+
"console.log(`max: ${sortedArr[sortedArr.length - 1].toFixed(8)}`);\n",
156+
"sortedArr.length"
157+
]
158+
},
159+
{
160+
"cell_type": "markdown",
161+
"metadata": {},
162+
"source": [
163+
"## Matrix Multiplication (1024\u00d71024)\n",
164+
"\n",
165+
"Multiplying two 1024\u00d71024 matrices requires 1024\u00b3 \u2248 1.07 billion floating-point multiply-accumulate operations. The inner loop follows the i\u2013k\u2013j iteration order for better cache locality \u2014 reading `B` in sequential memory strides rather than the naive i\u2013j\u2013k column-jumping order."
166+
]
167+
},
168+
{
169+
"cell_type": "code",
170+
"execution_count": null,
171+
"metadata": {},
172+
"outputs": [],
173+
"source": [
174+
"function matMul(a, b, n) {\n",
175+
" const c = new Float64Array(n * n);\n",
176+
" for (let i = 0; i < n; i++) {\n",
177+
" for (let k = 0; k < n; k++) {\n",
178+
" const aik = a[i * n + k];\n",
179+
" for (let j = 0; j < n; j++) {\n",
180+
" c[i * n + j] += aik * b[k * n + j];\n",
181+
" }\n",
182+
" }\n",
183+
" }\n",
184+
" return c;\n",
185+
"}\n",
186+
"\n",
187+
"const matSize = 1024;\n",
188+
"const matA = Float64Array.from({ length: matSize * matSize }, Math.random);\n",
189+
"const matB = Float64Array.from({ length: matSize * matSize }, Math.random);\n",
190+
"\n",
191+
"const matC = time(\n",
192+
" `Matrix multiply ${matSize}\u00d7${matSize}`,\n",
193+
" () => matMul(matA, matB, matSize)\n",
194+
");\n",
195+
"\n",
196+
"console.log(`C[0,0] = ${matC[0].toFixed(6)}`);\n",
197+
"matC.length"
198+
]
199+
},
200+
{
201+
"cell_type": "markdown",
202+
"metadata": {},
203+
"source": [
204+
"## Results Summary\n",
205+
"\n",
206+
"All four benchmarks ran entirely inside the Web Worker. The timings below are from your run \u2014 actual values vary by device and browser."
207+
]
208+
},
209+
{
210+
"cell_type": "code",
211+
"execution_count": null,
212+
"metadata": {},
213+
"outputs": [],
214+
"source": [
215+
"const maxElapsed = Math.max(...results.map(r => r.elapsed));\n",
216+
"const rows = results.map(({ label, elapsed }) => {\n",
217+
" const pct = (elapsed / maxElapsed * 90).toFixed(1);\n",
218+
" const bar = `<div style=\"background:linear-gradient(90deg,#4a90d9,#357abd);height:22px;width:${pct}%;border-radius:3px;min-width:2px\"></div>`;\n",
219+
" return `<tr><td style=\"padding:7px 20px 7px 0;font-family:monospace;font-size:13px;white-space:nowrap;color:#333\">${label}</td><td style=\"padding:7px 8px;min-width:260px;vertical-align:middle\">${bar}</td><td style=\"padding:7px 0 7px 10px;font-family:monospace;font-size:13px;color:#555;white-space:nowrap\">${elapsed} ms</td></tr>`;\n",
220+
"}).join('');\n",
221+
"\n",
222+
"`<div style=\"padding:20px 24px;background:#fafafa;border:1px solid #ddd;border-radius:8px;display:inline-block;font-family:sans-serif\"><h3 style=\"margin:0 0 16px;font-size:15px;color:#222\">Benchmark Results</h3><table style=\"border-collapse:collapse\">${rows}</table></div>`"
223+
]
224+
}
225+
],
226+
"metadata": {
227+
"kernelspec": {
228+
"display_name": "JavaScript (Worker)",
229+
"language": "javascript",
230+
"name": "javascript-worker"
231+
},
232+
"language_info": {
233+
"file_extension": ".js",
234+
"mimetype": "text/javascript",
235+
"name": "javascript",
236+
"version": "ES2021"
237+
}
238+
},
239+
"nbformat": 4,
240+
"nbformat_minor": 4
241+
}

packages/javascript-kernel/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@jupyterlite/services": "^0.7.0",
4848
"@lumino/coreutils": "^2.0.0",
4949
"astring": "^1.9.0",
50+
"comlink": "^4.3.1",
5051
"meriyah": "^4.3.9"
5152
},
5253
"devDependencies": {

packages/javascript-kernel/src/kernel.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,14 @@ export class JavaScriptKernel extends BaseKernel implements IKernel {
265265
await this.onRuntimeReady({
266266
runtime: 'iframe',
267267
globalScope: context.globalScope,
268-
executor: context.evaluator.executor,
269-
execute: async code => Promise.resolve(context.evaluate(code))
268+
executor: context.executor,
269+
execute: async code => {
270+
const reply = await context.execute(code);
271+
if (reply.status === 'error') {
272+
throw this._createRuntimeInitializationError(reply);
273+
}
274+
return reply;
275+
}
270276
});
271277
}
272278
});
@@ -403,7 +409,7 @@ export namespace JavaScriptKernel {
403409
| IWorkerRuntimeReadyContext;
404410

405411
/**
406-
* Factory used to customize the iframe runtime executor.
412+
* Factory used to customize iframe runtime evaluation behavior.
407413
*/
408414
export type IExecutorFactory = (
409415
globalScope: Record<string, any>

0 commit comments

Comments
 (0)