mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
a3910e7aa9
* feat(cloudflare): add default pages router worker entry * feat(cloudflare): add unified worker entry * chore(web): use unified worker entry * chore(examples): use built-in workers-cache entry * chore(examples): keep hackernews on app router entry * chore(cloudflare): rename unified entry to fetch handler * chore(cloudflare): keep pages entry source helper test-local
240 lines
8.1 KiB
TypeScript
240 lines
8.1 KiB
TypeScript
/**
|
|
* Cloudflare Worker entry point for the vinext Benchmarks Dashboard.
|
|
*
|
|
* Handles two concerns:
|
|
* 1. API routes (/api/upload, /api/results) with D1 database access
|
|
* 2. All other routes delegated to the vinext RSC handler
|
|
*/
|
|
import handler from "vinext/server/fetch-handler";
|
|
|
|
interface Env {
|
|
DB: D1Database;
|
|
BENCHMARK_UPLOAD_TOKEN: string;
|
|
}
|
|
|
|
/** Map runner keys from the benchmark JSON to DB enum values. */
|
|
const RUNNER_MAP: Record<string, string> = {
|
|
nextjs: "nextjs",
|
|
vinext: "vinext",
|
|
};
|
|
|
|
export default {
|
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
const url = new URL(request.url);
|
|
|
|
// ─── API: Upload benchmark results ─────────────────────────────────
|
|
if (url.pathname === "/api/upload" && request.method === "POST") {
|
|
return handleUpload(request, env);
|
|
}
|
|
|
|
// ─── API: Query benchmark results ──────────────────────────────────
|
|
if (url.pathname === "/api/results" && request.method === "GET") {
|
|
return handleResults(url, env);
|
|
}
|
|
|
|
// ─── API: Get single commit detail ─────────────────────────────────
|
|
if (url.pathname.startsWith("/api/commit/") && request.method === "GET") {
|
|
const sha = url.pathname.replace("/api/commit/", "");
|
|
return handleCommitDetail(sha, env);
|
|
}
|
|
|
|
// ─── All other routes: delegate to vinext RSC handler ────────────────
|
|
return handler.fetch(request);
|
|
},
|
|
};
|
|
|
|
// ─── Upload handler ──────────────────────────────────────────────────────────
|
|
|
|
async function handleUpload(request: Request, env: Env): Promise<Response> {
|
|
// Authenticate with bearer token
|
|
const auth = request.headers.get("Authorization");
|
|
if (!auth || auth !== `Bearer ${env.BENCHMARK_UPLOAD_TOKEN}`) {
|
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
let body: any;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
}
|
|
|
|
// Extract commit info from the request (CI provides these)
|
|
const commitSha = body.commitSha;
|
|
const commitShort = body.commitShort || commitSha?.slice(0, 7);
|
|
const commitMessage = body.commitMessage || "";
|
|
const commitDate = body.commitDate || new Date().toISOString();
|
|
const results = body.results;
|
|
|
|
if (!commitSha || !results) {
|
|
return Response.json(
|
|
{ error: "Missing required fields: commitSha, results" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Insert one row per runner, batched in a single transaction
|
|
const inserted: string[] = [];
|
|
const stmts: D1PreparedStatement[] = [];
|
|
|
|
for (const [key, dbRunner] of Object.entries(RUNNER_MAP)) {
|
|
const data = results[key];
|
|
if (!data || Object.keys(data).length === 0) continue;
|
|
|
|
stmts.push(
|
|
env.DB.prepare(`
|
|
INSERT INTO benchmark_results (
|
|
commit_sha, commit_short, commit_message, commit_date,
|
|
run_date, runner,
|
|
build_time_mean, build_time_stddev, build_time_min, build_time_max,
|
|
bundle_size_raw, bundle_size_gzip, bundle_file_count,
|
|
dev_cold_start_ms, dev_peak_rss_kb,
|
|
ssr_rps, ssr_ttfb_ms,
|
|
platform, arch, node_version, cpu_count, run_count
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).bind(
|
|
commitSha,
|
|
commitShort,
|
|
commitMessage,
|
|
commitDate,
|
|
results.timestamp || new Date().toISOString(),
|
|
dbRunner,
|
|
data.buildTime?.mean ?? null,
|
|
data.buildTime?.stddev ?? null,
|
|
data.buildTime?.min ?? null,
|
|
data.buildTime?.max ?? null,
|
|
data.bundleSize?.raw ?? null,
|
|
data.bundleSize?.gzip ?? null,
|
|
data.bundleSize?.files ?? null,
|
|
data.devColdStart?.meanMs ?? null,
|
|
data.devColdStart?.meanRssKb ?? null,
|
|
null, // ssr_rps (future)
|
|
null, // ssr_ttfb_ms (future)
|
|
results.system?.platform ?? null,
|
|
results.system?.arch ?? null,
|
|
results.system?.nodeVersion ?? null,
|
|
results.system?.cpus ?? null,
|
|
results.buildRuns ?? results.runs ?? null,
|
|
),
|
|
);
|
|
inserted.push(dbRunner);
|
|
}
|
|
|
|
if (stmts.length > 0) {
|
|
try {
|
|
await env.DB.batch(stmts);
|
|
} catch (err) {
|
|
console.error("DB.batch failed:", err);
|
|
return Response.json(
|
|
{ error: "Failed to insert benchmark results" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
return Response.json({ ok: true, inserted }, { status: 201 });
|
|
}
|
|
|
|
// ─── Results handler ─────────────────────────────────────────────────────────
|
|
|
|
async function handleResults(url: URL, env: Env): Promise<Response> {
|
|
const parsedLimit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
const limit = Number.isNaN(parsedLimit) ? 50 : Math.max(1, Math.min(parsedLimit, 200));
|
|
const runner = url.searchParams.get("runner"); // optional filter
|
|
|
|
let query = `
|
|
SELECT * FROM benchmark_results
|
|
${runner ? "WHERE runner = ?" : ""}
|
|
ORDER BY commit_date DESC, runner ASC
|
|
LIMIT ?
|
|
`;
|
|
const params: any[] = runner ? [runner, limit] : [limit];
|
|
|
|
const { results } = await env.DB.prepare(query).bind(...params).all();
|
|
|
|
// Group by commit for easier consumption
|
|
const grouped = groupByCommit(results as any[]);
|
|
|
|
return Response.json(grouped, {
|
|
headers: {
|
|
"Cache-Control": "public, max-age=60",
|
|
"Access-Control-Allow-Origin": "*",
|
|
},
|
|
});
|
|
}
|
|
|
|
// ─── Commit detail handler ───────────────────────────────────────────────────
|
|
|
|
async function handleCommitDetail(sha: string, env: Env): Promise<Response> {
|
|
const { results } = await env.DB.prepare(`
|
|
SELECT * FROM benchmark_results
|
|
WHERE commit_sha = ? OR commit_short = ?
|
|
ORDER BY runner ASC
|
|
`).bind(sha, sha).all();
|
|
|
|
if (!results || results.length === 0) {
|
|
return Response.json({ error: "Commit not found" }, { status: 404 });
|
|
}
|
|
|
|
return Response.json(results, {
|
|
headers: {
|
|
"Cache-Control": "public, max-age=300",
|
|
"Access-Control-Allow-Origin": "*",
|
|
},
|
|
});
|
|
}
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
interface BenchmarkRow {
|
|
commit_sha: string;
|
|
commit_short: string;
|
|
commit_message: string;
|
|
commit_date: string;
|
|
run_date: string;
|
|
runner: string;
|
|
build_time_mean: number | null;
|
|
bundle_size_gzip: number | null;
|
|
dev_cold_start_ms: number | null;
|
|
dev_peak_rss_kb: number | null;
|
|
[key: string]: any;
|
|
}
|
|
|
|
function groupByCommit(rows: BenchmarkRow[]) {
|
|
const map = new Map<string, any>();
|
|
|
|
for (const row of rows) {
|
|
if (!map.has(row.commit_sha)) {
|
|
map.set(row.commit_sha, {
|
|
commitSha: row.commit_sha,
|
|
commitShort: row.commit_short,
|
|
commitMessage: row.commit_message,
|
|
commitDate: row.commit_date,
|
|
runDate: row.run_date,
|
|
runners: {},
|
|
});
|
|
}
|
|
const entry = map.get(row.commit_sha)!;
|
|
entry.runners[row.runner] = {
|
|
buildTimeMean: row.build_time_mean,
|
|
buildTimeStddev: row.build_time_stddev,
|
|
buildTimeMin: row.build_time_min,
|
|
buildTimeMax: row.build_time_max,
|
|
bundleSizeRaw: row.bundle_size_raw,
|
|
bundleSizeGzip: row.bundle_size_gzip,
|
|
bundleFileCount: row.bundle_file_count,
|
|
devColdStartMs: row.dev_cold_start_ms,
|
|
devPeakRssKb: row.dev_peak_rss_kb,
|
|
ssrRps: row.ssr_rps,
|
|
ssrTtfbMs: row.ssr_ttfb_ms,
|
|
platform: row.platform,
|
|
arch: row.arch,
|
|
nodeVersion: row.node_version,
|
|
cpuCount: row.cpu_count,
|
|
runCount: row.run_count,
|
|
};
|
|
}
|
|
|
|
return Array.from(map.values());
|
|
}
|