feat: add workbench performance dashboard

This commit is contained in:
lyon
2026-06-19 16:49:13 +08:00
parent 80dc11f073
commit 0645e7ffa6
7 changed files with 1072 additions and 155 deletions
@@ -257,6 +257,7 @@ function webPerformanceSummaryPayload(): JsonRecord {
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),
rows,
problems: rows.filter((row) => row.tone === "warn"),
webVitals: rows.filter((row) => row.kind === "web-vital"),
@@ -272,6 +273,71 @@ function performanceRow(row: JsonRecord): JsonRecord {
return { method: "GET", statusClass: "2xx", outcome: "ok", unit: "seconds", sampleState: "ok", ...row };
}
function webPerformanceDashboardPayload(rows: JsonRecord[]): 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");
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 },
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 个样本" }
],
trends: [
{ id: "workbench-experience", title: "工作台体感趋势", subtitle: "首屏、会话切换和提交首个可见结果的 P95", unit: "seconds", points: workbench.map(dashboardPoint) },
{ id: "api-timing", title: "同源 API 耗时", subtitle: "Cloud Web 同源 API 请求 P95", unit: "seconds", points: api.map(dashboardPoint) },
{ id: "web-vitals", title: "浏览器核心指标", subtitle: "浏览器体感当前窗口", unit: "mixed", points: vital.map(dashboardPoint) }
],
distributions: [
{ id: "status", title: "状态分布", subtitle: "按健康状态聚合当前窗口行数", unit: "count", buckets: [{ label: "正常", count: 4, tone: "ok" }, { label: "预警", count: 2, tone: "warn" }] },
{ id: "samples", title: "样本状态", subtitle: "正常、低样本和暂无数据分布", unit: "count", buckets: [{ label: "正常样本", count: 6, tone: "ok" }] }
],
topN: [
{ id: "top-api", title: "慢 API TopN", subtitle: "同源 API P95 最慢路由", unit: "seconds", rows: api.map(dashboardTopRow) },
{ id: "top-workbench", title: "工作台慢路径 TopN", subtitle: "工作台 journey 和事件可见延迟", unit: "seconds", rows: workbench.map(dashboardTopRow) },
{ id: "top-long-task", title: "长任务 TopN", subtitle: "主线程长任务当前窗口", unit: "seconds", rows: longTask.map(dashboardTopRow) }
]
};
}
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 };
}
function dashboardTopRow(row: JsonRecord): JsonRecord {
return { label: dashboardMetricLabel(row), detail: dashboardRouteLabel(row.route), value: row.p95, unit: row.unit ?? "seconds", count: row.count, tone: row.tone };
}
function dashboardRouteLabel(route: unknown): string {
const value = String(route ?? "").trim();
if (!value || value === "-") return "当前窗口";
return value
.replace(/:sessionId/gu, ":")
.replace(/:traceId/gu, ":")
.replace(/:runId/gu, ":")
.replace(/:threadId/gu, ":线")
.replace(/:turnId/gu, ":");
}
function dashboardMetricLabel(row: JsonRecord): string {
const metric = String(row.metric ?? "unknown");
const labels: Record<string, string> = {
"session-switch-full-load": "切换会话完整加载",
"trace-event-projected": "轨迹事件投影",
"backend-event-visible": "后端事件到可见",
LCP: "最大内容绘制",
fetchJson: "同源 API 请求",
longtask: "主线程长任务"
};
return labels[metric] ?? metric;
}
function createScenarioState(scenarioId: string): ScenarioState {
const base = structuredClone(capture.scenario);
const id = scenarioId || "baseline";