大重构改完

This commit is contained in:
2026-08-12 21:31:02 +08:00
parent 7c00b8565d
commit 15c3d21f2d
73 changed files with 4303 additions and 1727 deletions
+472 -63
View File
@@ -57,7 +57,7 @@ function grouped(items) {
}
function adminiveFieldVisible(expression, data) {
if (!expression) return true;
const equality = expression.match(/^\$\{\$self\.([A-Za-z_][A-Za-z0-9_]*) == '([^']*)'\}$/);
const equality = expression.match(/^\$\{\$self\.([A-Za-z_][A-Za-z0-9_]*) == '([^']*)'}$/);
return equality ? String(data?.[equality[1]]) === equality[2] : true;
}
function adminiveControls(resource) {
@@ -212,7 +212,11 @@ class GalleryCard {
this.controls = [];
this.actions = [];
this.observers = [];
this.taskGraph = null;
this.renderPlan = null;
this.performanceCapture = {controller: {}, sessions: [], plans: []};
this.selectedCaptureSessionId = null;
this.selectedCaptureFrameId = null;
this.selectedCaptureNodeId = null;
this.menuGroupState = new Map();
this.menuRequestPending = false;
this.telemetry = {};
@@ -408,7 +412,11 @@ class GalleryCard {
if (data.type === "observer_state") {
this.telemetry = data.telemetry || {};
this.observers = this.telemetry.renderable_observers || this.observers;
this.performanceCapture = this.telemetry.performance_capture ||
this.performanceCapture;
this.updateDashboard();
if (activeCard === this && activeTab === "performance_capture")
renderPerformanceCaptureMenu();
return;
}
const manualRefresh = data.type === "refresh_state";
@@ -417,8 +425,9 @@ class GalleryCard {
this.actions = data.actions?.data || [];
this.telemetry = data.telemetry || {};
this.observers = this.telemetry.renderable_observers || data.controls?.observers || [];
const nextTaskGraph = data.controls?.task_graph || null;
this.taskGraph = nextTaskGraph;
this.renderPlan = data.controls?.render_plan || null;
this.performanceCapture = data.controls?.performance_capture ||
this.telemetry.performance_capture || this.performanceCapture;
this.ready = true;
this.backendActive = null;
this.node.dataset.ready = "true";
@@ -894,81 +903,469 @@ function renderPerformanceMenu() {
elements.menuBody.replaceChildren(...groups);
}
function taskGraphSvg(graph, sceneGraph = false) {
function captureMilliseconds(value) {
return `${(Number(value || 0) / 1e6).toFixed(3)} ms`;
}
function captureSection(title, className = "") {
const section = document.createElement("section");
section.className = `capture-section ${className}`.trim();
const heading = document.createElement("h3");
heading.textContent = title;
section.append(heading);
return section;
}
function captureMetricGrid(values) {
const grid = document.createElement("dl");
grid.className = "capture-metrics";
for (const [label, value] of values) {
const term = document.createElement("dt");
const detail = document.createElement("dd");
term.textContent = label;
detail.textContent = value;
grid.append(term, detail);
}
return grid;
}
function selectedCaptureContext() {
const capture = activeCard.performanceCapture || {controller: {}, sessions: [], plans: []};
const sessions = capture.sessions || [];
let session = sessions.find(item => item.session_id === activeCard.selectedCaptureSessionId);
if (!session) session = sessions.at(-1) || null;
if (session) activeCard.selectedCaptureSessionId = session.session_id;
let frame = session?.frames?.find(item => item.frame_id === activeCard.selectedCaptureFrameId);
if (!frame) frame = session?.frames?.at(-1) || null;
if (frame) activeCard.selectedCaptureFrameId = frame.frame_id;
const plan = frame
? (capture.plans || []).find(item => item.version === frame.render_plan_version) || null
: activeCard.renderPlan;
return {capture, sessions, session, frame, plan};
}
function captureDagSvg(plan, frame, statistics) {
const namespace = "http://www.w3.org/2000/svg";
const nodes = [...(graph.nodes || [])];
const byId = new Map(nodes.map(node => [node.id, node]));
const ordered = sceneGraph && graph.paint_order?.length
? graph.paint_order.map(id => byId.get(id)).filter(Boolean)
: nodes;
const nodes = [...(plan?.nodes || [])];
const kinds = ["prepare", "paint", "composite"];
const byKind = new Map(kinds.map(kind => [kind, nodes.filter(node => node.kind === kind)]));
const nodeWidth = 244, nodeHeight = 88, columnGap = 42, rowGap = 24;
const positions = new Map();
const nodeWidth = 196, nodeHeight = 44, rowGap = 22;
ordered.forEach((node, index) => {
const lane = sceneGraph && node.kind === "control" ? 1 : 0;
positions.set(node.id, {x: 18 + lane * 234, y: 18 + index * (nodeHeight + rowGap)});
});
const width = sceneGraph ? 468 : 232;
const height = Math.max(82, ordered.length * (nodeHeight + rowGap) + 18);
kinds.forEach((kind, column) => byKind.get(kind).forEach((node, row) => {
positions.set(String(node.id), {
x: 20 + column * (nodeWidth + columnGap),
y: 48 + row * (nodeHeight + rowGap)
});
}));
const maximumRows = Math.max(1, ...kinds.map(kind => byKind.get(kind).length));
const width = 20 + kinds.length * nodeWidth + (kinds.length - 1) * columnGap + 20;
const height = 58 + maximumRows * (nodeHeight + rowGap);
const svg = document.createElementNS(namespace, "svg");
svg.classList.add("task-graph-svg");
svg.classList.add("capture-dag");
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
svg.setAttribute("role", "img");
svg.setAttribute("aria-label", sceneGraph ? "场景任务图" : "控件内部任务图");
for (const edge of graph.edges || []) {
const from = positions.get(edge.from), to = positions.get(edge.to);
const executionByNode = new Map((frame?.node_executions || []).map(item => [Number(item.node_id), item]));
const analysisByNode = new Map((frame?.analysis?.nodes || []).map(item => [Number(item.node_id), item]));
const statisticsByNode = new Map((statistics || []).map(item => [Number(item.node_id), item]));
kinds.forEach((kind, column) => {
const heading = document.createElementNS(namespace, "text");
heading.classList.add("capture-dag-heading");
heading.setAttribute("x", 20 + column * (nodeWidth + columnGap));
heading.setAttribute("y", 25);
heading.textContent = kind.toUpperCase();
svg.append(heading);
});
for (const edge of plan?.edges || []) {
const from = positions.get(String(edge.from));
const to = positions.get(String(edge.to));
if (!from || !to) continue;
const path = document.createElementNS(namespace, "path");
const fromX = from.x + nodeWidth / 2, fromY = from.y + nodeHeight;
const toX = to.x + nodeWidth / 2, toY = to.y;
const bend = Math.max(14, (toY - fromY) * .45);
path.setAttribute("d", `M ${fromX} ${fromY} C ${fromX} ${fromY + bend}, ${toX} ${toY - bend}, ${toX} ${toY}`);
path.dataset.kind = edge.kind || "dependency";
path.classList.add("task-edge");
const startX = from.x + nodeWidth;
const startY = from.y + nodeHeight / 2;
const endX = to.x;
const endY = to.y + nodeHeight / 2;
const bend = Math.max(22, Math.abs(endX - startX) * .45);
path.setAttribute("d", `M ${startX} ${startY} C ${startX + bend} ${startY}, ${endX - bend} ${endY}, ${endX} ${endY}`);
path.classList.add("capture-dag-edge");
svg.append(path);
}
for (const node of ordered) {
const position = positions.get(node.id);
for (const node of nodes) {
const position = positions.get(String(node.id));
const execution = executionByNode.get(Number(node.node_id));
const analysis = analysisByNode.get(Number(node.node_id));
const historical = statisticsByNode.get(Number(node.node_id));
const group = document.createElementNS(namespace, "g");
group.classList.add("task-node"); group.dataset.kind = node.kind || "task";
group.classList.add("capture-dag-node");
group.dataset.kind = node.kind;
group.dataset.selected = String(Number(node.node_id) === Number(activeCard.selectedCaptureNodeId));
group.dataset.critical = String(Boolean(analysis?.on_critical_path));
group.setAttribute("tabindex", "0");
const select = () => {
activeCard.selectedCaptureNodeId = Number(node.node_id);
renderPerformanceCaptureMenu();
};
group.addEventListener("click", select);
group.addEventListener("keydown", event => {
if (event.key === "Enter" || event.key === " ") select();
});
const rect = document.createElementNS(namespace, "rect");
rect.setAttribute("x", position.x); rect.setAttribute("y", position.y);
rect.setAttribute("width", nodeWidth); rect.setAttribute("height", nodeHeight);
const title = document.createElementNS(namespace, "title"); title.textContent = node.label;
const text = document.createElementNS(namespace, "text");
text.setAttribute("x", position.x + 10); text.setAttribute("y", position.y + 27);
const order = node.paint_order ? `${node.paint_order}. ` : "";
const label = `${order}${node.label}`;
text.textContent = label.length > 24 ? `${label.slice(0, 23)}` : label;
group.append(rect, title, text); svg.append(group);
rect.setAttribute("x", position.x);
rect.setAttribute("y", position.y);
rect.setAttribute("width", nodeWidth);
rect.setAttribute("height", nodeHeight);
group.append(rect);
const lines = [
node.name || node.label,
execution ? `${captureMilliseconds(execution.duration_ns)} · worker ${execution.worker_id}` : "not executed",
analysis ? `wait ${captureMilliseconds(analysis.scheduler_wait_ns)} · critical ${(analysis.critical_path_contribution * 100).toFixed(1)}%` : "wait — · critical —",
historical ? `historical p95 ${captureMilliseconds(historical.p95_ns)}` : "historical p95 —"
];
lines.forEach((line, index) => {
const text = document.createElementNS(namespace, "text");
text.setAttribute("x", position.x + 10);
text.setAttribute("y", position.y + 20 + index * 19);
text.textContent = line.length > 34 ? `${line.slice(0, 33)}` : line;
group.append(text);
});
svg.append(group);
}
return svg;
}
function graphGroup(title, graph, key, sceneGraph = false, open = false) {
const section = document.createElement("details"); section.className = "control-group graph-group";
prepareGroup(section, key, open);
const summary = document.createElement("summary");
const label = document.createElement("span"); label.textContent = title;
const count = document.createElement("small");
count.textContent = `${graph.nodes?.length || 0} 节点 · ${graph.edges?.length || 0} 连线`;
summary.append(label, count);
const canvas = document.createElement("div"); canvas.className = "task-graph-canvas";
canvas.append(taskGraphSvg(graph, sceneGraph));
section.append(summary, canvas);
function captureTimeline(frame, plan) {
const section = captureSection("Worker Timeline", "capture-timeline-section");
if (!frame) {
section.append("Select a captured frame to inspect worker execution.");
return section;
}
const namespace = "http://www.w3.org/2000/svg";
const executions = (frame.node_executions || []).filter(item => item.end_offset_ns >= item.start_offset_ns);
const workers = [...new Set(executions.map(item => item.worker_id))].sort((a, b) => a - b);
const names = new Map((plan?.nodes || []).map(item => [Number(item.node_id), item.name]));
const left = 88, timelineWidth = 820, rowHeight = 42;
const duration = Math.max(1, Number(frame.render_duration_ns || 0));
const svg = document.createElementNS(namespace, "svg");
svg.classList.add("capture-timeline");
svg.setAttribute("viewBox", `0 0 ${left + timelineWidth + 20} ${42 + workers.length * rowHeight}`);
for (let tick = 0; tick <= 5; ++tick) {
const x = left + timelineWidth * tick / 5;
const line = document.createElementNS(namespace, "line");
line.setAttribute("x1", x); line.setAttribute("x2", x);
line.setAttribute("y1", 28); line.setAttribute("y2", 40 + workers.length * rowHeight);
line.classList.add("capture-time-grid");
const label = document.createElementNS(namespace, "text");
label.setAttribute("x", x); label.setAttribute("y", 18);
label.classList.add("capture-time-label");
label.textContent = `${(duration * tick / 5 / 1e6).toFixed(2)} ms`;
svg.append(line, label);
}
workers.forEach((worker, row) => {
const y = 36 + row * rowHeight;
const label = document.createElementNS(namespace, "text");
label.setAttribute("x", 6); label.setAttribute("y", y + 21);
label.classList.add("capture-worker-label");
label.textContent = `Worker ${worker}`;
svg.append(label);
for (const execution of executions.filter(item => item.worker_id === worker)) {
const x = left + timelineWidth * Number(execution.start_offset_ns) / duration;
const width = Math.max(2, timelineWidth * Number(execution.duration_ns) / duration);
const group = document.createElementNS(namespace, "g");
group.classList.add("capture-time-block");
group.dataset.selected = String(Number(execution.node_id) === Number(activeCard.selectedCaptureNodeId));
const rect = document.createElementNS(namespace, "rect");
rect.setAttribute("x", x); rect.setAttribute("y", y);
rect.setAttribute("width", width); rect.setAttribute("height", 27);
const title = document.createElementNS(namespace, "title");
title.textContent = `${names.get(Number(execution.node_id)) || execution.node_id} · ${captureMilliseconds(execution.duration_ns)}`;
group.append(rect, title);
group.addEventListener("click", () => {
activeCard.selectedCaptureNodeId = Number(execution.node_id);
renderPerformanceCaptureMenu();
});
svg.append(group);
}
});
section.append(svg);
return section;
}
function renderTaskGraphMenu() {
if (!activeCard.taskGraph) {
elements.menuBody.textContent = "当前场景尚未返回任务图";
function captureNodeDetail(frame, plan, session) {
const section = captureSection("Node Detail", "capture-node-detail");
const nodeId = Number(activeCard.selectedCaptureNodeId);
const node = (plan?.nodes || []).find(item => Number(item.node_id) === nodeId);
const execution = (frame?.node_executions || []).find(item => Number(item.node_id) === nodeId);
const analysis = (frame?.analysis?.nodes || []).find(item => Number(item.node_id) === nodeId);
const historical = (session?.node_statistics || []).find(item => Number(item.node_id) === nodeId);
if (!node || !execution) {
section.append("Select a DAG node or timeline interval.");
return section;
}
const criticalFrequency = `${historical?.critical_path_frequency || 0} / ${session?.captured_count || 0}`;
section.append(captureMetricGrid([
["Node", `${node.owner} / ${node.name}`],
["Kind", node.kind],
["Current duration", captureMilliseconds(execution.duration_ns)],
["Scheduler wait", captureMilliseconds(analysis?.scheduler_wait_ns)],
["Worker", String(execution.worker_id)],
["Critical contribution", `${((analysis?.critical_path_contribution || 0) * 100).toFixed(2)}%`],
["Moving average", captureMilliseconds(historical?.moving_average_ns)],
["P95", captureMilliseconds(historical?.p95_ns)],
["P99", captureMilliseconds(historical?.p99_ns)],
["Critical frequency", criticalFrequency]
]));
const metrics = Object.entries(execution.metrics || {});
if (metrics.length) {
const heading = document.createElement("h4");
heading.textContent = "Renderable metrics";
section.append(heading, captureMetricGrid(metrics));
}
return section;
}
function captureSessionStatistics(session) {
const section = captureSection("Multi-frame Statistics");
if (!session?.captured_count) {
section.append("Statistics appear after captured frames complete.");
return section;
}
const summary = session.summary || {};
section.append(captureMetricGrid([
["Frames", `${session.captured_count} / ${session.requested_count}`],
["Total render avg", captureMilliseconds(summary.render_average_ns)],
["Total render p50", captureMilliseconds(summary.render_p50_ns)],
["Total render p95", captureMilliseconds(summary.render_p95_ns)],
["Total render max", captureMilliseconds(summary.render_maximum_ns)],
["Average parallelism", Number(summary.average_parallelism || 0).toFixed(2)],
["Peak parallelism", String(summary.peak_parallelism || 0)],
["Scheduler wait avg", captureMilliseconds(summary.scheduler_wait_average_ns)],
["Scheduler wait p95", captureMilliseconds(summary.scheduler_wait_p95_ns)]
]));
const frequency = document.createElement("div");
frequency.className = "capture-frequency-list";
frequency.textContent = (summary.critical_path_frequency || [])
.map(item => `#${item.node_id}: ${item.frequency}`).join(" · ") || "No critical-path samples";
section.append(frequency);
return section;
}
function planTopologyDifference(first, second) {
const firstNodes = new Set((first?.nodes || []).map(item => Number(item.node_id)));
const secondNodes = new Set((second?.nodes || []).map(item => Number(item.node_id)));
const edgeKey = edge => `${edge.from}>${edge.to}`;
const firstEdges = new Set((first?.edges || []).map(edgeKey));
const secondEdges = new Set((second?.edges || []).map(edgeKey));
return {
addedNodes: [...secondNodes].filter(value => !firstNodes.has(value)),
removedNodes: [...firstNodes].filter(value => !secondNodes.has(value)),
addedEdges: [...secondEdges].filter(value => !firstEdges.has(value)),
removedEdges: [...firstEdges].filter(value => !secondEdges.has(value))
};
}
function planNodeList(ids, plan) {
const nodes = new Map((plan?.nodes || []).map(node => [Number(node.node_id), node]));
return ids.map(id => {
const node = nodes.get(Number(id));
return node ? `#${id} ${node.owner} / ${node.name}` : `#${id}`;
}).join(", ") || "none";
}
function planCacheChanges(first, second) {
const before = new Map((first?.renderables || []).map(item => [Number(item.owner_id), item]));
const after = new Map((second?.renderables || []).map(item => [Number(item.owner_id), item]));
const owners = new Set([...before.keys(), ...after.keys()]);
const changes = [];
for (const owner of owners) {
const left = before.get(owner);
const right = after.get(owner);
const leftState = left ? `${left.prepare_cache}/${left.paint_cache}` : "absent";
const rightState = right ? `${right.prepare_cache}/${right.paint_cache}` : "absent";
if (leftState !== rightState)
changes.push(`${right?.name || left?.name || `Renderable ${owner}`}: ${leftState} -> ${rightState}`);
}
return changes.join("; ") || "unchanged";
}
function criticalNodeList(statistics, plan) {
const names = new Map((plan?.nodes || []).map(node => [Number(node.node_id), node.name]));
return [...(statistics?.nodes || [])]
.filter(node => Number(node.critical_path_frequency) > 0)
.sort((left, right) => right.critical_path_frequency - left.critical_path_frequency)
.map(node => `#${node.node_id} ${names.get(Number(node.node_id)) || "retired"} (${node.critical_path_frequency})`)
.join(", ") || "none";
}
function capturePlanComparison(context) {
const section = captureSection("Plan Version Comparison");
const statistics = context.session?.plan_statistics || [];
if (!statistics.length) {
section.append("Capture frames to compare render plan versions.");
return section;
}
const row = document.createElement("div");
row.className = "capture-plan-selectors";
const makeSelect = value => {
const select = document.createElement("select");
for (const item of statistics) {
const option = document.createElement("option");
option.value = item.render_plan_version;
option.textContent = `Plan v${item.render_plan_version}`;
option.selected = Number(value) === Number(item.render_plan_version);
select.append(option);
}
return select;
};
const firstDefault = activeCard.captureCompareFirst || statistics[0].render_plan_version;
const secondDefault = activeCard.captureCompareSecond || statistics.at(-1).render_plan_version;
const firstSelect = makeSelect(firstDefault), secondSelect = makeSelect(secondDefault);
const update = () => {
activeCard.captureCompareFirst = Number(firstSelect.value);
activeCard.captureCompareSecond = Number(secondSelect.value);
renderPerformanceCaptureMenu();
};
firstSelect.addEventListener("change", update);
secondSelect.addEventListener("change", update);
row.append(firstSelect, document.createTextNode(" versus "), secondSelect);
section.append(row);
const first = statistics.find(item => Number(item.render_plan_version) === Number(firstSelect.value));
const second = statistics.find(item => Number(item.render_plan_version) === Number(secondSelect.value));
const firstPlan = context.capture.plans.find(item => Number(item.version) === Number(firstSelect.value));
const secondPlan = context.capture.plans.find(item => Number(item.version) === Number(secondSelect.value));
const difference = planTopologyDifference(firstPlan, secondPlan);
section.append(captureMetricGrid([
["Frame count", `${first?.frame_count || 0}${second?.frame_count || 0}`],
["Render average", `${captureMilliseconds(first?.render_average_ns)}${captureMilliseconds(second?.render_average_ns)}`],
["Render p95", `${captureMilliseconds(first?.render_p95_ns)}${captureMilliseconds(second?.render_p95_ns)}`],
["Average parallelism", `${Number(first?.average_parallelism || 0).toFixed(2)}${Number(second?.average_parallelism || 0).toFixed(2)}`],
["Peak parallelism", `${first?.peak_parallelism || 0}${second?.peak_parallelism || 0}`],
["Scheduler wait", `${captureMilliseconds(first?.scheduler_wait_average_ns)}${captureMilliseconds(second?.scheduler_wait_average_ns)}`],
["First critical nodes", criticalNodeList(first, firstPlan)],
["Second critical nodes", criticalNodeList(second, secondPlan)],
["Added nodes", planNodeList(difference.addedNodes, secondPlan)],
["Removed nodes", planNodeList(difference.removedNodes, firstPlan)],
["Added edges", difference.addedEdges.join(", ") || "none"],
["Removed edges", difference.removedEdges.join(", ") || "none"],
["Cache pruning", planCacheChanges(firstPlan, secondPlan)]
]));
return section;
}
function renderPerformanceCaptureMenu() {
if (!activeCard) return;
const context = selectedCaptureContext();
const view = document.createElement("div");
view.className = "performance-capture-view";
const controls = captureSection("Capture Control", "capture-controls");
const buttons = document.createElement("div");
buttons.className = "capture-control-row";
const next = document.createElement("button");
next.type = "button"; next.textContent = "Capture next frame";
const count = document.createElement("input");
count.type = "number"; count.min = "1"; count.max = "1000"; count.value = "20";
const many = document.createElement("button");
many.type = "button"; many.textContent = "Capture N frames";
const request = (id, argument) => {
const payload = {action: id};
if (argument !== undefined) payload.argument = argument;
activeCard.send("gallery_action", payload);
setTimeout(() => activeCard?.requestFrame(performance.now(), true), 40);
};
next.addEventListener("click", () => request("capture_next_frame"));
many.addEventListener("click", () => request("capture_frames", Math.max(1, Number(count.value) || 20)));
const controller = context.capture.controller || {};
next.disabled = many.disabled = Boolean(controller.enabled);
buttons.append(next, count, many);
const activeSession = context.sessions.find(item => item.session_id === controller.session_id) ||
context.sessions.find(item => item.active);
const progress = document.createElement("div");
progress.className = "capture-progress";
const captured = activeSession?.captured_count || 0;
const requested = activeSession?.requested_count || 0;
progress.textContent = requested ? `captured ${captured} / ${requested}` : "Capture disabled";
progress.dataset.active = String(Boolean(controller.enabled || activeSession?.active));
controls.append(buttons, progress);
view.append(controls);
const browser = captureSection("Captured Frames", "capture-browser");
const selectors = document.createElement("div");
selectors.className = "capture-browser-selectors";
const sessionSelect = document.createElement("select");
for (const session of context.sessions) {
const option = document.createElement("option");
option.value = session.session_id;
option.textContent = `Session #${session.session_id} · ${session.captured_count}/${session.requested_count}`;
option.selected = session === context.session;
sessionSelect.append(option);
}
sessionSelect.addEventListener("change", () => {
activeCard.selectedCaptureSessionId = Number(sessionSelect.value);
activeCard.selectedCaptureFrameId = null;
activeCard.selectedCaptureNodeId = null;
renderPerformanceCaptureMenu();
});
selectors.append(sessionSelect);
const frames = document.createElement("div");
frames.className = "capture-frame-list";
for (const frame of context.session?.frames || []) {
const button = document.createElement("button");
button.type = "button";
button.dataset.selected = String(frame === context.frame);
button.textContent = `Frame #${frame.frame_id} Plan v${frame.render_plan_version} ${captureMilliseconds(frame.render_duration_ns)}`;
button.addEventListener("click", () => {
activeCard.selectedCaptureFrameId = frame.frame_id;
activeCard.selectedCaptureNodeId = null;
renderPerformanceCaptureMenu();
});
frames.append(button);
}
if (!context.sessions.length) selectors.append("No capture sessions yet.");
browser.append(selectors, frames);
view.append(browser);
if (context.plan) {
const cache = captureSection(`Renderable Cache · Plan v${context.plan.version}`, "capture-cache");
for (const renderable of context.plan.renderables || []) {
const badge = document.createElement("div");
badge.className = "capture-cache-card";
const name = document.createElement("strong");
const prepare = document.createElement("span");
const paint = document.createElement("span");
name.textContent = renderable.name;
prepare.textContent = `prepare cache: ${renderable.prepare_cache}`;
paint.textContent = `paint cache: ${renderable.paint_cache}`;
badge.append(name, prepare, paint);
cache.append(badge);
}
view.append(cache);
const dag = captureSection(`Render DAG · Plan v${context.plan.version}`, "capture-dag-section");
dag.append(captureDagSvg(context.plan, context.frame, context.session?.node_statistics));
view.append(dag, captureTimeline(context.frame, context.plan),
captureNodeDetail(context.frame, context.plan, context.session));
}
view.append(captureSessionStatistics(context.session), capturePlanComparison(context));
elements.menuBody.replaceChildren(view);
}
function renderRenderPlanMenu() {
if (!activeCard.renderPlan) {
elements.menuBody.textContent = "The scene has not compiled a render plan yet.";
return;
}
const view = document.createElement("div"); view.className = "task-graph-view";
view.append(graphGroup("场景依赖与绘制顺序", activeCard.taskGraph,
"scene", true, true));
for (const resource of activeCard.taskGraph.renderables || [])
view.append(graphGroup(resource.title, resource.graph,
`renderable:${resource.target}`));
const view = document.createElement("div");
view.className = "render-plan-view";
const cache = captureSection(`Renderable Cache · Plan v${activeCard.renderPlan.version || 0}`, "capture-cache");
for (const renderable of activeCard.renderPlan.renderables || []) {
const card = document.createElement("div");
card.className = "capture-cache-card";
const name = document.createElement("strong");
const prepare = document.createElement("span");
const paint = document.createElement("span");
name.textContent = renderable.name;
prepare.textContent = `prepare cache: ${renderable.prepare_cache}`;
paint.textContent = `paint cache: ${renderable.paint_cache}`;
card.append(name, prepare, paint);
cache.append(card);
}
const dag = captureSection(`Current Render DAG · Plan v${activeCard.renderPlan.version || 0}`, "capture-dag-section");
dag.append(captureDagSvg(activeCard.renderPlan, null, []));
view.append(cache, dag);
elements.menuBody.replaceChildren(view);
}
function renderMenuBody() {
@@ -977,7 +1374,8 @@ function renderMenuBody() {
else if (activeTab === "controls") renderGroups(activeCard.controls, renderControl);
else if (activeTab === "observer") renderObserverMenu();
else if (activeTab === "performance") renderPerformanceMenu();
else if (activeTab === "task_graph") renderTaskGraphMenu();
else if (activeTab === "performance_capture") renderPerformanceCaptureMenu();
else if (activeTab === "render_plan") renderRenderPlanMenu();
}
function buildCatalog(data) {
@@ -1005,8 +1403,19 @@ function connectCatalog() {
socket.addEventListener("open", () => socket.send(message("gallery_catalog")));
socket.addEventListener("message", event => {
if (typeof event.data !== "string") return;
try { const data = JSON.parse(event.data); if (data.type === "catalog") { buildCatalog(data); socket.close(); } else if (data.type === "error") throw new Error(data.message); }
catch (error) { setConnection("error", "目录解析失败"); toast(error.message, true); }
try {
const data = JSON.parse(event.data);
if (data.type === "catalog") {
buildCatalog(data);
socket.close();
} else if (data.type === "error") {
setConnection("error", "目录读取失败");
toast(data.message, true);
}
} catch (error) {
setConnection("error", "目录解析失败");
toast(error.message, true);
}
});
socket.addEventListener("error", () => { setConnection("error", `无法连接 ${socketUrl} · 自动重连`); });
socket.addEventListener("close", () => {
+2 -1
View File
@@ -50,7 +50,8 @@
<button type="button" data-tab="actions">专属 API</button>
<button type="button" data-tab="observer">内核观察器</button>
<button type="button" data-tab="performance">性能监测</button>
<button type="button" data-tab="task_graph">任务图</button>
<button type="button" data-tab="performance_capture">Performance Capture</button>
<button type="button" data-tab="render_plan">Render DAG</button>
</nav>
<div class="menu-refresh-bar">
<button id="menu-reset" type="button" title="清空当前控件的性能监测窗口">&#8634; 重置监测</button>
+1 -1
View File
@@ -80,7 +80,7 @@ button:hover { border-color: var(--accent); } button:active { transform: transla
.action-row { display:flex; align-items:center; justify-content:space-between; gap:13px; padding:10px 9px; border-top:1px solid #ffffff0e; }.action-copy { min-width:0; }.action-controls { display:flex; gap:7px; align-items:center; }.action-controls input { width:105px; }.action-button { padding:7px 10px; color:#07110f; border-color:var(--accent); background:var(--accent); font-size:10px; font-weight:700; }
.telemetry-grid { display:grid; grid-template-columns:minmax(165px,.7fr) 1fr; margin:0; border:1px solid var(--line); }.telemetry-grid dt,.telemetry-grid dd { margin:0; padding:8px 10px; border-bottom:1px solid var(--line); font:10px/1.4 ui-monospace,monospace; overflow-wrap:anywhere; }.telemetry-grid dt { color:var(--muted); background:#ffffff06; }.telemetry-grid dd { color:var(--accent); }
.descriptor-group .telemetry-grid { border:0; }
.task-graph-view { min-width:0; }.task-graph-canvas { overflow:auto; padding:8px; border-inline:1px solid var(--line); background:#090d13; }.task-graph-svg { display:block; width:100%; min-width:440px; height:auto; }.task-edge { fill:none; stroke:#6aa9ff; stroke-width:1.5; opacity:.72; }.task-edge[data-kind="display"] { stroke:#ffd166; stroke-dasharray:5 4; }.task-node rect { fill:#161d27; stroke:#455468; rx:5; }.task-node[data-kind="control"] rect { fill:#10251f; stroke:#45ddbe; }.task-node[data-kind="layer"] rect { fill:#211c11; stroke:#d8a94b; }.task-node text { fill:#e9eef5; font:10px/1 ui-monospace,monospace; letter-spacing:0; pointer-events:none; }.graph-group > summary small { white-space:nowrap; }
.performance-capture-view,.render-plan-view{display:grid;gap:12px;min-width:0}.capture-section{min-width:0;padding:12px;border:1px solid var(--line);border-radius:8px;background:#0b1017}.capture-section>h3{margin:0 0 10px;color:#dce7f4;font-size:12px;letter-spacing:.06em;text-transform:uppercase}.capture-section>h4{margin:12px 0 7px;color:#9fb2c8;font-size:11px}.capture-control-row,.capture-browser-selectors,.capture-plan-selectors{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.capture-control-row button,.capture-frame-list button,.capture-plan-selectors select,.capture-browser-selectors select{border:1px solid #41556e;border-radius:5px;background:#121b26;color:#e9eef5;padding:7px 9px}.capture-control-row input{width:78px;border:1px solid #41556e;border-radius:5px;background:#090e15;color:#fff;padding:7px}.capture-control-row button:disabled{opacity:.45}.capture-progress{margin-top:9px;color:#91a5bb;font:11px ui-monospace,monospace}.capture-progress[data-active="true"]{color:#45ddbe}.capture-frame-list{display:grid;gap:5px;margin-top:9px;max-height:190px;overflow:auto}.capture-frame-list button{text-align:left;font:11px ui-monospace,monospace}.capture-frame-list button[data-selected="true"]{border-color:#45ddbe;background:#10251f}.capture-cache{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:7px}.capture-cache>h3{grid-column:1/-1}.capture-cache-card{display:grid;gap:4px;padding:8px;border:1px solid #2a394c;border-radius:5px;background:#0e151e}.capture-cache-card strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.capture-cache-card span{color:#91a5bb;font:10px ui-monospace,monospace}.capture-dag-section,.capture-timeline-section{overflow:auto}.capture-dag,.capture-timeline{display:block;min-width:850px;width:100%;height:auto}.capture-dag-heading{fill:#91a5bb;font:bold 11px ui-monospace,monospace;letter-spacing:.12em}.capture-dag-edge{fill:none;stroke:#536a84;stroke-width:1.4;opacity:.75}.capture-dag-node{cursor:pointer;outline:none}.capture-dag-node rect{rx:6;fill:#151d28;stroke:#45566c;stroke-width:1.2}.capture-dag-node[data-kind="prepare"] rect{fill:#10241f;stroke:#2f8873}.capture-dag-node[data-kind="paint"] rect{fill:#132037;stroke:#477bc1}.capture-dag-node[data-kind="composite"] rect{fill:#2b2110;stroke:#b98a38}.capture-dag-node[data-critical="true"] rect{stroke:#ffbb4d;stroke-width:2}.capture-dag-node[data-selected="true"] rect{stroke:#ff668a;stroke-width:3}.capture-dag-node text{fill:#e9eef5;font:10px ui-monospace,monospace;pointer-events:none}.capture-dag-node text:nth-of-type(n+2){fill:#9fb2c8}.capture-time-grid{stroke:#29384a;stroke-width:1}.capture-time-label,.capture-worker-label{fill:#91a5bb;font:9px ui-monospace,monospace}.capture-time-label{text-anchor:middle}.capture-time-block rect{fill:#3c78c2;stroke:#74a8e8;rx:3;cursor:pointer}.capture-time-block[data-selected="true"] rect{fill:#c44869;stroke:#ff8ca8}.capture-metrics{display:grid;grid-template-columns:minmax(120px,1fr) minmax(140px,1.5fr);gap:1px;margin:0;background:#233044}.capture-metrics dt,.capture-metrics dd{margin:0;padding:7px;background:#0e151e}.capture-metrics dt{color:#91a5bb}.capture-metrics dd{color:#eef4fb;font:11px ui-monospace,monospace;overflow-wrap:anywhere}.capture-frequency-list{margin-top:8px;color:#9fb2c8;font:10px ui-monospace,monospace}.capture-plan-selectors{margin-bottom:9px}.capture-plan-selectors select,.capture-browser-selectors select{min-width:145px}.capture-node-detail{border-color:#394c64}
.menu-footer { display:flex; justify-content:space-between; gap:14px; padding:10px 14px; border-top:1px solid var(--line); color:var(--muted); font-size:9px; }.menu-footer code { color:var(--accent); }
.toast { position:fixed; z-index:140; left:50%; bottom:22px; max-width:min(560px,calc(100vw - 30px)); padding:11px 15px; border:1px solid var(--strong); border-radius:8px; background:var(--panel2); box-shadow:0 18px 50px #00000073; transform:translateX(-50%); font-size:11px; }.toast[data-error="true"] { border-color:var(--danger); }
@media (max-width:980px) { .hero { grid-template-columns:1fr; }.gallery { grid-template-columns:1fr; }.mode-tabs button { flex-direction:column; align-items:flex-start; }.page-header { align-items:flex-start; flex-direction:column; }.page-description { text-align:left; } }