-
-
- 0.0
- 后端渲染 FPS
- 0.0
- 像素响应 FPS
- 0.0
- 浏览器呈现 FPS
- 0.00
- WS 往返 ms
- 0
- 未呈现覆盖
- 0.00
- Core 渲染 ms
- 0.00
- 像素编码 ms
- 0.0
- 响应负载 MB/s
- 0
- Kernel 待处理
- 0
- Kernel 丢弃
- 0→0
- 输入→绘制
- N/A
- 当前瓶颈
- none
- 观察事件
- 0
- 像素超时
- —
- 最近像素龄
From a39fe2bc4901d5ed47ca5aeb2c1d07313cea14e7 Mon Sep 17 00:00:00 2001
From: wyc <1104749580@qq.com>
Date: Tue, 11 Aug 2026 12:36:19 +0800
Subject: [PATCH] =?UTF-8?q?=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
webapp_gallery/app.js | 1082 ++++++++++++++++++++++---------------
webapp_gallery/index.html | 181 +------
webapp_gallery/styles.css | 675 ++++++++++++++++++++---
3 files changed, 1249 insertions(+), 689 deletions(-)
diff --git a/webapp_gallery/app.js b/webapp_gallery/app.js
index 869e862..6c75c16 100644
--- a/webapp_gallery/app.js
+++ b/webapp_gallery/app.js
@@ -1,52 +1,42 @@
"use strict";
-
-const $ = id => document.getElementById(id);
-const elements = {
- pages: $("pages"), pageTemplate: $("page-template"), cardTemplate: $("card-template"),
- connection: $("connection-status"), filters: $("category-filter"), modeTabs: $("mode-tabs"),
- streamToggle: $("toggle-streams"), pageCount: $("page-count"), caseCount: $("case-count"),
- canvasCount: $("canvas-count"), apiCount: $("api-count"), modeDescription: $("mode-description"),
- menu: $("context-menu"), menuTitle: $("menu-title"), menuComponent: $("menu-component"),
- menuDescription: $("menu-description"), menuBody: $("menu-body"), menuStatus: $("menu-status"),
- menuClose: $("menu-close"), menuTabs: [...document.querySelectorAll(".menu-tabs button")], toast: $("toast")
-};
-
const query = new URLSearchParams(location.search);
const hosted = location.protocol !== "file:" && ["/", "/index.html", "/gallery", "/gallery/"].includes(location.pathname);
const jetBrainsPreview = location.port === "63342";
const socketPort = query.get("port") || (hosted && !jetBrainsPreview ? location.port : "8848") || "8848";
const socketHost = query.get("host") || (hosted ? location.hostname : "127.0.0.1") || "127.0.0.1";
const socketUrl = `${location.protocol === "https:" ? "wss" : "ws"}://${socketHost}:${socketPort}/renderive/gallery`;
-
-const pages = new Map();
+const amisEmbed = window.amisRequire("amis/embed");
+const amisLib = window.amisRequire("amis");
+const React = window.amisRequire("react");
+const sessions = new Map();
+const rootNode = document.getElementById("root");
+const menuHost = document.getElementById("menu-host");
+let catalog = null;
let definitions = [];
let modes = [];
-let activeMode = "low_latency";
-let activeCategory = "全部";
-let activeCard = null;
-let activeTab = "controls";
+let mainScoped = null;
+let menuScoped = null;
+let menuSession = null;
+let selectedCategory = "全部";
let streamsPaused = false;
-let toastTimer = 0;
+let connectionState = "connecting";
+let connectionText = "读取后端目录";
+let noticeText = "";
+let noticeError = false;
+let noticeTimer = 0;
+let menuStatus = "字段、验证与 API 映射来自后端 JSON";
let lastAnimationFrameAt = 0;
let displayIntervalMs = 0;
let displayIntervalLatestMs = 0;
let displayIntervalP95Ms = 0;
let displayJitterMs = 0;
+let lastUiUpdateAt = 0;
const displayIntervalSamples = [];
-
const message = (type, payload = {}) => JSON.stringify({category: "event", type, ...payload});
-const setConnection = (state, text) => {
- elements.connection.dataset.state = state;
- elements.connection.querySelector("span").textContent = text;
-};
-function toast(text, error = false) {
- clearTimeout(toastTimer);
- elements.toast.textContent = text;
- elements.toast.dataset.error = String(error);
- elements.toast.hidden = false;
- toastTimer = setTimeout(() => { elements.toast.hidden = true; }, 2600);
-}
-function grouped(items) {
+const quote = value => JSON.stringify(String(value));
+const escapeHtml = value => String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
+const expression = path => `\${${path}}`;
+const groupItems = items => {
const groups = new Map();
for (const item of items || []) {
const group = item.group || "其他";
@@ -54,19 +44,37 @@ function grouped(items) {
groups.get(group).push(item);
}
return groups;
-}
-function flatten(value, prefix = "", result = []) {
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
- for (const [key, child] of Object.entries(value)) flatten(child, prefix ? `${prefix}.${key}` : key, result);
- } else result.push([prefix, typeof value === "object" ? JSON.stringify(value) : String(value)]);
- return result;
-}
+};
function formatNanoseconds(value) {
const nanoseconds = Math.max(0, Number(value) || 0);
if (nanoseconds < 1_000) return `${Math.round(nanoseconds)} ns`;
if (nanoseconds < 1_000_000) return `${(nanoseconds / 1_000).toFixed(2)} µs`;
return `${(nanoseconds / 1_000_000).toFixed(3)} ms`;
}
+function formatInteger(value) {
+ return Number(value || 0).toLocaleString();
+}
+function formatFps(value, digits = 1) {
+ return Number(value || 0).toFixed(digits);
+}
+function normalizeEventValue(payload, name) {
+ if (payload !== null && typeof payload === "object") {
+ if (Object.prototype.hasOwnProperty.call(payload, "value")) return payload.value;
+ if (Object.prototype.hasOwnProperty.call(payload, name)) return payload[name];
+ if (payload.data !== null && typeof payload.data === "object") return normalizeEventValue(payload.data, name);
+ }
+ return payload;
+}
+function notify(text, error = false) {
+ clearTimeout(noticeTimer);
+ noticeText = String(text || "");
+ noticeError = Boolean(error);
+ updateMainData();
+ noticeTimer = setTimeout(() => {
+ noticeText = "";
+ updateMainData();
+ }, 2600);
+}
function updateDisplayTiming(time) {
if (lastAnimationFrameAt > 0) {
displayIntervalLatestMs = Math.max(0, time - lastAnimationFrameAt);
@@ -86,14 +94,74 @@ function updateDisplayTiming(time) {
displayIntervalP95Ms = percentile(0.95);
displayJitterMs = Math.max(0, displayIntervalP95Ms - displayIntervalMs);
}
-
-class GalleryCard {
- constructor(definition, mode) {
+const observerMetricDefinitions = [
+ ["configured_frequency_hz", "配置频率"], ["observation_count", "观察次数"], ["latest_sequence", "最新序号"], ["produced_frame_count", "发布"],
+ ["consumed_frame_count", "完成"], ["pending_frame_count", "待处理"], ["dropped_frame_count", "丢弃"], ["failed_operation_count", "失败"],
+ ["target_interval_ns", "目标间隔"], ["paint_duration_ns", "PaintEvent"], ["render_duration_ns", "后台渲染"], ["bottleneck_duration_ns", "内部瓶颈"],
+ ["consumer_feedback_master_enabled", "消费者总开关"], ["consumer_feedback_enabled", "Kernel 反馈有效"], ["consumer_pixel_feedback_enabled", "像素响应反馈"], ["consumer_presentation_feedback_enabled", "浏览器呈现反馈"],
+ ["consumer_manual_feedback_enabled", "手动消费者反馈"], ["consumer_manual_fps", "手动消费者 FPS"], ["consumer_feedback_source", "生效反馈来源"], ["consumer_pixel_interval_ns", "像素响应周期"],
+ ["consumer_presentation_interval_ns", "浏览器呈现周期"], ["consumer_manual_interval_ns", "手动消费者周期"], ["consumer_sample_interval_ns", "消费者原始采样"], ["consumer_smoothed_interval_ns", "消费者平滑周期"],
+ ["consumer_variation_ns", "消费者抖动"], ["consumer_safety_interval_ns", "消费者安全期限"], ["consumer_interval_ns", "消费者限速周期"], ["consumer_effective_fps", "消费者限速 FPS"],
+ ["next_refresh_interval_ns", "下次刷新"], ["paint_lease_wait_ns", "Paint lease 等待"], ["paint_state_wait_ns", "Paint state 等待"], ["publish_state_wait_ns", "Publish state 等待"],
+ ["ready_wait_ns", "Ready 等待"], ["frame_age_at_render_ns", "渲染时帧龄"], ["render_lease_wait_ns", "Render lease 等待"], ["render_state_wait_ns", "Render state 等待"],
+ ["render_finish_state_wait_ns", "Render finish 等待"], ["queue_wait_ns", "Queue 等待"], ["end_to_end_ns", "端到端延迟"], ["last_event", "观察事件"]
+];
+const clientMetricDefinitions = [
+ ["display_interval_latest_ms", "RAF 最新周期"], ["display_interval_ms", "RAF 中位周期"], ["display_interval_p95_ms", "RAF P95 周期"], ["display_jitter_ms", "RAF P95-P50 抖动"],
+ ["frame_round_trip_ms", "WS 往返"], ["transport_fps", "像素响应 FPS"], ["presentation_fps", "浏览器呈现 FPS"], ["websocket_buffered_bytes", "WS 缓冲"],
+ ["overwritten_pixel_frames", "未呈现覆盖"], ["changed_pixel_frames", "变化像素帧"], ["duplicate_pixel_frames", "重复像素帧"], ["frame_request_timeout_count", "像素请求超时"],
+ ["last_pixel_receive_age_ms", "最近像素龄"], ["last_pixel_change_age_ms", "最近变化龄"]
+];
+const mainMetricDefinitions = [
+ ["backendFps", "后端渲染 FPS"], ["transportFps", "像素响应 FPS"], ["presentationFps", "浏览器呈现 FPS"], ["roundTrip", "WS 往返 ms"], ["overwritten", "未呈现覆盖"],
+ ["renderMs", "Core 渲染 ms"], ["encodeMs", "像素编码 ms"], ["bandwidth", "响应负载 MB/s"], ["pending", "Kernel 待处理"], ["dropped", "Kernel 丢弃"],
+ ["points", "输入→绘制"], ["limit", "当前瓶颈"], ["event", "观察事件"], ["timeouts", "像素超时"], ["pixelAge", "最近像素龄"]
+];
+const limitDefinitions = [
+ ["limitFrequency", "Kernel 用户频率"], ["limitPaint", "Kernel PaintEvent"], ["limitRender", "Kernel 后台渲染"], ["limitConsumer", "消费者反馈"]
+];
+const nanosecondFields = new Set([
+ "paint_duration_ns", "render_duration_ns", "target_interval_ns", "bottleneck_duration_ns", "consumer_pixel_interval_ns", "consumer_presentation_interval_ns",
+ "consumer_manual_interval_ns", "consumer_sample_interval_ns", "consumer_smoothed_interval_ns", "consumer_variation_ns", "consumer_safety_interval_ns", "consumer_interval_ns",
+ "next_refresh_interval_ns", "paint_lease_wait_ns", "paint_state_wait_ns", "publish_state_wait_ns", "ready_wait_ns", "frame_age_at_render_ns", "render_lease_wait_ns",
+ "render_state_wait_ns", "render_finish_state_wait_ns", "queue_wait_ns", "end_to_end_ns"
+]);
+const booleanObserverFields = new Set([
+ "consumer_feedback_master_enabled", "consumer_feedback_enabled", "consumer_pixel_feedback_enabled", "consumer_presentation_feedback_enabled", "consumer_manual_feedback_enabled"
+]);
+function observerValue(name, observer) {
+ const raw = observer[name] ?? (name === "last_event" || name === "consumer_feedback_source" ? "none" : 0);
+ if (name === "consumer_effective_fps") {
+ const interval = Number(observer.consumer_interval_ns || 0);
+ return interval > 0 ? `${(1e9 / interval).toFixed(2)} FPS` : "0 FPS";
+ }
+ if (name === "consumer_manual_fps") return `${Number(raw || 0).toFixed(2)} FPS`;
+ if (name === "consumer_feedback_source") {
+ const names = {disabled: "总开关关闭", none: "无", pixel: "像素响应", presentation: "浏览器呈现", manual: "手动"};
+ return String(raw).split("+").map(value => names[value] || value).join(" + ");
+ }
+ if (booleanObserverFields.has(name)) return raw ? "启用" : "关闭";
+ if (nanosecondFields.has(name)) return formatNanoseconds(raw);
+ if (name === "configured_frequency_hz") return observer.frequency_limit_enabled === false ? "已关闭" : `${Number(raw || 0).toLocaleString()} Hz`;
+ return typeof raw === "number" ? raw.toLocaleString() : String(raw);
+}
+function clientValue(name, client) {
+ const raw = Number(client[name] || 0);
+ if (name.endsWith("_fps")) return `${raw.toFixed(2)} FPS`;
+ if (name.endsWith("_ms")) return `${raw.toFixed(3)} ms`;
+ if (name === "websocket_buffered_bytes") return `${raw.toLocaleString()} B`;
+ return raw.toLocaleString();
+}
+class GallerySession {
+ constructor(key, definition, mode) {
+ this.key = key;
this.definition = definition;
this.mode = mode;
this.controls = [];
this.actions = [];
this.telemetry = {};
+ this.controlValues = new Map();
+ this.lastSubmittedControls = new Map();
this.frameCount = 0;
this.framePending = false;
this.frameTimeout = null;
@@ -109,6 +177,9 @@ class GalleryCard {
this.lastPixelReceivedAt = 0;
this.lastPixelChangeAt = 0;
this.motionState = "waiting";
+ this.motionText = "等待动态帧";
+ this.socketState = "connecting";
+ this.socketText = "等待可见";
this.reconnectTimer = null;
this.disposed = false;
this.ready = false;
@@ -118,54 +189,72 @@ class GalleryCard {
this.frameRoundTripMs = 0;
this.overwrittenPixelFrames = 0;
this.lastObserveRequest = 0;
- this.node = elements.cardTemplate.content.firstElementChild.cloneNode(true);
- this.node.dataset.category = definition.category;
- this.node.dataset.mode = mode.id;
- this.canvas = this.node.querySelector("canvas");
- this.shell = this.node.querySelector(".canvas-shell");
- this.context = this.canvas.getContext("2d", {alpha: false});
- this.socketState = this.node.querySelector(".card-socket");
- this.motionStatus = this.node.querySelector(".motion-status");
- this.observerFields = new Map([...this.node.querySelectorAll("[data-observer-field]")]
- .map(field => [field.dataset.observerField, field]));
- this.frameLabel = this.node.querySelector(".card-frames");
- this.node.querySelector(".card-category").textContent = definition.category;
- this.node.querySelector(".card-title").textContent = definition.title;
- this.node.querySelector(".card-description").textContent = definition.description;
- this.node.querySelector(".card-component").textContent = definition.component;
- this.node.querySelector(".card-controls").textContent = definition.control_count_by_mode?.[mode.id] ?? "—";
- this.node.querySelector(".card-actions").textContent = definition.action_count_by_mode?.[mode.id] ?? "—";
- const frameButton = this.node.querySelector(".frame-button");
- frameButton.textContent = mode.id === "manual" ? "手动刷新一帧" : mode.id === "playback" ? "消费下一帧" : "立即刷新";
- frameButton.addEventListener("click", () => this.requestFrame(performance.now(), true));
- this.node.querySelector(".open-menu").addEventListener("click", event => {
- const rect = event.currentTarget.getBoundingClientRect();
- openMenu(this, rect.right, rect.bottom);
- });
- this.node.addEventListener("contextmenu", event => {
+ this.shell = null;
+ this.canvas = null;
+ this.context = null;
+ this.eventAbort = null;
+ this.resizeObserver = null;
+ this.intersectionObserver = null;
+ this.lastResizeWidth = 0;
+ this.lastResizeHeight = 0;
+ }
+ attach(shell, canvas) {
+ this.shell = shell;
+ this.canvas = canvas;
+ this.context = canvas.getContext("2d", {alpha: false});
+ this.eventAbort = new AbortController();
+ const options = {signal: this.eventAbort.signal};
+ const pointer = (type, event) => this.send(type, {...this.position(event), button: ["left", "middle", "right"][event.button] || "none", buttons: event.buttons, modifiers: this.modifiers(event)});
+ canvas.addEventListener("pointermove", event => pointer("pointer_move", event), options);
+ canvas.addEventListener("pointerdown", event => {
+ if (event.button === 2) return;
+ shell.focus();
+ canvas.setPointerCapture(event.pointerId);
+ pointer("pointer_press", event);
+ }, options);
+ canvas.addEventListener("pointerup", event => {
+ if (event.button !== 2) pointer("pointer_release", event);
+ }, options);
+ canvas.addEventListener("pointerleave", () => this.send("leave"), options);
+ canvas.addEventListener("contextmenu", event => {
event.preventDefault();
- openMenu(this, event.clientX, event.clientY);
- });
- this.installCanvasEvents();
+ window.RenderiveGallery.openMenu(this.key);
+ }, options);
+ canvas.addEventListener("wheel", event => {
+ event.preventDefault();
+ this.send("wheel", {...this.position(event), pixelDeltaX: event.deltaX, pixelDeltaY: event.deltaY, angleDeltaX: -event.deltaX * 8, angleDeltaY: -event.deltaY * 8, buttons: event.buttons, modifiers: this.modifiers(event)});
+ }, {passive: false, signal: this.eventAbort.signal});
+ shell.addEventListener("keydown", event => this.send("key_press", {key: event.key, nativeKey: event.keyCode, repeat: event.repeat, modifiers: this.modifiers(event)}), options);
+ shell.addEventListener("keyup", event => this.send("key_release", {key: event.key, nativeKey: event.keyCode, repeat: false, modifiers: this.modifiers(event)}), options);
this.resizeObserver = new ResizeObserver(() => this.resize());
- this.resizeObserver.observe(this.shell);
+ this.resizeObserver.observe(shell);
this.intersectionObserver = new IntersectionObserver(entries => {
const intersecting = entries[0]?.isIntersecting === true;
if (this.intersecting === intersecting) return;
this.intersecting = intersecting;
this.syncActivity();
}, {rootMargin: "160px"});
- this.intersectionObserver.observe(this.node);
+ this.intersectionObserver.observe(shell);
+ this.syncActivity(true);
}
- setSocketState(state, text) {
- this.socketState.dataset.state = state;
- this.socketState.querySelector("span").textContent = text;
- }
- categoryVisible() {
- return activeCategory === "全部" || this.definition.category === activeCategory;
+ detach() {
+ this.eventAbort?.abort();
+ this.eventAbort = null;
+ this.resizeObserver?.disconnect();
+ this.resizeObserver = null;
+ this.intersectionObserver?.disconnect();
+ this.intersectionObserver = null;
+ this.intersecting = false;
+ this.lastResizeWidth = 0;
+ this.lastResizeHeight = 0;
+ this.shell = null;
+ this.canvas = null;
+ this.context = null;
+ if (this.ready && this.backendActive) this.send("hide");
+ this.backendActive = false;
}
displayVisible() {
- return !document.hidden && this.mode.id === activeMode && this.categoryVisible() && this.intersecting;
+ return !document.hidden && this.intersecting && this.shell !== null;
}
streamActive() {
return !streamsPaused && this.displayVisible();
@@ -188,29 +277,38 @@ class GalleryCard {
clearTimeout(this.reconnectTimer);
const socket = new WebSocket(socketUrl);
this.socket = socket;
+ this.socketState = "connecting";
+ this.socketText = "连接中";
socket.binaryType = "arraybuffer";
socket.addEventListener("open", () => {
if (this.socket !== socket) return;
- this.setSocketState("ready", "WS 已连接");
+ this.socketState = "ready";
+ this.socketText = "WS 已连接";
this.send("gallery_open", {case: this.definition.id, frame_mode: this.mode.id});
this.resize();
});
socket.addEventListener("message", event => {
- if (this.socket === socket)
- typeof event.data === "string" ? this.receiveJson(event.data) : this.receivePixels(event.data);
+ if (this.socket !== socket) return;
+ if (typeof event.data === "string") this.receiveJson(event.data);
+ else this.receivePixels(event.data);
});
socket.addEventListener("close", () => {
if (this.socket !== socket) return;
this.ready = false;
+ this.shell?.classList.remove("rv-canvas-ready");
this.backendActive = null;
this.framePending = false;
- clearTimeout(this.frameTimeout); this.frameTimeout = null;
- this.setSocketState("error", this.displayVisible() ? "连接关闭 · 自动重连" : "连接关闭 · 等待可见");
+ clearTimeout(this.frameTimeout);
+ this.frameTimeout = null;
+ this.socketState = "error";
+ this.socketText = this.displayVisible() ? "连接关闭 · 自动重连" : "连接关闭 · 等待可见";
this.updateMotionStatus();
if (!this.disposed && this.displayVisible()) this.reconnectTimer = setTimeout(() => this.connect(), 1000);
});
socket.addEventListener("error", () => {
- if (this.socket === socket) this.setSocketState("error", "连接错误 · 自动重连");
+ if (this.socket !== socket) return;
+ this.socketState = "error";
+ this.socketText = "连接错误 · 自动重连";
});
}
send(type, payload = {}) {
@@ -218,130 +316,43 @@ class GalleryCard {
}
receiveJson(raw) {
let data;
- try { data = JSON.parse(raw); } catch { toast(`${this.definition.title} 返回无效 JSON`, true); return; }
+ try {
+ data = JSON.parse(raw);
+ } catch {
+ notify(`${this.definition.title} 返回无效 JSON`, true);
+ return;
+ }
if (data.type === "error") {
- const detail = Object.values(data.field_errors || {})[0] || data.message;
- toast(detail || "后端拒绝操作", true);
- if (activeCard === this) elements.menuStatus.textContent = detail;
+ const detail = Object.values(data.field_errors || {})[0] || data.message || "后端拒绝操作";
+ this.lastSubmittedControls.clear();
+ notify(detail, true);
+ if (menuSession === this) {
+ menuStatus = detail;
+ renderMenu(this, true);
+ }
return;
}
if (data.type === "observer_state") {
this.telemetry = data.telemetry || {};
- this.updatePerformance();
- if (activeCard === this && ["observer", "performance"].includes(activeTab)) renderMenuBody();
return;
}
if (data.type !== "case_state") return;
this.controls = data.controls?.data || [];
this.actions = data.actions?.data || [];
this.telemetry = data.telemetry || {};
+ this.controlValues.clear();
+ for (const item of this.controls) this.controlValues.set(item.id, item.value);
+ this.lastSubmittedControls.clear();
this.ready = true;
+ this.shell?.classList.add("rv-canvas-ready");
this.backendActive = null;
- this.node.dataset.ready = "true";
+ this.socketState = "ready";
+ this.socketText = `${data.frame_mode?.strategy || "Core2"} 在线`;
this.syncActivity();
- this.node.querySelector(".card-controls").textContent = this.controls.length;
- this.node.querySelector(".card-actions").textContent = this.actions.length;
- this.setSocketState("ready", `${data.frame_mode?.strategy || "Core2"} 在线`);
- this.updatePerformance();
- if (data.notice && !data.notice.includes("已创建")) toast(`${this.definition.title}:${data.notice}`);
- if (activeCard === this) { elements.menuStatus.textContent = data.notice || "后端状态已回读"; renderMenuBody(); }
- }
- updatePerformance() {
- const performanceData = this.telemetry.performance || {};
- const observer = this.telemetry.kernel_observer || {};
- const dataShape = this.telemetry.data_shape || {};
- const limit = this.telemetry.low_latency_limit || {};
- const limitNames = {frequency_limited: "Kernel 频率受限", paint_limited: "PaintEvent 受限", render_limited: "后台渲染受限", consumer_limited: "消费者反馈受限", unlimited: "无限制", not_applicable: "N/A"};
- this.node.querySelector(".perf-fps").textContent = Number(performanceData.measured_fps || 0).toFixed(1);
- this.node.querySelector(".perf-transport-fps").textContent = Number(performanceData.pixel_response_fps || 0).toFixed(1);
- this.node.querySelector(".perf-present-fps").textContent = Number(this.presentationFps || 0).toFixed(1);
- this.node.querySelector(".perf-rtt").textContent = Number(this.frameRoundTripMs || 0).toFixed(2);
- this.node.querySelector(".perf-overwritten").textContent = this.overwrittenPixelFrames.toLocaleString();
- this.node.querySelector(".perf-render").textContent = Number(performanceData.last_render_ms || 0).toFixed(2);
- this.node.querySelector(".perf-encode").textContent = Number(performanceData.last_pixel_encode_ms || 0).toFixed(2);
- this.node.querySelector(".perf-bandwidth").textContent = Number(performanceData.pixel_payload_megabytes_per_second || 0).toFixed(1);
- this.node.querySelector(".perf-pending").textContent = observer.pending_frame_count ?? 0;
- this.node.querySelector(".perf-dropped").textContent = observer.dropped_frame_count ?? 0;
- this.node.querySelector(".perf-points").textContent = `${Number(dataShape.input_points || 0).toLocaleString()}→${Number(dataShape.rendered_elements || 0).toLocaleString()}`;
- const limitDurations = {frequency_limited: observer.target_interval_ns, paint_limited: observer.paint_duration_ns, render_limited: observer.render_duration_ns, consumer_limited: observer.consumer_interval_ns};
- const limitName = limitNames[limit.current] || limit.current || "N/A";
- const limitDuration = limitDurations[limit.current];
- const limitValue = limitDuration === undefined ? limitName : `${limitName} · ${formatNanoseconds(limitDuration)}`;
- const limitNode = this.node.querySelector(".perf-limit");
- limitNode.textContent = limitValue;
- limitNode.title = limitDuration === undefined ? limitName : `${limitName} · ${Number(limitDuration || 0).toLocaleString()} ns`;
- this.node.querySelector(".perf-event").textContent = observer.last_event || "none";
- this.node.querySelector(".perf-timeouts").textContent = this.frameTimeoutCount.toLocaleString();
- this.node.querySelector(".perf-pixel-age").textContent = this.lastPixelReceivedAt ? `${Math.max(0, performance.now() - this.lastPixelReceivedAt).toFixed(0)} ms` : "—";
- this.updateObserverDashboard(observer);
- this.updateClientDashboard(this.telemetry.client_performance || {});
- this.updateMotionStatus();
- const setLimitFlag = (selector, enabled, active, duration) => {
- const flag = this.node.querySelector(selector);
- flag.dataset.active = String(Boolean(active));
- flag.querySelector("b").textContent = enabled ? `${active ? "当前瓶颈" : "未受限"} · ${formatNanoseconds(duration)}` : `已关闭 · ${formatNanoseconds(duration)}`;
- };
- setLimitFlag(".limit-frequency", observer.frequency_limit_enabled !== false, limit.current === "frequency_limited", observer.target_interval_ns);
- setLimitFlag(".limit-paint", true, limit.current === "paint_limited", observer.paint_duration_ns);
- setLimitFlag(".limit-render", true, limit.current === "render_limited", observer.render_duration_ns);
- setLimitFlag(".limit-consumer", Boolean(observer.consumer_feedback_enabled), limit.current === "consumer_limited", observer.consumer_interval_ns);
- }
- updateObserverDashboard(observer) {
- const nanosecondFields = new Set([
- "paint_duration_ns", "render_duration_ns", "target_interval_ns",
- "bottleneck_duration_ns", "consumer_pixel_interval_ns", "consumer_presentation_interval_ns",
- "consumer_manual_interval_ns", "consumer_sample_interval_ns", "consumer_smoothed_interval_ns",
- "consumer_variation_ns", "consumer_safety_interval_ns", "consumer_interval_ns", "next_refresh_interval_ns", "paint_lease_wait_ns",
- "paint_state_wait_ns", "publish_state_wait_ns", "ready_wait_ns",
- "frame_age_at_render_ns", "render_lease_wait_ns", "render_state_wait_ns",
- "render_finish_state_wait_ns", "queue_wait_ns", "end_to_end_ns"
- ]);
- const booleanFields = new Set([
- "consumer_feedback_master_enabled", "consumer_feedback_enabled",
- "consumer_pixel_feedback_enabled", "consumer_presentation_feedback_enabled",
- "consumer_manual_feedback_enabled"
- ]);
- for (const [name, field] of this.observerFields) {
- const raw = observer[name] ?? (name === "limit_state" ? "not_applicable" :
- name === "last_event" || name === "consumer_feedback_source" ? "none" : 0);
- if (name === "consumer_effective_fps") {
- const interval = Number(observer.consumer_interval_ns || 0);
- field.textContent = interval > 0 ? `${(1e9 / interval).toFixed(2)} FPS` : "0 FPS";
- field.title = interval > 0 ? `${1e9 / interval} FPS` : "0 FPS";
- continue;
- }
- if (name === "consumer_manual_fps") {
- field.textContent = `${Number(raw || 0).toFixed(2)} FPS`;
- field.title = `${Number(raw || 0)} FPS`;
- continue;
- }
- if (name === "consumer_feedback_source") {
- const names = {disabled: "总开关关闭", none: "无", pixel: "像素响应",
- presentation: "浏览器呈现", manual: "手动"};
- field.textContent = String(raw).split("+").map(value => names[value] || value).join(" + ");
- field.title = String(raw);
- continue;
- }
- if (booleanFields.has(name)) {
- field.textContent = raw ? "启用" : "关闭";
- field.title = String(Boolean(raw));
- continue;
- }
- field.textContent = nanosecondFields.has(name) ? formatNanoseconds(raw) :
- name === "configured_frequency_hz" ? (observer.frequency_limit_enabled === false ? "已关闭" : `${Number(raw || 0).toLocaleString()} Hz`) :
- typeof raw === "number" ? raw.toLocaleString() : String(raw);
- field.title = nanosecondFields.has(name) ? `${Number(raw || 0).toLocaleString()} ns` : String(raw);
- }
- }
- updateClientDashboard(client) {
- for (const field of this.node.querySelectorAll("[data-client-field]")) {
- const name = field.dataset.clientField;
- const raw = Number(client[name] || 0);
- if (name.endsWith("_fps")) field.textContent = `${raw.toFixed(2)} FPS`;
- else if (name.endsWith("_ms")) field.textContent = `${raw.toFixed(3)} ms`;
- else if (name === "websocket_buffered_bytes") field.textContent = `${raw.toLocaleString()} B`;
- else field.textContent = raw.toLocaleString();
- field.title = String(raw);
+ if (data.notice && !data.notice.includes("已创建")) notify(`${this.definition.title}:${data.notice}`);
+ if (menuSession === this) {
+ menuStatus = data.notice || "后端状态已回读";
+ updateMenuData();
}
}
pixelSignature(buffer, width, height, stride) {
@@ -355,38 +366,26 @@ class GalleryCard {
}
return hash;
}
- updateMotionStatus(now = performance.now()) {
- let state = "waiting", label = "等待动态帧";
- if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) {
- state = "stalled"; label = "像素流断开";
- } else if (!this.streamActive()) {
- state = "waiting"; label = streamsPaused && this.displayVisible() ? "像素流已暂停" : "非活动视图";
- } else if (this.lastPixelChangeAt && now - this.lastPixelChangeAt < 1500) {
- state = "moving"; label = `画面变化 ${this.changedPixelFrames.toLocaleString()}`;
- } else if (this.lastPixelReceivedAt && now - this.lastPixelReceivedAt < 1500) {
- state = "duplicate"; label = `重复像素帧 ${this.duplicatePixelFrames.toLocaleString()}`;
- } else if (this.lastPixelReceivedAt) {
- state = "stalled"; label = "像素帧已停滞";
- }
- if (state !== this.motionState || this.motionStatus.querySelector("span").textContent !== label) {
- this.motionState = state;
- this.motionStatus.dataset.state = state;
- this.motionStatus.querySelector("span").textContent = label;
- }
- }
receivePixels(buffer) {
this.framePending = false;
- if (this.frameTimeout !== null) { clearTimeout(this.frameTimeout); this.frameTimeout = null; }
+ if (this.frameTimeout !== null) {
+ clearTimeout(this.frameTimeout);
+ this.frameTimeout = null;
+ }
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength < 16) {
- this.setSocketState("error", "像素帧无效 · 自动重连");
+ this.socketState = "error";
+ this.socketText = "像素帧无效 · 自动重连";
this.socket?.close(1003, "invalid pixel frame");
return;
}
const header = new DataView(buffer, 0, 16);
const magic = String.fromCharCode(...new Uint8Array(buffer, 0, 4));
- const width = header.getUint32(4, true), height = header.getUint32(8, true), stride = header.getUint32(12, true);
+ const width = header.getUint32(4, true);
+ const height = header.getUint32(8, true);
+ const stride = header.getUint32(12, true);
if (magic !== "RVP1" || !width || !height || stride < width * 4 || buffer.byteLength < 16 + stride * height) {
- this.setSocketState("error", "像素帧协议错误 · 自动重连");
+ this.socketState = "error";
+ this.socketText = "像素帧协议错误 · 自动重连";
this.socket?.close(1003, "invalid pixel frame");
return;
}
@@ -404,19 +403,21 @@ class GalleryCard {
}
if (this.latestPixelBuffer !== null) this.overwrittenPixelFrames++;
this.latestPixelBuffer = buffer;
- this.setSocketState("ready", `${this.mode.strategy || "Core2"} 在线`);
+ this.socketState = "ready";
+ this.socketText = `${this.mode.strategy || "Core2"} 在线`;
this.transportFps = this.recordRate(this.transportTimes, now);
this.frameCount++;
- this.frameLabel.textContent = this.frameCount.toLocaleString();
this.updateMotionStatus(now);
this.requestFrame(now);
}
presentLatest(time) {
const buffer = this.latestPixelBuffer;
- if (!buffer) return;
+ if (!buffer || !this.context || !this.canvas) return;
this.latestPixelBuffer = null;
const header = new DataView(buffer, 0, 16);
- const width = header.getUint32(4, true), height = header.getUint32(8, true), stride = header.getUint32(12, true);
+ const width = header.getUint32(4, true);
+ const height = header.getUint32(8, true);
+ const stride = header.getUint32(12, true);
if (this.canvas.width !== width || this.canvas.height !== height) {
this.canvas.width = width;
this.canvas.height = height;
@@ -424,7 +425,8 @@ class GalleryCard {
if (stride === width * 4) {
this.context.putImageData(new ImageData(new Uint8ClampedArray(buffer, 16, width * height * 4), width, height), 0, 0);
} else {
- const packed = new Uint8ClampedArray(width * height * 4), source = new Uint8Array(buffer, 16);
+ const packed = new Uint8ClampedArray(width * height * 4);
+ const source = new Uint8Array(buffer, 16);
for (let row = 0; row < height; row++) packed.set(source.subarray(row * stride, row * stride + width * 4), row * width * 4);
this.context.putImageData(new ImageData(packed, width, height), 0, 0);
}
@@ -440,9 +442,13 @@ class GalleryCard {
return this.currentRate(history, now);
}
resize() {
- if (this.socket?.readyState !== WebSocket.OPEN) return;
- const rect = this.shell.getBoundingClientRect();
- this.send("resize", {width: Math.max(240, Math.round(rect.width)), height: Math.max(180, Math.round(rect.height))});
+ if (this.socket?.readyState !== WebSocket.OPEN || !this.shell) return;
+ const width = Math.max(240, Math.round(this.shell.clientWidth));
+ const height = Math.max(180, Math.round(this.shell.clientHeight));
+ if (width === this.lastResizeWidth && height === this.lastResizeHeight) return;
+ this.lastResizeWidth = width;
+ this.lastResizeHeight = height;
+ this.send("resize", {width, height});
}
requestFrame(time, explicit = false) {
if ((!explicit && !this.streamActive()) || (explicit && !this.displayVisible()) || !this.ready || this.framePending || this.socket?.readyState !== WebSocket.OPEN) return;
@@ -456,7 +462,8 @@ class GalleryCard {
this.frameTimeout = null;
this.frameTimeoutCount++;
if (!this.streamActive() || this.socket?.readyState !== WebSocket.OPEN) return;
- this.setSocketState("error", "像素响应超时 · 正在恢复");
+ this.socketState = "error";
+ this.socketText = "像素响应超时 · 正在恢复";
this.syncActivity(true);
}, 1500);
}
@@ -465,256 +472,455 @@ class GalleryCard {
this.lastObserveRequest = time;
this.transportFps = this.currentRate(this.transportTimes, time);
this.presentationFps = this.currentRate(this.presentationTimes, time);
- this.send("gallery_observe", {client_metrics: {
+ this.send("gallery_observe", {client_metrics: this.clientMetrics(time)});
+ }
+ clientMetrics(time = performance.now()) {
+ return {
transport_fps: this.transportFps,
presentation_fps: this.presentationFps,
- websocket_buffered_bytes: this.socket.bufferedAmount || 0,
+ websocket_buffered_bytes: this.socket?.bufferedAmount || 0,
changed_pixel_frames: this.changedPixelFrames,
duplicate_pixel_frames: this.duplicatePixelFrames,
frame_request_timeout_count: this.frameTimeoutCount,
frame_round_trip_ms: this.frameRoundTripMs,
- display_interval_ms: displayIntervalMs,
display_interval_latest_ms: displayIntervalLatestMs,
+ display_interval_ms: displayIntervalMs,
display_interval_p95_ms: displayIntervalP95Ms,
display_jitter_ms: displayJitterMs,
overwritten_pixel_frames: this.overwrittenPixelFrames,
last_pixel_receive_age_ms: this.lastPixelReceivedAt ? Math.max(0, time - this.lastPixelReceivedAt) : 0,
last_pixel_change_age_ms: this.lastPixelChangeAt ? Math.max(0, time - this.lastPixelChangeAt) : 0
- }});
+ };
+ }
+ updateMotionStatus(now = performance.now()) {
+ let state = "waiting";
+ let label = "等待动态帧";
+ if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) {
+ state = "stalled";
+ label = "像素流断开";
+ } else if (!this.streamActive()) {
+ state = "waiting";
+ label = streamsPaused && this.displayVisible() ? "像素流已暂停" : "非活动视图";
+ } else if (this.lastPixelChangeAt && now - this.lastPixelChangeAt < 1500) {
+ state = "moving";
+ label = `画面变化 ${this.changedPixelFrames.toLocaleString()}`;
+ } else if (this.lastPixelReceivedAt && now - this.lastPixelReceivedAt < 1500) {
+ state = "duplicate";
+ label = `重复像素帧 ${this.duplicatePixelFrames.toLocaleString()}`;
+ } else if (this.lastPixelReceivedAt) {
+ state = "stalled";
+ label = "像素帧已停滞";
+ }
+ this.motionState = state;
+ this.motionText = label;
}
position(event) {
const rect = this.canvas.getBoundingClientRect();
return {x: (event.clientX - rect.left) * this.canvas.width / Math.max(1, rect.width), y: (event.clientY - rect.top) * this.canvas.height / Math.max(1, rect.height)};
}
- modifiers(event) { return (event.shiftKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.altKey ? 4 : 0) | (event.metaKey ? 8 : 0); }
- installCanvasEvents() {
- const pointer = (type, event) => this.send(type, {...this.position(event), button: ["left", "middle", "right"][event.button] || "none", buttons: event.buttons, modifiers: this.modifiers(event)});
- this.canvas.addEventListener("pointermove", event => pointer("pointer_move", event));
- this.canvas.addEventListener("pointerdown", event => { if (event.button !== 2) { this.shell.focus(); this.canvas.setPointerCapture(event.pointerId); pointer("pointer_press", event); } });
- this.canvas.addEventListener("pointerup", event => { if (event.button !== 2) pointer("pointer_release", event); });
- this.canvas.addEventListener("pointerleave", () => this.send("leave"));
- this.canvas.addEventListener("wheel", event => { event.preventDefault(); this.send("wheel", {...this.position(event), pixelDeltaX: event.deltaX, pixelDeltaY: event.deltaY, angleDeltaX: -event.deltaX * 8, angleDeltaY: -event.deltaY * 8, buttons: event.buttons, modifiers: this.modifiers(event)}); }, {passive: false});
- this.shell.addEventListener("keydown", event => this.send("key_press", {key: event.key, nativeKey: event.keyCode, repeat: event.repeat, modifiers: this.modifiers(event)}));
- this.shell.addEventListener("keyup", event => this.send("key_release", {key: event.key, nativeKey: event.keyCode, repeat: false, modifiers: this.modifiers(event)}));
+ modifiers(event) {
+ return (event.shiftKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.altKey ? 4 : 0) | (event.metaKey ? 8 : 0);
}
-}
-
-function createPage(mode) {
- const page = elements.pageTemplate.content.firstElementChild.cloneNode(true);
- page.dataset.mode = mode.id;
- page.querySelector(".page-strategy").textContent = mode.strategy;
- page.querySelector(".page-title").textContent = `${mode.title} · 全控件页`;
- page.querySelector(".page-description").textContent = mode.description;
- const gallery = page.querySelector(".gallery");
- const cards = definitions.map(definition => new GalleryCard(definition, mode));
- cards.forEach(card => gallery.append(card.node));
- elements.pages.append(page);
- pages.set(mode.id, {mode, page, cards});
- return pages.get(mode.id);
-}
-function syncCardActivity() {
- for (const page of pages.values()) for (const card of page.cards) card.syncActivity();
-}
-function selectMode(id) {
- activeMode = id;
- let selected = pages.get(id);
- if (!selected) selected = createPage(modes.find(mode => mode.id === id));
- for (const [modeId, page] of pages) page.page.hidden = modeId !== id;
- [...elements.modeTabs.children].forEach(button => button.classList.toggle("active", button.dataset.mode === id));
- elements.modeDescription.textContent = selected.mode.description;
- applyCategory();
- syncCardActivity();
- closeMenu();
-}
-function applyCategory() {
- const page = pages.get(activeMode);
- if (!page) return;
- for (const card of page.cards) {
- card.node.hidden = !card.categoryVisible();
- card.syncActivity();
+ commitControl(id, payload) {
+ const item = this.controls.find(control => control.id === id);
+ if (!item) return;
+ let value = normalizeEventValue(payload, id);
+ if (item.input === "number") value = Number(value);
+ else if (item.input === "boolean") value = Boolean(value);
+ else value = String(value ?? "");
+ if (item.input === "number" && (!Number.isFinite(value) || value < Number(item.minimum) || value > Number(item.maximum))) {
+ menuStatus = `${item.label} 超出后端允许范围`;
+ renderMenu(this, true);
+ return;
+ }
+ if (Object.is(this.lastSubmittedControls.get(id), value) || Object.is(this.controlValues.get(id), value)) return;
+ this.lastSubmittedControls.set(id, value);
+ this.send("gallery_patch", {patch: {[id]: value}});
+ menuStatus = `提交 ${item.api} · 等待后端回读`;
+ updateMenuData();
}
-}
-function installNavigation() {
- elements.modeTabs.replaceChildren(...modes.map(mode => {
- const button = document.createElement("button");
- button.type = "button"; button.dataset.mode = mode.id;
- button.innerHTML = `${mode.title}${mode.strategy}`;
- button.addEventListener("click", () => selectMode(mode.id));
- return button;
- }));
- const categories = ["全部", ...new Set(definitions.map(item => item.category))];
- elements.filters.replaceChildren(...categories.map(category => {
- const button = document.createElement("button");
- button.type = "button"; button.textContent = category;
- button.classList.toggle("active", category === activeCategory);
- button.addEventListener("click", () => {
- activeCategory = category;
- [...elements.filters.children].forEach(child => child.classList.toggle("active", child === button));
- applyCategory();
- });
- return button;
- }));
-}
-
-function openMenu(card, x, y) {
- if (!card.ready) { toast("该控件仍在等待后端描述", true); return; }
- activeCard = card; activeTab = "controls";
- elements.menuComponent.textContent = `${card.mode.strategy} / ${card.definition.component}`;
- elements.menuTitle.textContent = card.definition.title;
- elements.menuDescription.textContent = card.definition.description;
- elements.menuStatus.textContent = "菜单仅包含当前控件和当前帧策略可调用的 API";
- elements.menuTabs.forEach(button => button.classList.toggle("active", button.dataset.tab === activeTab));
- renderMenuBody();
- elements.menu.hidden = false;
- const rect = elements.menu.getBoundingClientRect(), gap = 10;
- elements.menu.style.left = `${Math.max(gap, Math.min(x, innerWidth - rect.width - gap))}px`;
- elements.menu.style.top = `${Math.max(gap, Math.min(y, innerHeight - rect.height - gap))}px`;
-}
-function closeMenu() { elements.menu.hidden = true; activeCard = null; }
-function renderControl(item) {
- const card = activeCard;
- const row = document.createElement("div"); row.className = "control-row";
- const copy = document.createElement("div"); copy.className = "control-copy";
- const label = document.createElement("label"); label.textContent = item.label;
- const api = document.createElement("code"); api.textContent = item.api; api.title = item.api; copy.append(label, api);
- let input;
- if (item.input === "select") {
- input = document.createElement("select");
- for (const value of item.options || []) { const option = document.createElement("option"); option.value = value; option.textContent = value; input.append(option); }
- input.value = String(item.value);
- } else {
- input = document.createElement("input"); input.type = item.input === "boolean" ? "checkbox" : item.input;
- if (item.input === "boolean") input.checked = Boolean(item.value); else input.value = item.value;
- if (item.input === "number") { input.min = item.minimum; input.max = item.maximum; input.step = item.step || "any"; }
+ runAction(id, payload) {
+ const item = this.actions.find(action => action.id === id);
+ if (!item) return;
+ const request = {action: item.id};
+ if (item.argument_input) {
+ const field = `action_${item.id}`;
+ let value = normalizeEventValue(payload, field);
+ if (item.argument_input === "number") value = Number(value);
+ request.argument = value;
+ }
+ this.send("gallery_action", request);
+ menuStatus = `执行 ${item.api}`;
+ updateMenuData();
+ if (["mode_render", "mode_dequeue", "mode_cycle"].includes(item.id)) setTimeout(() => this.requestFrame(performance.now(), true), 40);
}
- input.className = "control-input"; input.title = item.description || item.api;
- const submit = value => {
- card.send("gallery_patch", {patch: {[item.id]: value}});
- elements.menuStatus.textContent = `提交 ${item.api} · 等待后端回读`;
- };
- if (item.input === "number") {
- let committedValue = input.value;
- const commit = () => {
- const value = input.valueAsNumber;
- const minimum = Number(item.minimum);
- const maximum = Number(item.maximum);
- if (input.value === "" || !Number.isFinite(value) ||
- (Number.isFinite(minimum) && value < minimum) ||
- (Number.isFinite(maximum) && value > maximum)) {
- input.value = committedValue;
- return;
- }
- if (value === Number(committedValue)) {
- input.value = committedValue;
- return;
- }
- committedValue = input.value;
- item.value = value;
- submit(value);
+ controlData() {
+ const data = {};
+ for (const item of this.controls) data[item.id] = this.controlValues.get(item.id) ?? item.value;
+ for (const item of this.actions) if (item.argument_input) data[`action_${item.id}`] = item.argument_default;
+ return data;
+ }
+ snapshot() {
+ const performanceData = this.telemetry.performance || {};
+ const observer = this.telemetry.kernel_observer || {};
+ const dataShape = this.telemetry.data_shape || {};
+ const limit = this.telemetry.low_latency_limit || {};
+ const limitNames = {frequency_limited: "Kernel 频率受限", paint_limited: "PaintEvent 受限", render_limited: "后台渲染受限", consumer_limited: "消费者反馈受限", unlimited: "无限制", not_applicable: "N/A"};
+ const limitDurations = {frequency_limited: observer.target_interval_ns, paint_limited: observer.paint_duration_ns, render_limited: observer.render_duration_ns, consumer_limited: observer.consumer_interval_ns};
+ const limitName = limitNames[limit.current] || limit.current || "N/A";
+ const limitDuration = limitDurations[limit.current];
+ const client = this.clientMetrics();
+ const observerValues = {};
+ const clientValues = {};
+ observerValues.mode = observer.mode || this.mode.id;
+ observerValues.limit_state = observer.limit_state || "not_applicable";
+ for (const [name] of observerMetricDefinitions) observerValues[name] = observerValue(name, observer);
+ for (const [name] of clientMetricDefinitions) clientValues[name] = clientValue(name, client);
+ const limitTile = (enabled, active, duration) => ({text: enabled ? `${active ? "当前瓶颈" : "未受限"} · ${formatNanoseconds(duration)}` : `已关闭 · ${formatNanoseconds(duration)}`, active: Boolean(active)});
+ return {
+ ready: this.ready,
+ socketState: this.socketState,
+ socketText: this.socketText,
+ motionState: this.motionState,
+ motionText: this.motionText,
+ frameCount: formatInteger(this.frameCount),
+ backendFps: formatFps(performanceData.measured_fps),
+ transportFps: formatFps(performanceData.pixel_response_fps || this.transportFps),
+ presentationFps: formatFps(this.presentationFps),
+ roundTrip: Number(this.frameRoundTripMs || 0).toFixed(2),
+ overwritten: formatInteger(this.overwrittenPixelFrames),
+ renderMs: Number(performanceData.last_render_ms || 0).toFixed(2),
+ encodeMs: Number(performanceData.last_pixel_encode_ms || 0).toFixed(2),
+ bandwidth: Number(performanceData.pixel_payload_megabytes_per_second || 0).toFixed(1),
+ pending: formatInteger(observer.pending_frame_count),
+ dropped: formatInteger(observer.dropped_frame_count),
+ points: `${formatInteger(dataShape.input_points)}→${formatInteger(dataShape.rendered_elements)}`,
+ limit: limitDuration === undefined ? limitName : `${limitName} · ${formatNanoseconds(limitDuration)}`,
+ event: observer.last_event || "none",
+ timeouts: formatInteger(this.frameTimeoutCount),
+ pixelAge: this.lastPixelReceivedAt ? `${Math.max(0, performance.now() - this.lastPixelReceivedAt).toFixed(0)} ms` : "—",
+ limitFrequency: limitTile(observer.frequency_limit_enabled !== false, limit.current === "frequency_limited", observer.target_interval_ns),
+ limitPaint: limitTile(true, limit.current === "paint_limited", observer.paint_duration_ns),
+ limitRender: limitTile(true, limit.current === "render_limited", observer.render_duration_ns),
+ limitConsumer: limitTile(Boolean(observer.consumer_feedback_enabled), limit.current === "consumer_limited", observer.consumer_interval_ns),
+ observer: observerValues,
+ client: clientValues,
+ rawTelemetry: this.telemetry
};
- input.addEventListener("blur", commit);
- input.addEventListener("keydown", event => {
- if (event.key === "Enter") {
- event.preventDefault();
- commit();
- } else if (event.key === "Escape") {
- event.preventDefault();
- event.stopPropagation();
- input.value = committedValue;
- }
- });
- } else input.addEventListener("change", () => submit(item.input === "boolean" ? input.checked : input.value));
- row.append(copy, input); return row;
-}
-function renderAction(item) {
- const row = document.createElement("div"); row.className = "action-row";
- const copy = document.createElement("div"); copy.className = "action-copy";
- const label = document.createElement("strong"); label.textContent = item.label;
- const api = document.createElement("code"); api.textContent = item.api; api.title = item.api; copy.append(label, api);
- const controls = document.createElement("div"); controls.className = "action-controls";
- let argument = null;
- if (item.argument_input) { argument = document.createElement("input"); argument.className = "control-input"; argument.type = item.argument_input; argument.value = item.argument_default; controls.append(argument); }
- const button = document.createElement("button"); button.className = "action-button"; button.type = "button"; button.textContent = "执行";
- button.addEventListener("click", () => {
- const card = activeCard;
- const payload = {action: item.id};
- if (argument) payload.argument = item.argument_input === "number" ? Number(argument.value) : argument.value;
- card.send("gallery_action", payload);
- elements.menuStatus.textContent = `执行 ${item.api}`;
- if (["mode_render", "mode_dequeue", "mode_cycle"].includes(item.id)) setTimeout(() => card.requestFrame(performance.now(), true), 40);
- });
- controls.append(button); row.append(copy, controls); return row;
-}
-function renderGroups(items, renderer) {
- const fragment = document.createDocumentFragment();
- for (const [name, children] of grouped(items)) {
- const section = document.createElement("section"); section.className = "control-group";
- const title = document.createElement("h3"); title.textContent = name;
- section.append(title, ...children.map(renderer)); fragment.append(section);
}
- elements.menuBody.replaceChildren(fragment);
+ dispose() {
+ this.disposed = true;
+ clearTimeout(this.frameTimeout);
+ clearTimeout(this.reconnectTimer);
+ this.send("hide");
+ this.socket?.close();
+ this.detach();
+ }
}
-function renderData(value) {
- const list = document.createElement("dl"); list.className = "telemetry-grid";
- for (const [key, content] of flatten(value)) { const dt = document.createElement("dt"), dd = document.createElement("dd"); dt.textContent = key; dd.textContent = content; list.append(dt, dd); }
- elements.menuBody.replaceChildren(list);
+function sessionKey(modeId, caseId) {
+ return `${modeId}_${caseId}`;
}
-function renderMenuBody() {
- if (!activeCard) return;
- if (activeTab === "actions") renderGroups(activeCard.actions, renderAction);
- else if (activeTab === "observer") renderData(activeCard.telemetry.kernel_observer || {});
- else if (activeTab === "performance") renderData({performance: activeCard.telemetry.performance || {}, client_performance: activeCard.telemetry.client_performance || {}, low_latency_limit: activeCard.telemetry.low_latency_limit || {current: "not_applicable"}, data_shape: activeCard.telemetry.data_shape || {}, overlay: {enabled: activeCard.telemetry.performance_overlay_enabled, lines: activeCard.telemetry.performance_overlay_lines}, frame_mode: activeCard.telemetry.frame_mode});
- else renderGroups(activeCard.controls, renderControl);
+function ensureSession(key, definition, mode) {
+ if (!sessions.has(key)) sessions.set(key, new GallerySession(key, definition, mode));
+ return sessions.get(key);
+}
+function RenderiveCanvas(props) {
+ const shellRef = React.useRef(null);
+ const canvasRef = React.useRef(null);
+ const definition = definitions.find(item => item.id === props.caseId);
+ const mode = modes.find(item => item.id === props.modeId);
+ const session = ensureSession(props.cardKey, definition, mode);
+ React.useEffect(() => {
+ session.attach(shellRef.current, canvasRef.current);
+ return () => session.detach();
+ }, [session]);
+ return React.createElement("div", {ref: shellRef, className: `rv-canvas-shell ${session.ready ? "rv-canvas-ready" : ""}`, tabIndex: 0},
+ React.createElement("canvas", {ref: canvasRef, "aria-label": "Core2 后端像素画布"}),
+ React.createElement("div", {className: "rv-canvas-hint"}, "右键:本控件全部 API"),
+ React.createElement("div", {className: "rv-canvas-loading"}, React.createElement("small", null, "创建 Kernel Scene")));
+}
+amisLib.Renderer({test: /(^|\/)renderive-canvas$/})(RenderiveCanvas);
+function metricGrid(key, definitionsList, className, prefix = "sessions") {
+ return {
+ type: "grid",
+ className,
+ columns: definitionsList.map(([field, label]) => ({xs: 6, sm: 4, md: 2, body: {type: "tpl", tpl: `
${mode.description}
THREE REAL KERNEL STRATEGIES
标准 UI 由 AMIS SDK 渲染;实时二进制像素画布使用 React 自定义 Renderer。
${escapeHtml(api)}${session.mode.strategy} / ${session.definition.component}
${session.definition.description}
RENDERIVE · AMIS SDK
正在连接 WebSocket catalog。
CORE2 · KERNEL · WEBSOCKET
THREE REAL KERNEL STRATEGIES
-所有属性、动作、观察者和性能数据均由后端通过 WebSocket 返回。
-