Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
31
infra/perf-monitor/probe/src/index.ts
Normal file
31
infra/perf-monitor/probe/src/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Perf probe Worker -- deployed per-region with placement hints.
|
||||
* Receives measurement requests via service binding fetch(),
|
||||
* runs the measurements from its placed location, returns results.
|
||||
*/
|
||||
|
||||
import { measureRoutes } from "./measure.js";
|
||||
import type { MeasureRequest, MeasureResponse } from "./measure.js";
|
||||
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
if (request.method !== "POST") {
|
||||
return new Response("Method not allowed", { status: 405 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json<MeasureRequest & { region?: string }>();
|
||||
const results = await measureRoutes(body);
|
||||
|
||||
const response: MeasureResponse = {
|
||||
results,
|
||||
probeRegion: body.region ?? "unknown",
|
||||
};
|
||||
|
||||
return Response.json(response);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
return Response.json({ error: message }, { status: 500 });
|
||||
}
|
||||
},
|
||||
} satisfies ExportedHandler;
|
||||
212
infra/perf-monitor/probe/src/measure.ts
Normal file
212
infra/perf-monitor/probe/src/measure.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/** Measurement logic -- runs inside the placed probe Worker. */
|
||||
|
||||
export interface MeasureRequest {
|
||||
targetUrl: string;
|
||||
routes: Array<{ path: string; label: string }>;
|
||||
warmRequests: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed Server-Timing header. Keyed by timing name. `desc` is optional.
|
||||
* Example: { render: { dur: 42, desc: "Page render" }, mw: { dur: 58 } }
|
||||
*/
|
||||
export type ServerTimings = Record<string, { dur: number; desc?: string }>;
|
||||
|
||||
export interface RouteResult {
|
||||
path: string;
|
||||
label: string;
|
||||
coldTtfbMs: number;
|
||||
/**
|
||||
* Median warm-request TTFB. Null if warmRequests was 0 and no warm
|
||||
* samples were taken — caller should fall back to coldTtfbMs in that case.
|
||||
*/
|
||||
warmTtfbMs: number | null;
|
||||
/** p95 warm-request TTFB. Null when no warm samples were taken. */
|
||||
p95TtfbMs: number | null;
|
||||
statusCode: number;
|
||||
cfColo: string | null;
|
||||
cfPlacement: string | null;
|
||||
/** Parsed from the cold response. Null if header absent or unparseable. */
|
||||
coldServerTimings: ServerTimings | null;
|
||||
/**
|
||||
* Median of each Server-Timing metric across all warm requests.
|
||||
* Null if no warm responses carried the header or no warm requests
|
||||
* were issued. Use this to isolate steady-state render/middleware/
|
||||
* runtime cost, independent of cold-start.
|
||||
*/
|
||||
warmServerTimings: ServerTimings | null;
|
||||
}
|
||||
|
||||
export interface MeasureResponse {
|
||||
results: RouteResult[];
|
||||
probeRegion: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the Server-Timing response header.
|
||||
*
|
||||
* Grammar (RFC 8673 §2):
|
||||
* Server-Timing: metric[;param]*[, metric[;param]*]*
|
||||
* param = dur=<number> | desc="<string>" | desc=<token>
|
||||
*
|
||||
* We only extract `dur` and `desc` and silently skip malformed entries.
|
||||
* Unknown params are ignored rather than rejected so future additions
|
||||
* upstream don't cause us to drop data.
|
||||
*/
|
||||
export function parseServerTiming(header: string | null): ServerTimings | null {
|
||||
if (!header) return null;
|
||||
const out: ServerTimings = {};
|
||||
for (const rawEntry of header.split(",")) {
|
||||
const parts = rawEntry.split(";").map((p) => p.trim());
|
||||
const name = parts[0];
|
||||
if (!name) continue;
|
||||
const entry: { dur: number; desc?: string } = { dur: 0 };
|
||||
let sawDur = false;
|
||||
for (const param of parts.slice(1)) {
|
||||
const eq = param.indexOf("=");
|
||||
if (eq === -1) continue;
|
||||
const key = param.slice(0, eq).trim();
|
||||
let value = param.slice(eq + 1).trim();
|
||||
// desc may be quoted
|
||||
if (value.startsWith('"') && value.endsWith('"')) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (key === "dur") {
|
||||
const n = Number(value);
|
||||
if (Number.isFinite(n)) {
|
||||
entry.dur = n;
|
||||
sawDur = true;
|
||||
}
|
||||
} else if (key === "desc") {
|
||||
entry.desc = value;
|
||||
}
|
||||
}
|
||||
if (sawDur) out[name] = entry;
|
||||
}
|
||||
return Object.keys(out).length > 0 ? out : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure TTFB for a single URL.
|
||||
* Returns wall-clock time from fetch start to first byte (headers received).
|
||||
*/
|
||||
async function measureTtfb(url: string): Promise<{
|
||||
ttfbMs: number;
|
||||
statusCode: number;
|
||||
cfColo: string | null;
|
||||
cfPlacement: string | null;
|
||||
serverTimings: ServerTimings | null;
|
||||
}> {
|
||||
const start = performance.now();
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"User-Agent": "emdash-perf-probe/1.0",
|
||||
// Bust any edge cache
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
redirect: "follow",
|
||||
});
|
||||
const ttfbMs = performance.now() - start;
|
||||
|
||||
// Consume the body so the connection is properly released
|
||||
await response.arrayBuffer();
|
||||
|
||||
// Extract cf-ray colo: format is "<ray-id>-<COLO>"
|
||||
const cfRay = response.headers.get("cf-ray");
|
||||
const cfColo = cfRay?.split("-").pop() ?? null;
|
||||
const cfPlacement = response.headers.get("cf-placement");
|
||||
const serverTimings = parseServerTiming(response.headers.get("server-timing"));
|
||||
|
||||
return { ttfbMs, statusCode: response.status, cfColo, cfPlacement, serverTimings };
|
||||
}
|
||||
|
||||
/** Compute the median of an array. */
|
||||
function median(values: number[]): number {
|
||||
const sorted = values.toSorted((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
if (sorted.length % 2 === 0) {
|
||||
return (sorted[mid - 1]! + sorted[mid]!) / 2;
|
||||
}
|
||||
return sorted[mid]!;
|
||||
}
|
||||
|
||||
/** Compute p95 of an array. */
|
||||
function p95(values: number[]): number {
|
||||
const sorted = values.toSorted((a, b) => a - b);
|
||||
const idx = Math.ceil(sorted.length * 0.95) - 1;
|
||||
return sorted[Math.max(0, idx)]!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run measurements for all routes.
|
||||
* For each route: 1 cold request (cache-busted with unique query param),
|
||||
* then N warm requests. Returns structured results.
|
||||
*/
|
||||
export async function measureRoutes(req: MeasureRequest): Promise<RouteResult[]> {
|
||||
const results: RouteResult[] = [];
|
||||
|
||||
for (const route of req.routes) {
|
||||
const url = `${req.targetUrl}${route.path}`;
|
||||
|
||||
// Cold request -- add a unique query param to avoid any isolate reuse
|
||||
const coldUrl = url + (url.includes("?") ? "&" : "?") + `_perf_cold=${Date.now()}`;
|
||||
const cold = await measureTtfb(coldUrl);
|
||||
|
||||
// Warm requests — keep per-metric samples so we can median each one.
|
||||
const warmTimings: number[] = [];
|
||||
const warmMetricSamples: Record<string, { durs: number[]; desc?: string }> = {};
|
||||
let lastStatusCode = cold.statusCode;
|
||||
for (let i = 0; i < req.warmRequests; i++) {
|
||||
const warm = await measureTtfb(url);
|
||||
warmTimings.push(warm.ttfbMs);
|
||||
lastStatusCode = warm.statusCode;
|
||||
if (warm.serverTimings) {
|
||||
for (const [name, entry] of Object.entries(warm.serverTimings)) {
|
||||
const acc = warmMetricSamples[name] ?? { durs: [], desc: entry.desc };
|
||||
acc.durs.push(entry.dur);
|
||||
if (!acc.desc && entry.desc) acc.desc = entry.desc;
|
||||
warmMetricSamples[name] = acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collapse per-metric samples into medians so the stored shape
|
||||
// mirrors coldServerTimings.
|
||||
const warmServerTimings: ServerTimings | null = Object.keys(warmMetricSamples).length
|
||||
? Object.fromEntries(
|
||||
Object.entries(warmMetricSamples).map(([name, { durs, desc }]) => {
|
||||
const entry: { dur: number; desc?: string } = {
|
||||
dur: Math.round(median(durs) * 100) / 100,
|
||||
};
|
||||
if (desc) entry.desc = desc;
|
||||
return [name, entry];
|
||||
}),
|
||||
)
|
||||
: null;
|
||||
|
||||
// Handle the (uncommon) warmRequests=0 case: without warm samples,
|
||||
// median/p95 would compute against an empty array and produce NaN.
|
||||
// Report the cold TTFB in both slots so the row remains valid;
|
||||
// warm timings are reported as null so downstream code knows there's
|
||||
// no warm breakdown to render.
|
||||
const hasWarm = warmTimings.length > 0;
|
||||
const warmTtfbMs = hasWarm ? Math.round(median(warmTimings) * 100) / 100 : null;
|
||||
const p95TtfbMs = hasWarm ? Math.round(p95(warmTimings) * 100) / 100 : null;
|
||||
|
||||
results.push({
|
||||
path: route.path,
|
||||
label: route.label,
|
||||
coldTtfbMs: Math.round(cold.ttfbMs * 100) / 100,
|
||||
warmTtfbMs,
|
||||
p95TtfbMs,
|
||||
statusCode: lastStatusCode,
|
||||
cfColo: cold.cfColo,
|
||||
cfPlacement: cold.cfPlacement,
|
||||
coldServerTimings: cold.serverTimings,
|
||||
warmServerTimings,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user