Add D601 Tekton CI
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
interface TimingSample {
|
||||
label: string;
|
||||
method: string;
|
||||
url: string;
|
||||
ok: boolean;
|
||||
status: number;
|
||||
durationMs: number;
|
||||
bytes: number;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
function envNumber(name: string, fallback: number): number {
|
||||
const raw = process.env[name];
|
||||
if (raw === undefined || raw.length === 0) return fallback;
|
||||
const value = Number(raw);
|
||||
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`);
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
function baseUrl(): string {
|
||||
return (process.env.CI_CODE_QUEUE_URL ?? "http://code-queue-ci-read.unidesk-ci.svc.cluster.local:4222").replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
async function fetchSample(label: string, url: string, timeoutMs = 30_000): Promise<TimingSample> {
|
||||
const started = performance.now();
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
const text = await response.text();
|
||||
return {
|
||||
label,
|
||||
method: "GET",
|
||||
url,
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
durationMs: Math.round((performance.now() - started) * 10) / 10,
|
||||
bytes: text.length,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
label,
|
||||
method: "GET",
|
||||
url,
|
||||
ok: false,
|
||||
status: 0,
|
||||
durationMs: Math.round((performance.now() - started) * 10) / 10,
|
||||
bytes: 0,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function percentile(values: number[], percentileValue: number): number {
|
||||
if (values.length === 0) return 0;
|
||||
const sorted = values.slice().sort((left, right) => left - right);
|
||||
if (percentileValue <= 0) return sorted[0] ?? 0;
|
||||
if (percentileValue >= 100) return sorted[sorted.length - 1] ?? 0;
|
||||
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((percentileValue / 100) * sorted.length) - 1));
|
||||
return sorted[index] ?? 0;
|
||||
}
|
||||
|
||||
async function candidateTaskIds(url: string): Promise<string[]> {
|
||||
const response = await fetch(`${url}/api/tasks/overview?limit=24&transcriptLimit=0&compact=1&selected=1&includeActive=0&stats=0&skipTrace=1`, {
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const body = await response.json() as { selected?: { task?: { id?: string } }; tasks?: Array<{ id?: string }> };
|
||||
const ids = [
|
||||
body.selected?.task?.id,
|
||||
...(body.tasks ?? []).map((task) => task.id),
|
||||
].filter((id): id is string => typeof id === "string" && id.length > 0);
|
||||
return [...new Set(ids)];
|
||||
}
|
||||
|
||||
async function traceSeq(url: string, taskId: string): Promise<number | null> {
|
||||
const response = await fetch(`${url}/api/tasks/${encodeURIComponent(taskId)}/trace-steps?tail=1&limit=8`, {
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const body = await response.json() as { steps?: Array<{ seq?: number }> };
|
||||
const seq = body.steps?.find((step) => Number.isFinite(Number(step.seq)))?.seq;
|
||||
if (!Number.isFinite(Number(seq))) return null;
|
||||
return Number(seq);
|
||||
}
|
||||
|
||||
async function traceTarget(url: string): Promise<{ taskId: string; seq: number; skippedTaskIds: string[] }> {
|
||||
const ids = await candidateTaskIds(url);
|
||||
if (ids.length === 0) throw new Error("Code Queue CI perf could not find a task id in the production PostgreSQL task table");
|
||||
const skippedTaskIds: string[] = [];
|
||||
for (const taskId of ids) {
|
||||
const seq = await traceSeq(url, taskId);
|
||||
if (seq !== null) return { taskId, seq, skippedTaskIds };
|
||||
skippedTaskIds.push(taskId);
|
||||
}
|
||||
throw new Error(`Code Queue CI perf could not find a task with trace steps among ${ids.length} candidates: ${skippedTaskIds.join(",")}`);
|
||||
}
|
||||
|
||||
async function measureFirstPaint(url: string): Promise<Record<string, unknown>> {
|
||||
const sample = await fetchSample("code-queue-read-first-paint-proxy", `${url}/api/tasks/overview?limit=12&transcriptLimit=1&compact=1&selected=0&includeActive=0&stats=0&skipTrace=1`, 60_000);
|
||||
return {
|
||||
ok: sample.ok,
|
||||
url: sample.url,
|
||||
firstPaintMs: sample.durationMs,
|
||||
apiTimings: [sample],
|
||||
consoleErrors: [],
|
||||
note: "Code Queue service is API-only in k3s; this measures the first overview payload used by the frontend Code Queue page.",
|
||||
};
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const url = baseUrl();
|
||||
const budgets = {
|
||||
firstPaintMs: envNumber("FIRST_PAINT_BUDGET_MS", 2000),
|
||||
traceSummaryMs: envNumber("TRACE_SUMMARY_BUDGET_MS", 700),
|
||||
traceStepsMs: envNumber("TRACE_STEPS_BUDGET_MS", 900),
|
||||
traceStepDetailMs: envNumber("TRACE_STEP_DETAIL_BUDGET_MS", 700),
|
||||
overviewP95Ms: envNumber("OVERVIEW_P95_BUDGET_MS", 900),
|
||||
};
|
||||
const health = await fetchSample("health", `${url}/health`);
|
||||
if (!health.ok) throw new Error(`Code Queue CI read health failed: ${JSON.stringify(health)}`);
|
||||
const target = await traceTarget(url);
|
||||
const { taskId, seq } = target;
|
||||
const firstPaint = await measureFirstPaint(url);
|
||||
const traceSummary = await fetchSample("trace-summary", `${url}/api/tasks/${encodeURIComponent(taskId)}/trace-summary`);
|
||||
const traceSteps = await fetchSample("trace-steps", `${url}/api/tasks/${encodeURIComponent(taskId)}/trace-steps?tail=1&limit=20`);
|
||||
const traceStepDetail = await fetchSample("trace-step-detail", `${url}/api/tasks/${encodeURIComponent(taskId)}/trace-step?seq=${encodeURIComponent(String(seq))}`);
|
||||
const overviewSamples: TimingSample[] = [];
|
||||
for (let index = 0; index < 10; index += 1) {
|
||||
overviewSamples.push(await fetchSample("overview", `${url}/api/tasks/overview?limit=12&transcriptLimit=1&compact=1&selected=0&includeActive=0&stats=0&skipTrace=1&__ci=${Date.now()}-${index}`));
|
||||
}
|
||||
const overviewSuccessful = overviewSamples.filter((sample) => sample.ok).map((sample) => sample.durationMs);
|
||||
const overviewP95Ms = Math.round(percentile(overviewSuccessful, 95) * 10) / 10;
|
||||
const firstPaintMs = Number((firstPaint as { firstPaintMs?: number }).firstPaintMs ?? 0);
|
||||
const checks = [
|
||||
{ name: "first-paint", ok: firstPaintMs <= budgets.firstPaintMs, valueMs: firstPaintMs, budgetMs: budgets.firstPaintMs },
|
||||
{ name: "trace-summary", ok: traceSummary.ok && traceSummary.durationMs <= budgets.traceSummaryMs, valueMs: traceSummary.durationMs, budgetMs: budgets.traceSummaryMs },
|
||||
{ name: "trace-steps", ok: traceSteps.ok && traceSteps.durationMs <= budgets.traceStepsMs, valueMs: traceSteps.durationMs, budgetMs: budgets.traceStepsMs },
|
||||
{ name: "trace-step-detail", ok: traceStepDetail.ok && traceStepDetail.durationMs <= budgets.traceStepDetailMs, valueMs: traceStepDetail.durationMs, budgetMs: budgets.traceStepDetailMs },
|
||||
{ name: "overview-p95", ok: overviewSamples.every((sample) => sample.ok) && overviewP95Ms <= budgets.overviewP95Ms, valueMs: overviewP95Ms, budgetMs: budgets.overviewP95Ms },
|
||||
];
|
||||
const result = {
|
||||
ok: checks.every((check) => check.ok),
|
||||
measuredAt: new Date().toISOString(),
|
||||
url,
|
||||
taskId,
|
||||
seq,
|
||||
skippedTaskIds: target.skippedTaskIds,
|
||||
budgets,
|
||||
checks,
|
||||
health,
|
||||
firstPaint,
|
||||
traceSummary,
|
||||
traceSteps,
|
||||
traceStepDetail,
|
||||
overview: {
|
||||
p50Ms: Math.round(percentile(overviewSuccessful, 50) * 10) / 10,
|
||||
p95Ms: overviewP95Ms,
|
||||
samples: overviewSamples,
|
||||
},
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!result.ok) process.exitCode = 1;
|
||||
}
|
||||
|
||||
await main();
|
||||
Reference in New Issue
Block a user