Skip to content

Commit 836523a

Browse files
committed
Distinguish between multiple emulator worker instances in fallback mode
Generate a worker ID when creating a new instance, and include in the fallback communication channels with the service worker. That allows it to maintain a per-worker state and queue of commands. Updates #382
1 parent 7fc2687 commit 836523a

5 files changed

Lines changed: 120 additions & 47 deletions

File tree

public/embed-testbed.html

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ <h1>Embed Testbed</h1>
3030
type="checkbox"
3131
name="screen_update_messages"
3232
value="true"
33-
checked
3433
/>
3534
Get screen updates
3635
</label>
@@ -53,9 +52,8 @@ <h1>Embed Testbed</h1>
5352
<input type="submit" value="Load" />
5453
</form>
5554

56-
57-
<div id='iframes' style='display: flex; gap: 10px;'>
58-
<div class='iframe-outer'>
55+
<div id="iframes" style="display: flex; gap: 10px">
56+
<div class="iframe-outer">
5957
<iframe
6058
allow="cross-origin-isolated"
6159
style="border: 2px solid red"
@@ -73,12 +71,6 @@ <h1>Embed Testbed</h1>
7371
<script>
7472
const frame = document.querySelector("iframe");
7573

76-
onload = function () {
77-
if (location.search.includes("autoload")) {
78-
document.querySelector("form").submit();
79-
}
80-
};
81-
8274
onmessage = function (e) {
8375
const data = e.data;
8476
switch (data.type) {
@@ -91,30 +83,50 @@ <h1>Embed Testbed</h1>
9183
};
9284

9385
// Duplicate everything
94-
95-
const newFormEl = document.querySelector('form').cloneNode(true);
96-
newFormEl.target = 'frame2';
97-
document.body.insertBefore(newFormEl, document.querySelector('form').nextElementSibling);
98-
99-
const newIframeEl = document.querySelector('#iframes .iframe-outer').cloneNode(true);
100-
newIframeEl.querySelector('iframe').name = 'frame2';
101-
document.querySelector('#iframes').append(newIframeEl);
102-
86+
87+
const newFormEl = document.querySelector("form").cloneNode(true);
88+
newFormEl.target = "frame2";
89+
document.body.insertBefore(
90+
newFormEl,
91+
document.querySelector("form").nextElementSibling
92+
);
93+
94+
const newIframeEl = document
95+
.querySelector("#iframes .iframe-outer")
96+
.cloneNode(true);
97+
newIframeEl.querySelector("iframe").name = "frame2";
98+
document.querySelector("#iframes").append(newIframeEl);
99+
103100
// Event listeners
104-
101+
105102
document.querySelectorAll(".pause").forEach(el => {
106103
el.onclick = function () {
107-
el.closest('.iframe-outer').querySelector('iframe').contentWindow.postMessage({type: "emulator_pause"}, "*");
104+
el.closest(".iframe-outer")
105+
.querySelector("iframe")
106+
.contentWindow.postMessage(
107+
{type: "emulator_pause"},
108+
"*"
109+
);
108110
};
109111
});
110112
document.querySelectorAll(".unpause").forEach(el => {
111113
el.onclick = function () {
112-
el.closest('.iframe-outer').querySelector('iframe').contentWindow.postMessage(
113-
{type: "emulator_unpause"},
114-
"*"
115-
)
114+
el.closest(".iframe-outer")
115+
.querySelector("iframe")
116+
.contentWindow.postMessage(
117+
{type: "emulator_unpause"},
118+
"*"
119+
);
116120
};
117121
});
122+
123+
onload = function () {
124+
if (location.search.includes("autoload")) {
125+
for (const form of document.querySelectorAll("form")) {
126+
form.submit();
127+
}
128+
}
129+
};
118130
</script>
119131
</body>
120132
</html>

src/emulator/emulator-common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export function generateNextChunkUrl(
145145
}
146146

147147
export type EmulatorWorkerConfig = EmulatorDef & {
148+
workerId: string;
148149
wasm: ArrayBuffer;
149150
disks: EmulatorChunkedFileSpec[];
150151
delayedDisks?: EmulatorChunkedFileSpec[];

src/emulator/emulator-service-worker.ts

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import {
77

88
declare const self: ServiceWorkerGlobalScope;
99

10-
let workerCommands: EmulatorFallbackCommand[] = [];
11-
let isPaused = false;
12-
let onUnpauseCallbacks: ((response: Response) => void)[] = [];
10+
const workerInfoById = new Map<string, WorkerInfo>();
11+
12+
class WorkerInfo {
13+
public commands: EmulatorFallbackCommand[] = [];
14+
public isPaused = false;
15+
public onUnpauseCallbacks: ((response: Response) => void)[] = [];
16+
}
1317

1418
function constructJsonResponse(json: any) {
1519
return new Response(JSON.stringify(json), {
@@ -28,23 +32,29 @@ self.addEventListener("message", event => {
2832
const {data} = event;
2933
if (data.type === "worker-command") {
3034
const command = data.command as EmulatorFallbackCommand;
35+
const workerId = data.workerId as string;
36+
let workerInfo = workerInfoById.get(workerId);
37+
if (!workerInfo) {
38+
workerInfo = new WorkerInfo();
39+
workerInfoById.set(workerId, workerInfo);
40+
}
3141
let shouldPush = true;
3242
if (command.type === "input") {
3343
if (command.event.type === "pause") {
34-
isPaused = true;
44+
workerInfo.isPaused = true;
3545
shouldPush = false;
3646
}
3747
if (command.event.type === "unpause") {
38-
isPaused = false;
48+
workerInfo.isPaused = false;
3949
shouldPush = false;
40-
for (const callback of onUnpauseCallbacks) {
41-
callback(prepareCommandsFetchResponse());
50+
for (const callback of workerInfo.onUnpauseCallbacks) {
51+
callback(prepareCommandsFetchResponse(workerId));
4252
}
43-
onUnpauseCallbacks = [];
53+
workerInfo.onUnpauseCallbacks = [];
4454
}
4555
}
4656
if (shouldPush) {
47-
workerCommands.push(data.command);
57+
workerInfo.commands.push(data.command);
4858
}
4959
} else if (data.type === "init-disk-cache") {
5060
const diskFileSpec = data.spec as EmulatorChunkedFileSpec;
@@ -78,9 +88,21 @@ self.addEventListener("message", event => {
7888
self.addEventListener("fetch", (event: FetchEvent) => {
7989
const requestUrl = new URL(event.request.url);
8090
if (requestUrl.pathname.endsWith("/worker-commands")) {
81-
handleWorkerCommands(event);
91+
const workerId = requestUrl.searchParams.get("worker-id");
92+
if (!workerId) {
93+
console.error("No worker-id provided");
94+
event.respondWith(constructJsonResponse([]));
95+
return;
96+
}
97+
handleWorkerCommands(event, workerId);
8298
} else if (requestUrl.pathname.endsWith("/worker-idlewait")) {
83-
handleIdleWait(event);
99+
const workerId = requestUrl.searchParams.get("worker-id");
100+
if (!workerId) {
101+
console.error("No worker-id provided");
102+
event.respondWith(constructJsonResponse({}));
103+
return;
104+
}
105+
handleIdleWait(event, workerId);
84106
} else if (
85107
diskCacheSpecs.some(spec =>
86108
decodeURIComponent(requestUrl.pathname).startsWith(spec.baseUrl)
@@ -90,12 +112,18 @@ self.addEventListener("fetch", (event: FetchEvent) => {
90112
}
91113
});
92114

93-
function handleWorkerCommands(event: FetchEvent) {
94-
if (isPaused) {
115+
function handleWorkerCommands(event: FetchEvent, workerId: string) {
116+
const workerInfo = workerInfoById.get(workerId);
117+
if (!workerInfo) {
118+
console.error("No worker info found for worker ID", workerId);
119+
event.respondWith(constructJsonResponse([]));
120+
return;
121+
}
122+
if (workerInfo.isPaused) {
95123
const unpausePromise = new Promise<Response>(resolve => {
96124
console.log("Emulator paused, waiting for input");
97125
const startTime = performance.now();
98-
onUnpauseCallbacks.push(response => {
126+
workerInfo.onUnpauseCallbacks.push(response => {
99127
console.log(
100128
"Emulator unpaused after",
101129
((performance.now() - startTime) / 1000).toFixed(1),
@@ -107,16 +135,33 @@ function handleWorkerCommands(event: FetchEvent) {
107135
event.respondWith(unpausePromise);
108136
return;
109137
}
110-
event.respondWith(prepareCommandsFetchResponse());
138+
event.respondWith(prepareCommandsFetchResponse(workerId));
111139
}
112140

113-
function prepareCommandsFetchResponse(): Response {
114-
const fetchResponse = constructJsonResponse(workerCommands);
115-
workerCommands = [];
141+
function prepareCommandsFetchResponse(workerId: string): Response {
142+
const workerInfo = workerInfoById.get(workerId);
143+
if (!workerInfo) {
144+
console.error(
145+
"No worker info found for worker ID, returning no commands",
146+
workerId
147+
);
148+
return constructJsonResponse([]);
149+
}
150+
const fetchResponse = constructJsonResponse(workerInfo.commands);
151+
workerInfo.commands = [];
116152
return fetchResponse;
117153
}
118154

119-
function handleIdleWait(event: FetchEvent) {
155+
function handleIdleWait(event: FetchEvent, workerId: string) {
156+
const workerInfo = workerInfoById.get(workerId);
157+
if (!workerInfo) {
158+
console.error(
159+
"No worker info found for worker ID, skipping idlewait",
160+
workerId
161+
);
162+
event.respondWith(constructJsonResponse({}));
163+
return;
164+
}
120165
const requestUrl = new URL(event.request.url);
121166
const timeout = parseInt(requestUrl.searchParams.get("timeout")!, 10);
122167
// Subtract 1ms since that's the overhead of a roundtrip with the
@@ -127,7 +172,10 @@ function handleIdleWait(event: FetchEvent) {
127172
const interval = self.setInterval(() => {
128173
// Interrupt idlewait if new commands (presumably input) has
129174
// come in.
130-
if (workerCommands.length || performance.now() >= endTime) {
175+
if (
176+
workerInfo.commands.length ||
177+
performance.now() >= endTime
178+
) {
131179
self.clearInterval(interval);
132180
resolve(constructJsonResponse({}));
133181
}

src/emulator/emulator-ui.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ export class Emulator {
156156
#delegate?: EmulatorDelegate;
157157
#worker: Worker;
158158
#workerTerminated: boolean = false;
159+
// Doesn't have to be secure, just unique enough for a few instances that
160+
// may be used concurrently.
161+
#workerId = Math.random().toString(16).slice(2);
159162

160163
#screenCanvasContext: CanvasRenderingContext2D;
161164
#screenImageData: ImageData;
@@ -208,6 +211,7 @@ export class Emulator {
208211
if (serviceWorkerAvailable) {
209212
this.#serviceWorker!.postMessage({
210213
type: "worker-command",
214+
workerId: this.#workerId,
211215
command,
212216
});
213217
} else {
@@ -443,6 +447,7 @@ export class Emulator {
443447
emulatorType,
444448
emulatorSubtype,
445449
} as EmulatorDef),
450+
workerId: this.#workerId,
446451
wasm,
447452
disks,
448453
delayedDisks,

src/emulator/emulator-worker.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ class EmulatorWorkerApi {
145145
let fallbackEndpoint: EmulatorFallbackEndpoint | undefined;
146146
function getFallbackEndpoint(): EmulatorFallbackEndpoint {
147147
if (!fallbackEndpoint) {
148-
fallbackEndpoint = new EmulatorFallbackEndpoint();
148+
fallbackEndpoint = new EmulatorFallbackEndpoint(
149+
config.workerId
150+
);
149151
}
150152
return fallbackEndpoint;
151153
}
@@ -556,10 +558,15 @@ class EmulatorWorkerApi {
556558
}
557559

558560
export class EmulatorFallbackEndpoint {
561+
#workerId: string;
559562
#commandQueue: EmulatorFallbackCommand[] = [];
560563
#fetchFailures = 0;
561564
#loggedMaxFetchFailures = false;
562565

566+
constructor(workerId: string) {
567+
this.#workerId = workerId;
568+
}
569+
563570
idleWait(timeout: number) {
564571
this.#fetchSync(`./worker-idlewait?timeout=${timeout}&t=${Date.now()}`);
565572
}
@@ -643,7 +650,7 @@ export class EmulatorFallbackEndpoint {
643650
return undefined;
644651
}
645652
const xhr = new XMLHttpRequest();
646-
xhr.open("GET", url, false);
653+
xhr.open("GET", url + `&worker-id=${this.#workerId}`, false);
647654
xhr.send(null);
648655
if (xhr.status !== 200) {
649656
console.warn(

0 commit comments

Comments
 (0)