#!/usr/bin/env -S deno run --quiet --allow-net --allow-ffi --allow-read --allow-write --allow-env --allow-run --allow-sys /** * Run one `cf` invocation in this process and write what it measured. * * The launcher's own flag set plus `--allow-sys`, which the in-process V8 * profiler needs — the reason this exists rather than `cf` itself: a * `.cpuprofile` of a CLI command has to come from inside the command's * process, and the launcher does not grant that. * * CF_PROF_OUT=/tmp/run CF_PROF_CPU=1 CF_TIMING_MEASURES=1 \ * deno run --quiet --config deno.jsonc --allow-net --allow-ffi \ * --allow-read --allow-write --allow-env --allow-run --allow-sys \ * skills/perf-investigation/scripts/profile-cf.ts cell get
... * * Writes, under the `CF_PROF_OUT` prefix: `.timing.json` and `.counts.json` * (the logger's statistics and counts for the whole run), `.flags.json`, * `.measures.json` when `CF_TIMING_MEASURES=1` (the input the other scripts * here read), and with `CF_PROF_CPU=1` a `.cpuprofile` that Chrome DevTools * and speedscope load beside a `.report.txt` ranked by self time. * `CF_PROF_INTERVAL_US` sets the sampling period (500 unless set). Every * other variable `cf` reads — `CF_API_URL`, `CF_IDENTITY`, `CF_SPACE`, * `CF_CLI_TRACE_TIMINGS`, `CF_MEMORY_FRAME_LOG` — applies as it would to * `cf`. Run from the repository root. */ // deno-lint-ignore no-external-import import { Session } from "node:inspector"; import { main } from "../../../packages/cli/mod.ts"; import { getLoggerCountsBreakdown, getLoggerFlagsBreakdown, getTimingStatsBreakdown, TIMING_MEASURE_PREFIX, } from "@commonfabric/utils/logger"; import { renderProfileReport } from "../../../packages/integration/cdp-profiler.ts"; const out = Deno.env.get("CF_PROF_OUT") ?? "cf-prof"; const cpu = Deno.env.get("CF_PROF_CPU") === "1"; const interval = Number(Deno.env.get("CF_PROF_INTERVAL_US") ?? "500"); Deno.env.set("CF_CLI_NAME", Deno.env.get("CF_CLI_NAME") ?? "cf"); let session: Session | undefined; const post = (method: string, params?: unknown) => new Promise((resolve, reject) => session!.post( method, params as never, (error: unknown, result: unknown) => error ? reject(error) : resolve(result), ) ); const started = performance.now(); if (cpu) { session = new Session(); session.connect(); await post("Profiler.enable"); await post("Profiler.setSamplingInterval", { interval }); await post("Profiler.start"); } let finalized = false; // Captured before anything can replace it below. const realExit = Deno.exit; async function finalize(code: number): Promise { if (finalized) return; finalized = true; const wall = performance.now() - started; try { if (cpu) { const { profile } = await post("Profiler.stop") as { profile: Parameters[0]; }; await Deno.writeTextFile(`${out}.cpuprofile`, JSON.stringify(profile)); await Deno.writeTextFile( `${out}.report.txt`, renderProfileReport(profile, out, { topFrames: 60, topFiles: 25 }), ); } await Deno.writeTextFile( `${out}.timing.json`, JSON.stringify(getTimingStatsBreakdown()), ); await Deno.writeTextFile( `${out}.counts.json`, JSON.stringify(getLoggerCountsBreakdown()), ); await Deno.writeTextFile( `${out}.flags.json`, JSON.stringify(getLoggerFlagsBreakdown()), ); const measures: { name: string; startTime: number; duration: number }[] = []; // Read and left on the timeline: the process exits below, so nothing // reads the entries again and nothing more is emitted against their budget. for (const entry of performance.getEntriesByType("measure")) { if (entry.name.startsWith(TIMING_MEASURE_PREFIX)) { measures.push({ name: entry.name, startTime: entry.startTime, duration: entry.duration, }); } } if (measures.length > 0) { await Deno.writeTextFile( `${out}.measures.json`, JSON.stringify(measures), ); } console.error( `[profile-cf] wall ${wall.toFixed(0)}ms, exit ${code}, ` + `measures ${measures.length}, wrote ${out}.*`, ); } catch (error) { console.error( `[profile-cf] writing the artifacts failed: ${ error instanceof Error ? error.message : String(error) }`, ); } finally { // The command's own code, whatever became of the artifacts. realExit(code); } } // A command may leave through `Deno.exit` directly rather than through the // exit `main` is handed. Both routes end in `finalize`, which performs the // real exit once the artifacts are flushed. Until then the caller must not // continue past its exit — `Deno.exit` never returns, and code after it // assumes as much — so the replacement throws a sentinel that unwinds the // caller and is swallowed wherever it surfaces. class ExitRequested extends Error { constructor(readonly code: number) { super(`[profile-cf] exit ${code} requested; writing artifacts`); this.name = "ExitRequested"; } } const isExitRequested = (reason: unknown): boolean => reason instanceof ExitRequested; globalThis.addEventListener("unhandledrejection", (event) => { if (isExitRequested(event.reason)) event.preventDefault(); }); globalThis.addEventListener("error", (event) => { if (isExitRequested(event.error)) event.preventDefault(); }); Deno.exit = ((code?: number): never => { const exitCode = code ?? Deno.exitCode ?? 0; void finalize(exitCode); throw new ExitRequested(exitCode); }) as typeof Deno.exit; try { await main(Deno.args, { exit: (code) => { void finalize(code); }, }); } catch (error) { if (!isExitRequested(error)) throw error; }