Files
cfw-autumn/server/experiments/paginationBenchmark.ts
2026-05-14 10:19:12 +01:00

369 lines
9.6 KiB
TypeScript

import { AppEnv } from "@autumn/shared";
import chalk from "chalk";
import { sql, type SQL } from "drizzle-orm";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { RELEVANT_STATUSES } from "../src/internal/customers/cusProducts/CusProductService";
import { initDrizzle } from "../src/db/initDrizzle";
import { loadLocalEnv } from "../src/utils/envUtils";
import { getCursorPaginatedFullCusQuery } from "../src/internal/customers/cursorPaginatedFullCusQuery";
loadLocalEnv();
const ORG_ID = "r7pwHiekGsqt32qGqcVku6thWFh5aHh8";
const ENV: AppEnv = AppEnv.Live;
const LABEL = "runable";
const REPEATS = 5;
const STATEMENT_TIMEOUT_MS = 30_000;
const DEEP_OFFSET_PCT = 45;
const BASE_LIMIT = 1000;
const READ_ONLY = true;
const CUS_PRODUCT_LIMIT = 15;
const RESULTS_DIR = join(import.meta.dir, "results");
type DB = ReturnType<typeof initDrizzle>["db"];
const sharedFullCusOpts = {
inStatuses: RELEVANT_STATUSES,
includeInvoices: false,
withEntities: false,
withTrialsUsed: false,
withSubs: true,
cusProductLimit: CUS_PRODUCT_LIMIT,
};
type Cell = {
name: string;
build: () => SQL;
includeExplain?: boolean;
};
type CellResult = {
name: string;
medianMs: number;
p95Ms: number;
minMs: number;
maxMs: number;
rowCount: number;
samples: number[];
explainPlan?: string;
error?: string;
};
const normalizeRows = (r: unknown): Record<string, unknown>[] => {
if (Array.isArray(r)) return r as Record<string, unknown>[];
if (r && typeof r === "object" && "rows" in r) {
return (r as { rows: Record<string, unknown>[] }).rows;
}
return [];
};
const runQueryInTxn = async ({
db,
query,
}: {
db: DB;
query: SQL;
}): Promise<Record<string, unknown>[]> => {
return await db.transaction(async (tx) => {
await tx.execute(sql.raw(`SET LOCAL statement_timeout = ${STATEMENT_TIMEOUT_MS}`));
if (READ_ONLY) {
await tx.execute(sql.raw("SET LOCAL transaction_read_only = on"));
}
return normalizeRows(await tx.execute(query));
});
};
const measureCell = async ({
db,
cell,
}: {
db: DB;
cell: Cell;
}): Promise<CellResult> => {
const samples: number[] = [];
let rowCount = 0;
let error: string | undefined;
for (let i = 0; i < REPEATS; i++) {
const query = cell.build();
const startedAt = performance.now();
try {
const result = await runQueryInTxn({ db, query });
const elapsed = performance.now() - startedAt;
samples.push(elapsed);
rowCount = result.length;
} catch (err) {
const elapsed = performance.now() - startedAt;
samples.push(elapsed);
error = err instanceof Error ? err.message : String(err);
break;
}
}
let explainPlan: string | undefined;
if (cell.includeExplain && !error) {
try {
const query = cell.build();
const explainRows = await runQueryInTxn({
db,
query: sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query}`,
});
explainPlan = explainRows.map((row) => row["QUERY PLAN"]).join("\n");
} catch (err) {
explainPlan = `EXPLAIN failed: ${err instanceof Error ? err.message : String(err)}`;
}
}
const sorted = [...samples].sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
const min = sorted[0] ?? 0;
const max = sorted[sorted.length - 1] ?? 0;
return {
name: cell.name,
medianMs: median,
p95Ms: p95,
minMs: min,
maxMs: max,
rowCount,
samples,
explainPlan,
error,
};
};
const revenuecatCountQuery = (): SQL => sql`
SELECT COUNT(*)::int AS total_count
FROM customers c
WHERE c.org_id = ${ORG_ID}
AND c.env = ${ENV}
AND EXISTS (
SELECT 1 FROM customer_products cp
WHERE cp.internal_customer_id = c.internal_id
AND cp.processor->>'type' = 'revenuecat'
)
`;
const resolveRevenuecatCount = async ({ db }: { db: DB }) => {
const result = await runQueryInTxn({ db, query: revenuecatCountQuery() });
return (result[0] as { total_count: number }).total_count;
};
const resolveRevenuecatDeepCursor = async ({
db,
deepOffset,
}: {
db: DB;
deepOffset: number;
}): Promise<{ v: 0; t: number; id: string }> => {
const result = await runQueryInTxn({
db,
query: sql`
SELECT c.created_at, c.id
FROM customers c
WHERE c.org_id = ${ORG_ID}
AND c.env = ${ENV}
AND EXISTS (
SELECT 1 FROM customer_products cp
WHERE cp.internal_customer_id = c.internal_id
AND cp.processor->>'type' = 'revenuecat'
)
ORDER BY c.created_at DESC, c.id DESC
LIMIT 1 OFFSET ${deepOffset}
`,
});
const row = result[0] as { created_at: number; id: string } | undefined;
if (!row) {
throw new Error(
`Could not resolve revenuecat deep cursor at offset ${deepOffset}. Not enough revenuecat customers.`,
);
}
return { v: 0, t: row.created_at, id: row.id };
};
const buildCells = ({
deepCursor,
}: {
deepCursor: { v: 0; t: number; id: string };
}): Cell[] => {
return [
{
name: `10 cursor / revcat=true / first page / limit ${BASE_LIMIT}`,
build: () =>
getCursorPaginatedFullCusQuery({
orgId: ORG_ID,
env: ENV,
limit: BASE_LIMIT,
processors: ["revenuecat"],
...sharedFullCusOpts,
}),
includeExplain: true,
},
{
name: `11 cursor / revcat=true / deep / limit ${BASE_LIMIT}`,
build: () =>
getCursorPaginatedFullCusQuery({
orgId: ORG_ID,
env: ENV,
limit: BASE_LIMIT,
cursor: deepCursor,
processors: ["revenuecat"],
...sharedFullCusOpts,
}),
includeExplain: true,
},
];
};
const renderResults = ({
totalCount,
deepOffset,
deepCursor,
results,
}: {
totalCount: number;
deepOffset: number;
deepCursor: { v: 0; t: number; id: string };
results: CellResult[];
}): string => {
const date = new Date().toISOString().slice(0, 10);
const lines: string[] = [];
lines.push(`# Pagination Benchmark — ${date} — prod / ${LABEL}`);
lines.push("");
lines.push("## Config");
lines.push("");
lines.push(`- org_id: \`${ORG_ID}\``);
lines.push(`- env: \`${ENV}\``);
lines.push(`- revenuecat_customers: \`${totalCount.toLocaleString()}\``);
lines.push(
`- deep_offset (${DEEP_OFFSET_PCT}% within revcat subset): \`${deepOffset.toLocaleString()}\``,
);
lines.push(
`- deep_cursor (revcat-aware): \`{ t: ${deepCursor.t}, id: ${deepCursor.id} }\``,
);
lines.push(`- limit: \`${BASE_LIMIT}\``);
lines.push(`- repeats per cell: ${REPEATS}`);
lines.push(`- statement_timeout_ms: ${STATEMENT_TIMEOUT_MS}`);
lines.push(`- read_only: ${READ_ONLY}`);
lines.push(`- focus: revenuecat filter (single SQL call, cursor + EXISTS combined)`);
lines.push(`- partial index in place: idx_customer_products_revenuecat_processor`);
lines.push("");
lines.push("## Results");
lines.push("");
lines.push("| # | Cell | Rows | median ms | p95 ms | min ms | max ms | Error |");
lines.push("|---|------|------|-----------|--------|--------|--------|-------|");
for (const r of results) {
const idx = r.name.split(" ")[0];
const desc = r.name.split(" ").slice(1).join(" ");
const errCol = r.error ? `\`${r.error.slice(0, 80)}\`` : "";
lines.push(
`| ${idx} | ${desc} | ${r.rowCount} | ${r.medianMs.toFixed(2)} | ${r.p95Ms.toFixed(2)} | ${r.minMs.toFixed(2)} | ${r.maxMs.toFixed(2)} | ${errCol} |`,
);
}
lines.push("");
lines.push("## EXPLAIN ANALYZE");
lines.push("");
for (const r of results) {
if (!r.explainPlan) continue;
lines.push(`### ${r.name}`);
lines.push("");
lines.push("```");
lines.push(r.explainPlan);
lines.push("```");
lines.push("");
}
return lines.join("\n");
};
const main = async () => {
console.log(
chalk.magentaBright(
`\n================ Pagination Benchmark — prod / ${LABEL} ================\n`,
),
);
const { db, client } = initDrizzle();
try {
console.log(chalk.cyan(`Org: ${ORG_ID} (${ENV})`));
console.log(chalk.cyan("Resolving revenuecat customer count..."));
const totalCount = await resolveRevenuecatCount({ db });
const deepOffset = Math.floor((totalCount * DEEP_OFFSET_PCT) / 100);
console.log(
chalk.gray(
` revenuecat_customers = ${totalCount.toLocaleString()}, deep_offset (${DEEP_OFFSET_PCT}% within revcat) = ${deepOffset.toLocaleString()}`,
),
);
console.log(
chalk.cyan(`Resolving revcat-aware deep cursor at offset ${deepOffset}...`),
);
const deepCursor = await resolveRevenuecatDeepCursor({ db, deepOffset });
console.log(
chalk.gray(` cursor = { t: ${deepCursor.t}, id: ${deepCursor.id} }`),
);
console.log();
const cells = buildCells({ deepCursor });
const results: CellResult[] = [];
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
process.stdout.write(
chalk.gray(`[${i + 1}/${cells.length}] ${cell.name} ... `),
);
const result = await measureCell({ db, cell });
results.push(result);
if (result.error) {
console.log(chalk.red(`error: ${result.error.slice(0, 80)}`));
} else {
console.log(
chalk.green(
`median=${result.medianMs.toFixed(2)}ms p95=${result.p95Ms.toFixed(2)}ms rows=${result.rowCount}`,
),
);
}
}
const report = renderResults({
totalCount,
deepOffset,
deepCursor,
results,
});
const date = new Date().toISOString().slice(0, 10);
const outPath = join(RESULTS_DIR, `${date}-prod-${LABEL}.md`);
writeFileSync(outPath, report);
console.log();
console.log(chalk.green(`✅ Report written to ${outPath}`));
console.log(
chalk.magentaBright(
`\n================ Benchmark Complete ================\n`,
),
);
} catch (error) {
console.error(chalk.red("\n❌ Benchmark failed:"));
if (error instanceof Error) {
console.error(chalk.red(` ${error.message}`));
console.error(chalk.gray(error.stack));
} else {
console.error(chalk.red(` ${String(error)}`));
}
process.exit(1);
} finally {
await client.end();
process.exit(0);
}
};
await main();