Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion ipc-runtime/ts/src/wasm/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,12 @@ export class WasmFfiBackend implements IpcClientAsync {
module,
options: {
threads: opts.threads,
memory: opts.memory,
memory: {
...opts.memory,
// The worker cannot tell an iPad from a Mac: its navigator has no maxTouchPoints.
maximum:
opts.memory?.maximum ?? bound.platform.maximumMemoryPages?.(),
},
env: opts.env,
entry: opts.entry,
},
Expand Down
12 changes: 7 additions & 5 deletions ipc-runtime/ts/src/wasm/browser/platform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { WasmPlatform, WorkerHandle, WorkerSide } from "../platform.js";
import {
type WasmPlatform,
type WorkerHandle,
type WorkerSide,
isIos,
} from "../platform.js";

/** The parent's handle on a browser `Worker`. */
export function browserWorkerHandle(worker: Worker): WorkerHandle {
Expand Down Expand Up @@ -26,10 +31,7 @@ export const browserPlatform: WasmPlatform = {
true,
// iOS browsers kill a page well before it reaches the 4 GiB a wasm memory may declare; asking
// for a 1 GiB maximum keeps the reservation within what they grant.
maximumMemoryPages: () =>
typeof navigator !== "undefined" && /iPad|iPhone/.test(navigator.userAgent)
? 2 ** 14
: 2 ** 16,
maximumMemoryPages: () => (isIos() ? 2 ** 14 : 2 ** 16),
};

/** The worker's side of a browser `Worker` channel. */
Expand Down
1 change: 1 addition & 0 deletions ipc-runtime/ts/src/wasm/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {

export type { WasmModuleSource } from "./module_source.js";
export type { WasmPlatform, WorkerHandle, WorkerSide } from "./platform.js";
export { isIos } from "./platform.js";
export type { WasmFfiBackendOptions, WasmFfiBinding, WasmFfiOptions };
export {
MAX_THREADS,
Expand Down
21 changes: 20 additions & 1 deletion ipc-runtime/ts/src/wasm/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,29 @@ export interface WasmPlatform extends ModuleLoadPlatform {
hardwareConcurrency(): number;
/** Threads need a shared memory; browsers only allow one under COOP/COEP. */
sharedMemoryAvailable(): boolean;
/** Upper bound on linear memory (64 KiB pages) to ask for when the caller sets none. */
/**
* Upper bound on linear memory (64 KiB pages) to ask for when the caller sets none. The backend
* reads it on the thread that creates it: in a worker, an iPad reads as a Mac (see `isIos`).
*/
maximumMemoryPages?(): number;
}

/**
* Whether this runs on an iPhone or an iPad. iPadOS Safari and an iPhone that requests the desktop
* website send a Mac user agent, and only their touch points tell them from a Mac. A worker has no
* `maxTouchPoints`, so there those two devices read as a Mac.
*/
export function isIos(): boolean {
if (typeof navigator === "undefined") {
return false;
}
const { userAgent, maxTouchPoints } = navigator;
return (
/iPad|iPhone/.test(userAgent) ||
(/Macintosh/.test(userAgent) && maxTouchPoints > 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check will identify the new MacBooks with touchscreens as iPads. Just pointing out in case we needed the extra memory on MacBooks.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm yeah good point. We'll have to see when they're released if/how we need to tweak this.

);
}

/** The worker's view of its parent (node `parentPort` or a browser worker global). */
export interface WorkerSide {
onMessage(handler: (message: any) => void): void;
Expand Down
60 changes: 60 additions & 0 deletions ipc-runtime/ts/src/wasm_platform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import { afterEach, test } from "node:test";
import { WasmFfiBackend } from "./wasm/backend.js";
import { browserPlatform } from "./wasm/browser/platform.js";
import type { WorkerHandle } from "./wasm/platform.js";

const IPHONE = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)";
const IPAD = "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X)";
const MAC = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)";

const realNavigator = Object.getOwnPropertyDescriptor(globalThis, "navigator")!;
afterEach(() => Object.defineProperty(globalThis, "navigator", realNavigator));

for (const [device, userAgent, maxTouchPoints, pages] of [
["an iPhone", IPHONE, 5, 2 ** 14],
["an iPad", IPAD, 5, 2 ** 14],
["a Mac user agent with touch points", MAC, 5, 2 ** 14],
["a Mac", MAC, 0, 2 ** 16],
["a worker (no touch points) on an iPad or a Mac", MAC, undefined, 2 ** 16],
] as const) {
test(`${device} gets a maximum of ${pages} memory pages`, () => {
Object.defineProperty(globalThis, "navigator", {
value: { userAgent, maxTouchPoints },
configurable: true,
});
assert.equal(browserPlatform.maximumMemoryPages!(), pages);
});
}

async function mainWorkerMaximum(callerMaximum?: number): Promise<number> {
let init: any;
const handlers: ((message: unknown) => void)[] = [];
const worker: WorkerHandle = {
postMessage: (message) => {
init = message;
queueMicrotask(() => handlers.forEach((h) => h({ type: "ready" })));
},
onMessage: (handler) => handlers.push(handler),
onError: () => {},
terminate: async () => {},
unref: () => {},
};
await WasmFfiBackend.create(
{
module: new Uint8Array([0x00, 0x61, 0x73, 0x6d, 1, 0, 0, 0]),
memory: { maximum: callerMaximum },
},
{
platform: { ...browserPlatform, maximumMemoryPages: () => 1234 },
createMainWorker: () => worker,
createThreadWorker: () => worker,
},
);
return init.options.memory.maximum;
}

test("the main worker gets the caller's memory maximum, else that of the thread that creates it", async () => {
assert.equal(await mainWorkerMaximum(99), 99);
assert.equal(await mainWorkerMaximum(), 1234);
});
Loading