paperclip/scripts/benchmark-skill-preparation.ts

116 lines
6.8 KiB
TypeScript

/** Isolated, repeatable preparation benchmark. Run with pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-skill-preparation.ts. */
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { companies, companySkills, createDb, startEmbeddedPostgresTestDatabase } from "../packages/db/src/index.js";
import { removeRuntimeSkillCache } from "../server/src/services/runtime-skill-cache.js";
import { companySkillService } from "../server/src/services/company-skills.js";
const child = process.argv.includes("--warm-child");
const home = child ? process.env.PAPERCLIP_HOME! : await fs.mkdtemp(path.join(os.tmpdir(), "skill-preparation-benchmark-"));
process.env.PAPERCLIP_HOME = home;
process.env.PAPERCLIP_INSTANCE_ID = "default";
const database = child ? null : await startEmbeddedPostgresTestDatabase("skill-preparation-benchmark-");
const db = createDb(database?.connectionString ?? process.env.SKILL_BENCH_DATABASE_URL!);
const companyId = child ? process.env.SKILL_BENCH_COMPANY_ID! : randomUUID();
const svc = companySkillService(db);
let upstreamFetches = 0;
let inventoryRefreshes = 0;
let rebuilds = 0;
const originalFetch = globalThis.fetch;
globalThis.fetch = async (input) => { upstreamFetches++; return new Response(`# Fixture\n${String(input)}\n`); };
const originalSelect = db.select.bind(db);
db.select = ((fields?: Record<string, unknown>) => {
if (fields && Object.keys(fields).length === 1 && fields.id === companies.id) inventoryRefreshes++;
return originalSelect(fields as never);
}) as typeof db.select;
const originalMkdtemp = fs.mkdtemp;
fs.mkdtemp = ((prefix: string, ...args: unknown[]) => {
if (String(prefix).includes("__runtime_cache_v1__") && String(prefix).includes(".staging-")) rebuilds++;
return Reflect.apply(originalMkdtemp, fs, [prefix, ...args]);
}) as typeof fs.mkdtemp;
async function measure(label: string) {
upstreamFetches = inventoryRefreshes = rebuilds = 0;
const start = performance.now();
const entries = await svc.listRuntimeSkillEntries(companyId);
const durationMs = performance.now() - start;
const missingEntries = entries.filter((entry) => entry.sourceStatus === "missing").length;
assert.equal(entries.length, 114);
assert.equal(missingEntries, 0);
assert.equal(inventoryRefreshes, 1);
if (label !== "cold") { assert.equal(upstreamFetches, 0); assert.equal(rebuilds, 0); }
// Check every synthetic supporting file against its installed revision, outside the timed section.
const installed = await db.select().from(companySkills);
for (const skill of installed.filter((skill) => skill.sourceType === "github" || skill.sourceType === "skills_sh")) {
const entry = entries.find((entry) => entry.key === skill.key)!;
for (const file of skill.fileInventory) {
const expected = `# Fixture\nhttps://raw.githubusercontent.com/fixture/skills/${skill.sourceRef}/${skill.slug}/${file.path}\n`;
assert.equal(await fs.readFile(path.join(entry.source, file.path), "utf8"), expected);
}
}
return { label, durationMs: Math.round(durationMs * 100) / 100, inventoryRefreshes, upstreamFetches, rebuilds, missingEntries, entries: entries.length };
}
try {
if (child) {
console.log(JSON.stringify(await measure("warm-new-process")));
} else {
await db.insert(companies).values({ id: companyId, name: "Skill benchmark", issuePrefix: "BENCH" });
const bundled = await svc.listFull(companyId);
const additional = 114 - bundled.length;
assert.ok(additional >= 6, "Bundled inventory leaves insufficient room for mixed benchmark fixtures");
const remoteCount = 33;
const catalogCount = 21;
assert.ok(additional > remoteCount + catalogCount, "Bundled inventory leaves no room for local fixtures");
const filesPerRemote = Math.max(9, Math.ceil(400 / remoteCount));
for (let index = 0; index < additional; index++) {
const slug = `fixture-${index}`;
const sourceType = index < remoteCount ? (index < 28 ? "github" : "skills_sh")
: index === remoteCount ? "url" : index <= remoteCount + catalogCount ? "catalog" : "local_path";
const localDir = path.join(home, "instances", "default", "skills", companyId, slug);
if (sourceType === "local_path" || sourceType === "catalog") {
await fs.mkdir(localDir, { recursive: true });
await fs.writeFile(path.join(localDir, "SKILL.md"), `---\nname: ${slug}\ndescription: Benchmark\n---\n# Local\n`);
}
await db.insert(companySkills).values({
id: randomUUID(), companyId, key: `company/${companyId}/${slug}`, slug, name: slug,
markdown: `# ${slug}`, sourceType, sourceLocator: sourceType === "local_path" || sourceType === "catalog" ? localDir : `https://example.com/${slug}`,
sourceRef: index < remoteCount ? "a".repeat(40) : null,
trustLevel: "markdown_only", compatibility: "compatible",
metadata: { owner: "fixture", repo: "skills", repoSkillDir: slug },
fileInventory: [{ path: "SKILL.md", kind: "skill" }, ...Array.from({ length: index < remoteCount ? filesPerRemote - 1 : 0 }, (_, file) => ({ path: `references/${file}.md`, kind: "reference" as const }))],
});
}
const samples = [await measure("cold")];
for (let index = 0; index < 9; index++) samples.push(await measure(`warm-${index + 1}`));
const childOutput = await new Promise<string>((resolve, reject) => {
const proc = spawn(process.execPath, ["--import", fileURLToPath(new URL("../server/node_modules/tsx/dist/loader.mjs", import.meta.url)), fileURLToPath(import.meta.url), "--warm-child"], {
env: { ...process.env, SKILL_BENCH_DATABASE_URL: database!.connectionString, SKILL_BENCH_COMPANY_ID: companyId },
stdio: ["ignore", "pipe", "inherit"],
});
let output = "";
proc.stdout.on("data", (chunk) => { output += chunk; });
proc.on("error", reject);
proc.on("exit", (code) => code === 0 ? resolve(output) : reject(new Error(`Warm subprocess failed: ${code}`)));
});
samples.push(JSON.parse(childOutput.trim().split("\n").at(-1)!));
const warm = samples.slice(1).map((sample) => sample.durationMs).sort((a, b) => a - b);
console.log(JSON.stringify({ inventory: 114, materializedRemoteFiles: remoteCount * filesPerRemote,
warmMedianMs: (warm[4] + warm[5]) / 2, warmMaxMs: warm.at(-1), samples }, null, 2));
}
} finally {
globalThis.fetch = originalFetch;
fs.mkdtemp = originalMkdtemp;
if (!child) for (const skill of await db.select().from(companySkills)) {
await removeRuntimeSkillCache(path.join(home, "instances", "default", "skills", companyId), skill.id);
}
await db.$client.end();
await database?.cleanup();
if (!child) await fs.rm(home, { recursive: true, force: true });
}