History

Pre-warm the app plane on requests that do not need it (#2072)

Rhys Sullivan committed 1 month ago

Commit 1aa6fd91 parent c6d10831 file · +64 −10Browse files

Unified · Split

apps/cloud/src/server.ts+64−10
@@ -243,19 +243,64 @@ const markStartGraphEntered = (): void => { · 242 unmodified lines
243// `servedByAppPlane` (./app-paths) decides which paths qualify — two under243// `servedByAppPlane` (./app-paths) decides which paths qualify — two under
244// `/api` are claimed by Start's middleware first and must keep their old route.244// `/api` are claimed by Start's middleware first and must keep their old route.
245 245
246// Instantiated on the first request that needs it and memoized per isolate,246// Instantiated once per isolate and memoized as a promise, mirroring
247// mirroring `start.ts`'s `getApp`. The import stays dynamic so an isolate that247// `start.ts`'s `getApp`. The import stays dynamic so the Worker's static
248// only serves pages or proxies never evaluates the app graph at all.248// startup closure does not include the app graph; the promise memo means a
249let appPlane: ReturnType<typeof import("./app").cloudApiHandler> | undefined;249// pre-warm and a real request racing on a fresh isolate share one import.
250type AppPlane = ReturnType<typeof import("./app").cloudApiHandler>;
251let appPlanePromise: Promise<AppPlane> | undefined;
250let appGraphEntered = false;252let appGraphEntered = false;
251 253
252const getAppPlane = async (): Promise<NonNullable<typeof appPlane>> => {254const getAppPlane = (): Promise<AppPlane> => {
253 if (appPlane === undefined) {255 if (appPlanePromise === undefined) {
254 const { cloudApiHandler } = await import("./app");256 appPlanePromise = import("./app").then(
255 appPlane = cloudApiHandler();257 ({ cloudApiHandler }) => {
256 appGraphEntered = true;258 const plane = cloudApiHandler();
259 appGraphEntered = true;
260 return plane;
261 },
262 (cause: unknown) => {
263 // Do not memoize a failure: the next request re-imports, as the
264 // un-memoized version did, instead of failing every request after.
265 appPlanePromise = undefined;
266 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: re-raise the import failure to the awaiting request
267 throw cause;
268 },
269 );
257 }270 }
258 return appPlane;271 return appPlanePromise;
272};
273
274// ---------------------------------------------------------------------------
275// Pre-warming the app plane.
276// ---------------------------------------------------------------------------
277//
278// Measured on production 2026-09-18: 30% of `/api/*` dispatches landed on an
279// isolate that had not yet evaluated the app graph, and paid ~2s (p50) for it
280// against ~100ms warm. Only 43% of those isolates were under 5s old. The rest
281// had been alive for seconds to minutes serving `/mcp`, discovery documents,
282// or proxies, none of which enter the app graph, so the dashboard's first
283// call was the one that paid. Isolates live about a minute at the median, so
284// there is rarely a second dashboard request to benefit.
285//
286// So any request that does NOT need the app plane starts its import in the
287// background. The request itself returns as before; the import runs under
288// `waitUntil`, so the isolate stays up until it finishes. A dashboard call
289// arriving afterwards finds the graph evaluated. The truly fresh isolate
290// (first request IS a dashboard call) still pays; that cost is the graph's
291// evaluation itself, addressed separately.
292//
293// Failure is swallowed on purpose: a pre-warm that fails must not fail the
294// request that triggered it, and the next real app-plane request re-imports
295// through the same memo and surfaces the error where it belongs.
296const prewarmAppPlane = (ctx: ExecutionContext): void => {
297 if (appGraphEntered) return;
298 ctx.waitUntil(
299 getAppPlane().then(
300 () => undefined,
301 () => undefined,
302 ),
303 );
259};304};
260 305
261const cloudflareHandler: ExportedHandler<Env> = {306const cloudflareHandler: ExportedHandler<Env> = {
@@ -266,6 +311,12 @@ const cloudflareHandler: ExportedHandler<Env> = { · 4 unmodified lines
266 // import loads the entire React + Effect server graph and can take seconds311 // import loads the entire React + Effect server graph and can take seconds
267 // on a cold isolate. Classify and service-bind marketing at the Worker312 // on a cold isolate. Classify and service-bind marketing at the Worker
268 // entry, before telemetry or fetchHandler touches that graph.313 // entry, before telemetry or fetchHandler touches that graph.
314 // Everything that returns before the app-plane dispatch below leaves the
315 // graph unevaluated for the next request; warm it in the background.
316 if (!servedByAppPlane(new URL(request.url).pathname, request.method)) {
317 prewarmAppPlane(ctx);
318 }
319
269 const marketingRequest = marketingProxyRequest(request);320 const marketingRequest = marketingProxyRequest(request);
270 const marketing: Fetcher | undefined = env.MARKETING;321 const marketing: Fetcher | undefined = env.MARKETING;
271 if (marketingRequest && marketing) return marketing.fetch(marketingRequest);322 if (marketingRequest && marketing) return marketing.fetch(marketingRequest);
@@ -445,6 +496,9 @@ const cloudflareHandler: ExportedHandler<Env> = { · 173 unmodified lines
445 // isolate goes idle.496 // isolate goes idle.
446 scheduled: async (_controller, _env, ctx) => {497 scheduled: async (_controller, _env, ctx) => {
447 installTracerProvider();498 installTracerProvider();
499 // The cron fires every minute, often on an isolate that has served no
500 // dashboard request yet: the cheapest pre-warm there is.
501 prewarmAppPlane(ctx);
448 await runWorkOsEventsSync();502 await runWorkOsEventsSync();
449 ctx.waitUntil(flushTracerProvider());503 ctx.waitUntil(flushTracerProvider());
450 },504 },