|
|
|
@@ -42,6 +42,15 @@ interface ScenarioState {
|
|
|
|
|
liveBackfillReadyAtMs: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FakePerformanceWindow {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
seconds: number | null;
|
|
|
|
|
windowFrom: string | null;
|
|
|
|
|
windowTo: string;
|
|
|
|
|
generatedAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cwd = process.cwd();
|
|
|
|
|
const args = new Map<string, string>();
|
|
|
|
|
for (let index = 2; index < process.argv.length; index += 1) {
|
|
|
|
@@ -214,7 +223,10 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
|
|
|
|
|
if (path === "/v1/live-builds") return json(response, 200, { status: "ok", builds: [] });
|
|
|
|
|
if (path === "/v1/hwpod/specs") return json(response, 200, hwpodSpecsPayload());
|
|
|
|
|
if (path === "/v1/hwpod-node-ops") return json(response, 200, hwpodNodeOpsPayload());
|
|
|
|
|
if (path === "/v1/web-performance/summary") return json(response, 200, webPerformanceSummaryPayload());
|
|
|
|
|
if (path === "/v1/web-performance/summary") {
|
|
|
|
|
if (state.scenarioId === "performance-summary-error") return json(response, 503, { ok: false, status: 503, error: { code: "performance_summary_unavailable", message: "performance summary read model is unavailable" } });
|
|
|
|
|
return json(response, 200, state.scenarioId === "performance-empty" ? webPerformanceEmptyPayload(url) : webPerformanceSummaryPayload(url));
|
|
|
|
|
}
|
|
|
|
|
if (path.startsWith("/v1/") || path.startsWith("/auth/") || path.startsWith("/health")) return json(response, 500, { ok: false, status: 500, error: { code: "unmocked_request", path } });
|
|
|
|
|
|
|
|
|
|
return staticFile(response, path);
|
|
|
|
@@ -261,24 +273,28 @@ function hwpodNodeOpsPayload(): JsonRecord {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function webPerformanceSummaryPayload(): JsonRecord {
|
|
|
|
|
function webPerformanceSummaryPayload(url: URL): JsonRecord {
|
|
|
|
|
const sampleWindow = fakePerformanceWindow(url.searchParams.get("window"));
|
|
|
|
|
const rows = [
|
|
|
|
|
performanceRow({ kind: "workbench-journey", metric: "session-switch-full-load", route: "/workbench/sessions/:sessionId", count: 21, p50: 0.18, p75: 0.31, p95: 1.12, tone: "warn", backend: "agentrun", transport: "sse", targetState: "completed" }),
|
|
|
|
|
performanceRow({ kind: "workbench-event-phase", metric: "trace-event-projected", route: "/workbench", count: 28, p50: 0.08, p75: 0.14, p95: 0.42, tone: "ok", eventType: "tool_result", phase: "render", source: "rum" }),
|
|
|
|
|
performanceRow({ kind: "workbench-backend-event", metric: "backend-event-visible", route: "/v1/workbench/events", count: 15, p50: 0.2, p75: 0.34, p95: 1.38, tone: "warn", eventType: "agentrun.stdout", backend: "agentrun", cache: "miss" }),
|
|
|
|
|
performanceRow({ kind: "web-vital", metric: "LCP", route: "/performance", count: 9, p50: 1.2, p75: 1.8, p95: 2.9, tone: "ok" }),
|
|
|
|
|
performanceRow({ kind: "api", metric: "fetchJson", route: "/v1/web-performance/summary", method: "GET", statusClass: "2xx", outcome: "ok", count: 17, p50: 0.09, p75: 0.15, p95: 0.33, tone: "ok" }),
|
|
|
|
|
performanceRow({ kind: "long-task", metric: "longtask", route: "/workbench", count: 3, p50: 0.08, p75: 0.11, p95: 0.2, tone: "warn", problem: "main-thread busy" })
|
|
|
|
|
performanceRow({ kind: "workbench-journey", metric: "session-switch-full-load", route: "/workbench/sessions/:sessionId", count: 21, p50: 0.18, p75: 0.31, p95: 1.12, tone: "warn", backend: "agentrun", transport: "sse", targetState: "completed", sourceFamily: "workbench" }, sampleWindow),
|
|
|
|
|
performanceRow({ kind: "workbench-event-phase", metric: "trace-event-projected", route: "/workbench", count: 28, p50: 0.08, p75: 0.14, p95: 0.42, tone: "ok", eventType: "tool_result", phase: "render", source: "rum", sourceFamily: "workbench" }, sampleWindow),
|
|
|
|
|
performanceRow({ kind: "workbench-backend-event", metric: "backend-event-visible", route: "/v1/workbench/events", count: 15, p50: 0.2, p75: 0.34, p95: 1.38, tone: "warn", eventType: "agentrun.stdout", backend: "agentrun", cache: "miss", sourceFamily: "backend" }, sampleWindow),
|
|
|
|
|
performanceRow({ kind: "web-vital", metric: "LCP", route: "/performance", count: 9, p50: 1.2, p75: 1.8, p95: 2.9, tone: "ok", sourceFamily: "web" }, sampleWindow),
|
|
|
|
|
performanceRow({ kind: "api", metric: "api_request", route: "/v1/web-performance/summary", method: "GET", statusClass: "2xx", outcome: "ok", count: 17, p50: 0.09, p75: 0.15, p95: 0.33, tone: "ok", sourceFamily: "web" }, sampleWindow),
|
|
|
|
|
performanceRow({ kind: "api", metric: "api_request", route: "/v1/workbench/sessions", method: "GET", statusClass: "2xx", outcome: "ok", count: 3, p50: 2.5, p75: 5, p95: 10, tone: "warn", problem: "slow-api_request", sampleState: "low-sample", freshness: "low-sample", aggregationKind: "histogram", approximation: "bucketed", sourceFamily: "web" }, sampleWindow),
|
|
|
|
|
performanceRow({ kind: "long-task", metric: "longtask", route: "/workbench", count: 3, p50: 0.08, p75: 0.11, p95: 0.2, tone: "warn", problem: "main-thread busy", sourceFamily: "web" }, sampleWindow)
|
|
|
|
|
];
|
|
|
|
|
const sampleCount = performanceSampleCount(rows);
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
status: "ready",
|
|
|
|
|
source: "fake-server",
|
|
|
|
|
observedAt: "2026-06-18T04:30:00.000Z",
|
|
|
|
|
observedAt: sampleWindow.generatedAt,
|
|
|
|
|
namespace: "hwlab-v03",
|
|
|
|
|
gitopsTarget: "D601/v03",
|
|
|
|
|
summary: { status: "ready", sampleCount: 93, sampleSeries: 12, problemCount: 2, lowSampleThreshold: 5, workbenchJourneySeries: 1, workbenchEventPhaseSeries: 1, workbenchBackendEventVisibleSeries: 1 },
|
|
|
|
|
dashboard: webPerformanceDashboardPayload(rows),
|
|
|
|
|
sampleWindow: performanceSampleWindowPayload(sampleWindow),
|
|
|
|
|
summary: performanceSummaryContract(sampleWindow, rows, { status: "ready", sampleCount, sampleSeries: 12, problemCount: rows.filter((row) => row.tone === "warn").length }),
|
|
|
|
|
dashboard: webPerformanceDashboardPayload(rows, sampleWindow),
|
|
|
|
|
rows,
|
|
|
|
|
problems: rows.filter((row) => row.tone === "warn"),
|
|
|
|
|
webVitals: rows.filter((row) => row.kind === "web-vital"),
|
|
|
|
@@ -290,25 +306,68 @@ function webPerformanceSummaryPayload(): JsonRecord {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function performanceRow(row: JsonRecord): JsonRecord {
|
|
|
|
|
return { method: "GET", statusClass: "2xx", outcome: "ok", unit: "seconds", sampleState: "ok", ...row };
|
|
|
|
|
function webPerformanceEmptyPayload(url: URL): JsonRecord {
|
|
|
|
|
const sampleWindow = fakePerformanceWindow(url.searchParams.get("window"));
|
|
|
|
|
const rows: JsonRecord[] = [];
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
status: "waiting",
|
|
|
|
|
source: "fake-server",
|
|
|
|
|
observedAt: sampleWindow.generatedAt,
|
|
|
|
|
namespace: "hwlab-v03",
|
|
|
|
|
gitopsTarget: "D601/v03",
|
|
|
|
|
sampleWindow: performanceSampleWindowPayload(sampleWindow),
|
|
|
|
|
summary: performanceSummaryContract(sampleWindow, rows, { status: "waiting", sampleCount: 0, sampleSeries: 0, problemCount: 0 }),
|
|
|
|
|
dashboard: webPerformanceDashboardPayload(rows, sampleWindow),
|
|
|
|
|
rows,
|
|
|
|
|
problems: [],
|
|
|
|
|
webVitals: [],
|
|
|
|
|
apiRoutes: [],
|
|
|
|
|
longTasks: [],
|
|
|
|
|
workbenchJourneys: [],
|
|
|
|
|
workbenchEventPhases: [],
|
|
|
|
|
workbenchBackendEvents: []
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function webPerformanceDashboardPayload(rows: JsonRecord[]): JsonRecord {
|
|
|
|
|
function performanceRow(row: JsonRecord, sampleWindow: FakePerformanceWindow): JsonRecord {
|
|
|
|
|
const count = Number(row.count ?? 0);
|
|
|
|
|
const sampleState = count === 0 ? "empty" : count < 5 ? "low-sample" : "ok";
|
|
|
|
|
return {
|
|
|
|
|
method: "GET",
|
|
|
|
|
statusClass: "2xx",
|
|
|
|
|
outcome: "ok",
|
|
|
|
|
unit: "seconds",
|
|
|
|
|
sampleState,
|
|
|
|
|
sampleCount: count,
|
|
|
|
|
sourceFamily: "mixed",
|
|
|
|
|
freshness: sampleState === "ok" ? "fresh" : sampleState,
|
|
|
|
|
aggregationKind: "exact",
|
|
|
|
|
approximation: "exact",
|
|
|
|
|
windowFrom: sampleWindow.windowFrom,
|
|
|
|
|
windowTo: sampleWindow.windowTo,
|
|
|
|
|
generatedAt: sampleWindow.generatedAt,
|
|
|
|
|
...row
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function webPerformanceDashboardPayload(rows: JsonRecord[], sampleWindow: FakePerformanceWindow): JsonRecord {
|
|
|
|
|
const workbench = rows.filter((row) => String(row.kind).startsWith("workbench"));
|
|
|
|
|
const api = rows.filter((row) => row.kind === "api");
|
|
|
|
|
const vital = rows.filter((row) => row.kind === "web-vital");
|
|
|
|
|
const longTask = rows.filter((row) => row.kind === "long-task");
|
|
|
|
|
const sampleCount = performanceSampleCount(rows);
|
|
|
|
|
const problemCount = rows.filter((row) => row.tone === "warn").length;
|
|
|
|
|
return {
|
|
|
|
|
schemaVersion: "hwlab-web-performance-dashboard-v1",
|
|
|
|
|
generatedAt: "2026-06-18T04:30:00.000Z",
|
|
|
|
|
sampleWindow: { label: "当前采集窗口", bucketKind: "snapshot", buckets: [{ id: "current", label: "当前窗口", observedAt: "2026-06-18T04:30:00.000Z", sampleCount: 93 }] },
|
|
|
|
|
freshness: { observedAt: "2026-06-18T04:30:00.000Z", source: "fake-server", namespace: "hwlab-v03", gitopsTarget: "D601/v03", status: "watch", statusLabel: "预警", lowSampleThreshold: 5 },
|
|
|
|
|
generatedAt: sampleWindow.generatedAt,
|
|
|
|
|
sampleWindow: { ...performanceSampleWindowPayload(sampleWindow), buckets: [{ id: sampleWindow.id, label: sampleWindow.label, observedAt: sampleWindow.generatedAt, sampleCount }] },
|
|
|
|
|
freshness: { observedAt: sampleWindow.generatedAt, generatedAt: sampleWindow.generatedAt, windowFrom: sampleWindow.windowFrom, windowTo: sampleWindow.windowTo, sampleCount, source: "fake-server", sourceFamily: "mixed", namespace: "hwlab-v03", gitopsTarget: "D601/v03", status: sampleCount === 0 ? "waiting" : "watch", statusLabel: sampleCount === 0 ? "等待数据" : "预警", lowSampleThreshold: 5 },
|
|
|
|
|
cards: [
|
|
|
|
|
{ id: "health", label: "采集状态", value: "预警", unit: "状态", tone: "warn", detail: "fake-server" },
|
|
|
|
|
{ id: "samples", label: "样本数", value: 93, unit: "样本", tone: "ok", detail: "12 个序列" },
|
|
|
|
|
{ id: "workbench", label: "工作台序列", value: 3, unit: "序列", tone: "ok", detail: "hwlab-v03 / D601/v03" },
|
|
|
|
|
{ id: "problems", label: "问题行", value: 2, unit: "条", tone: "warn", detail: "低样本阈值 5 个样本" }
|
|
|
|
|
{ id: "health", label: "采集状态", value: sampleCount === 0 ? "等待数据" : "预警", unit: "状态", tone: sampleCount === 0 ? "source" : "warn", detail: "fake-server" },
|
|
|
|
|
{ id: "samples", label: "样本数", value: sampleCount, unit: "样本", tone: sampleCount === 0 ? "source" : "ok", detail: `${sampleWindow.label} · ${rows.length} 个序列` },
|
|
|
|
|
{ id: "workbench", label: "工作台序列", value: workbench.length, unit: "序列", tone: workbench.length > 0 ? "ok" : "source", detail: "hwlab-v03 / D601/v03" },
|
|
|
|
|
{ id: "problems", label: "问题行", value: problemCount, unit: "条", tone: problemCount > 0 ? "warn" : "ok", detail: "低样本阈值 5 个样本" }
|
|
|
|
|
],
|
|
|
|
|
trends: [
|
|
|
|
|
{ id: "workbench-experience", title: "工作台体感趋势", subtitle: "首屏、会话切换和提交首个可见结果的 P95", unit: "seconds", points: workbench.map(dashboardPoint) },
|
|
|
|
@@ -327,6 +386,49 @@ function webPerformanceDashboardPayload(rows: JsonRecord[]): JsonRecord {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fakePerformanceWindow(value: string | null): FakePerformanceWindow {
|
|
|
|
|
const id = ["5m", "15m", "1h", "6h", "24h", "all"].includes(String(value)) ? String(value) : "15m";
|
|
|
|
|
const generatedAt = "2026-06-18T04:30:00.000Z";
|
|
|
|
|
const specs: Record<string, Omit<FakePerformanceWindow, "id" | "generatedAt" | "windowTo">> = {
|
|
|
|
|
"5m": { label: "最近 5 分钟", seconds: 300, windowFrom: "2026-06-18T04:25:00.000Z" },
|
|
|
|
|
"15m": { label: "最近 15 分钟", seconds: 900, windowFrom: "2026-06-18T04:15:00.000Z" },
|
|
|
|
|
"1h": { label: "最近 1 小时", seconds: 3600, windowFrom: "2026-06-18T03:30:00.000Z" },
|
|
|
|
|
"6h": { label: "最近 6 小时", seconds: 21600, windowFrom: "2026-06-17T22:30:00.000Z" },
|
|
|
|
|
"24h": { label: "最近 24 小时", seconds: 86400, windowFrom: "2026-06-17T04:30:00.000Z" },
|
|
|
|
|
all: { label: "全部样本", seconds: null, windowFrom: null }
|
|
|
|
|
};
|
|
|
|
|
const spec = specs[id] ?? { label: "最近 15 分钟", seconds: 900, windowFrom: "2026-06-18T04:15:00.000Z" };
|
|
|
|
|
return { id, generatedAt, windowTo: generatedAt, ...spec };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function performanceSampleWindowPayload(sampleWindow: FakePerformanceWindow): JsonRecord {
|
|
|
|
|
return { id: sampleWindow.id, label: sampleWindow.label, seconds: sampleWindow.seconds, windowFrom: sampleWindow.windowFrom, windowTo: sampleWindow.windowTo, generatedAt: sampleWindow.generatedAt, aggregationKind: "exact", approximation: "exact" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function performanceSummaryContract(sampleWindow: FakePerformanceWindow, rows: JsonRecord[], summary: JsonRecord): JsonRecord {
|
|
|
|
|
return {
|
|
|
|
|
routeCount: rows.length,
|
|
|
|
|
durationSeries: rows.filter((row) => row.unit === "seconds").length,
|
|
|
|
|
lowSampleThreshold: 5,
|
|
|
|
|
workbenchJourneySeries: rows.filter((row) => row.kind === "workbench-journey").length,
|
|
|
|
|
workbenchEventPhaseSeries: rows.filter((row) => row.kind === "workbench-event-phase").length,
|
|
|
|
|
workbenchBackendEventVisibleSeries: rows.filter((row) => row.kind === "workbench-backend-event").length,
|
|
|
|
|
window: sampleWindow.id,
|
|
|
|
|
windowLabel: sampleWindow.label,
|
|
|
|
|
windowSeconds: sampleWindow.seconds,
|
|
|
|
|
windowFrom: sampleWindow.windowFrom,
|
|
|
|
|
windowTo: sampleWindow.windowTo,
|
|
|
|
|
generatedAt: sampleWindow.generatedAt,
|
|
|
|
|
aggregationKind: "exact",
|
|
|
|
|
approximation: "exact",
|
|
|
|
|
...summary
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function performanceSampleCount(rows: JsonRecord[]): number {
|
|
|
|
|
return rows.reduce((total, row) => total + (Number(row.count) || 0), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dashboardPoint(row: JsonRecord): JsonRecord {
|
|
|
|
|
return { label: dashboardMetricLabel(row), detail: dashboardRouteLabel(row.route), value: row.p95, p50: row.p50, p75: row.p75, p95: row.p95, count: row.count, tone: row.tone, sampleState: row.sampleState };
|
|
|
|
|
}
|
|
|
|
|