改方式

This commit is contained in:
2026-08-29 12:03:22 +08:00
parent 05a79fc351
commit 6045221174
18 changed files with 478 additions and 281 deletions
+65 -33
View File
@@ -56,7 +56,7 @@ type Gallery_Layout = {kind: "gallery_layout"; protocol: "aethera.gallery.video"
tile_width: number; tile_height: number; width: number; height: number; plots: Record<string, Gallery_Tile>};
type Gallery_Source_Metrics = {has_rendered_frame: boolean; logical_completion_rate_fps: number; rendered_frame_rate_fps: number;
logical_completion_count: number; rendered_frame_count: number; latest_completion_sequence: number;
latest_rendered_sequence: number; latest_rendered_clock_sequence: number; clock_lag_ticks: number};
latest_rendered_sequence: number; latest_rendered_clock_sequence: number; frame_lag: number};
type Gallery_Transport_Metrics = {kind: "gallery_metrics"; protocol: "aethera.gallery.video"; version: 2 | 3; clock_sequence: number;
encoded_frame_count: number; pixel_frame_count?: number; encoder_backend: "nvenc" | "vulkan_video" | "inactive";
target_frame_rate_fps: number; clock_delivery_rate_fps: number; sampled_frame_rate_fps?: number;
@@ -119,17 +119,19 @@ type Taskflow_Frame_Response = {protocol: "aethera.taskflow.frames"; version: 1;
captured: number; complete: boolean; frames: Taskflow_Frame_Trace[]; media_requested?: number;
media_captured?: number; media_remaining?: number};
type Gallery_Pipeline_State = Record<string, unknown>;
type Taskflow_Worker_State = {id: number; task_count: number; current_queue_size: number; current_queue_capacity: number;
type Taskflow_Worker_State = {id: number; task_count: number; entry_queue_size: number; entry_queue_capacity: number;
peak_queue_size: number; max_queue_capacity: number; active_task: {native_id: string; type: string; time_ns: number};
task_time_ns: number; busy_time_ns: number; cpu_time_ns: number; non_cpu_time_ns: number; idle_time_ns: number;
task_time_ns: number; busy_time_ns: number; cpu_time_ns: number; cooperative_wait_count: number;
cooperative_wait_time_ns: number; idle_time_ns: number;
min_task_time_ns: number; max_task_time_ns: number; utilization: number; cpu_utilization: number};
type Taskflow_Type_State = {name: string; count: number; total_time_ns: number; min_time_ns: number; max_time_ns: number};
type Taskflow_Runtime_State = {protocol: "aethera.taskflow.runtime"; version: 1; worker_count: number; active_topologies: number;
type Taskflow_Runtime_State = {protocol: "aethera.taskflow.runtime"; version: 2; worker_count: number; active_topologies: number;
active_taskflows: number; peak_active_taskflows: number; completed_taskflows: number; failed_taskflows: number;
active_tasks: number; peak_active_tasks: number; active_workers: number; peak_active_workers: number; observed_tasks: number;
named_tasks: number; peak_worker_queue_size: number; max_worker_queue_capacity: number;
longest_task: {native_id: string; name: string; type: string; time_ns: number}; total_task_time_ns: number;
worker_busy_time_ns: number; worker_cpu_time_ns: number; observed_wall_time_ns: number;
cooperative_wait_count: number; cooperative_wait_time_ns: number;
worker_utilization: number; worker_cpu_utilization: number;
task_types: Taskflow_Type_State[]; workers: Taskflow_Worker_State[]};
@@ -187,9 +189,11 @@ function use_selected_plot_diagnostics(plot: Plot | null) {
const sample = async () => {
try {
const [plot_response, gallery_response] = await Promise.all([
fetch(plot.diagnostics, {cache: "no-store"}),
fetch(plot.diagnostics, {cache: "no-store",
signal: AbortSignal.timeout(3000)}),
gallery_endpoint
? fetch(gallery_endpoint, {cache: "no-store"})
? fetch(gallery_endpoint, {cache: "no-store",
signal: AbortSignal.timeout(3000)})
: Promise.resolve(null)
]);
const diagnostics: unknown = await plot_response.json();
@@ -1214,7 +1218,8 @@ function Frame_Policy_Pane({plot, analysis, busy, on_refresh, on_update, on_manu
const [capture_busy, set_capture_busy] = useState(false);
const [capture_error, set_capture_error] = useState("");
const load_samples = useCallback(async () => {
const request = await fetch(plot.taskflow, {cache: "no-store"});
const request = await fetch(plot.taskflow, {cache: "no-store",
signal: AbortSignal.timeout(3000)});
const value = await request.json() as Taskflow_Frame_Response & {error?: string};
if (!request.ok) throw new Error(value.error ?? "读取帧策略状态失败");
set_response(value);
@@ -1227,9 +1232,15 @@ function Frame_Policy_Pane({plot, analysis, busy, on_refresh, on_update, on_manu
}, [load_samples]);
useEffect(() => {
if (!response || response.requested === 0 || response.complete) return;
const timer = window.setInterval(() => void load_samples().catch(failure =>
set_capture_error(failure instanceof Error ? failure.message : "读取帧策略状态失败")), 400);
return () => window.clearInterval(timer);
let stopped = false;
let timer = 0;
const poll = async () => {
try { await load_samples(); }
catch (failure) { set_capture_error(failure instanceof Error ? failure.message : "读取帧策略状态失败"); }
if (!stopped) timer = window.setTimeout(() => void poll(), 400);
};
void poll();
return () => { stopped = true; window.clearTimeout(timer); };
}, [load_samples, response?.requested, response?.complete]);
const capture = async () => {
set_capture_busy(true); set_capture_error(""); set_frame_index(0);
@@ -2413,7 +2424,8 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon
const [gallery_state, set_gallery_state] = useState<Gallery_Pipeline_State | null>(null);
const response = scene_response;
const read_trace = useCallback(async (endpoint: string) => {
const request = await fetch(endpoint);
const request = await fetch(endpoint, {
signal: AbortSignal.timeout(3000)});
const value = await request.json() as Taskflow_Frame_Response & {error?: string};
if (!request.ok) throw new Error(value.error ?? "读取 Taskflow 帧失败");
return value;
@@ -2422,7 +2434,8 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon
const group_id = new URL(plot.media, location.href).searchParams.get("group");
const [scene, gallery] = await Promise.all([
read_trace(plot.taskflow),
group_id ? fetch(`/gallery/${encodeURIComponent(group_id)}/diagnostics`, {cache: "no-store"})
group_id ? fetch(`/gallery/${encodeURIComponent(group_id)}/diagnostics`, {
cache: "no-store", signal: AbortSignal.timeout(3000)})
.then(async request => request.ok ? await request.json() as Gallery_Pipeline_State : null)
.catch(() => null) : Promise.resolve(null)
]);
@@ -2438,8 +2451,15 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon
}, [plot.taskflow]);
useEffect(() => {
if (!response || response.requested === 0 || response.complete) return;
const timer = window.setInterval(() => void load().catch(failure => set_error(failure instanceof Error ? failure.message : "读取 Taskflow 帧失败")), 400);
return () => window.clearInterval(timer);
let stopped = false;
let timer = 0;
const poll = async () => {
try { await load(); }
catch (failure) { set_error(failure instanceof Error ? failure.message : "读取 Taskflow 帧失败"); }
if (!stopped) timer = window.setTimeout(() => void poll(), 400);
};
void poll();
return () => { stopped = true; window.clearTimeout(timer); };
}, [load, response?.requested, response?.complete]);
const capture = async () => {
set_busy(true); set_error(""); set_frame_index(0); set_stage_key("");
@@ -2574,33 +2594,42 @@ function Taskflow_Runtime_Pane() {
const [state, set_state] = useState<Taskflow_Runtime_State | null>(null);
const [error, set_error] = useState("");
const load = useCallback(async () => {
const request = await fetch("/taskflow/diagnostics");
const request = await fetch("/taskflow/diagnostics", {
signal: AbortSignal.timeout(3000)});
const value = await request.json() as Taskflow_Runtime_State & {error?: string};
if (!request.ok) throw new Error(value.error ?? "读取 Taskflow 总体状态失败");
set_state(value); set_error("");
}, []);
useEffect(() => {
void load().catch(failure => set_error(failure instanceof Error ? failure.message : "读取 Taskflow 总体状态失败"));
const timer = window.setInterval(() => void load().catch(failure => set_error(failure instanceof Error ? failure.message : "读取 Taskflow 总体状态失败")), 1000);
return () => window.clearInterval(timer);
let stopped = false;
let timer = 0;
const poll = async () => {
try { await load(); }
catch (failure) { set_error(failure instanceof Error ? failure.message : "读取 Taskflow 总体状态失败"); }
if (!stopped) timer = window.setTimeout(() => void poll(), 1000);
};
void poll();
return () => { stopped = true; window.clearTimeout(timer); };
}, [load]);
return <section className="workspacePane"><header className="taskflowRuntimeHeader"><div><span className="eyebrow"></span><h2>Taskflow </h2><p> Observer Taskflow </p></div><button onClick={() => void load()}></button></header>
<div className="workspaceBody taskflowRuntimePane">{error ? <p className="taskflowError">{error}</p> : null}{state ? <>
<dl className="taskflowRuntimeSummary">
<div><dt>Worker</dt><dd>{state.worker_count}</dd></div><div title="Worker 位于最外层任务体内的累计墙钟占比,不等同于 CPU 利用率。"><dt></dt><dd>{state.worker_utilization.toFixed(1)}%</dd></div>
<div title=" Worker 最外层活跃区间累计的线程 CPU 时间占比。"><dt>线 CPU </dt><dd>{state.worker_cpu_utilization.toFixed(1)}%</dd></div>
<div title="从各 Worker 首次观察任务起读取的原生线程 CPU 时间占比,包含 Executor 调度开销。"><dt>线 CPU </dt><dd>{state.worker_cpu_utilization.toFixed(1)}%</dd></div>
<div><dt> Worker</dt><dd>{state.active_workers}/{state.worker_count}</dd></div><div><dt></dt><dd>{state.active_tasks}</dd></div>
<div><dt> Topology</dt><dd>{state.active_topologies}</dd></div><div><dt> Taskflow</dt><dd>{state.active_taskflows}</dd></div>
<div><dt></dt><dd>{state.observed_tasks.toLocaleString("zh-CN")}</dd></div><div><dt> Taskflow</dt><dd>{state.failed_taskflows}</dd></div>
<div><dt></dt><dd>{state.peak_worker_queue_size}</dd></div><div title={`${state.longest_task.name || state.longest_task.native_id} · ${state.longest_task.type}`}><dt></dt><dd>{nanoseconds(state.longest_task.time_ns)}</dd></div>
<div title="外层任务通过 Task_Graph::corun/corun_until 挂起并让当前 Worker 执行其他就绪任务;该时长不计入任务体墙钟占用。"><dt></dt><dd>{state.cooperative_wait_count.toLocaleString("zh-CN")} · {nanoseconds(state.cooperative_wait_time_ns)}</dd></div>
</dl>
<p className="diagnosticNotice"> Worker CPU 使线 CPU OS Executor </p>
<section className="taskflowRuntimeSection"><header><strong>Worker </strong><span> GET </span></header><div className="taskflowWorkerGrid">{state.workers.map(worker => <article key={worker.id}>
<p className="diagnosticNotice"> Worker 线 CPU Worker 线 CPU Executor 线 CPU CPU </p>
<section className="taskflowRuntimeSection"><header><strong>Worker </strong><span> GET </span></header><div className="taskflowWorkerGrid">{state.workers.map(worker => <article key={worker.id}>
<header><strong>Worker {worker.id}</strong><span> {worker.utilization.toFixed(1)}% · CPU {worker.cpu_utilization.toFixed(1)}%</span></header>
<div className="taskflowUtilization" title={`任务体墙钟 ${worker.utilization.toFixed(1)}% / 线程 CPU ${worker.cpu_utilization.toFixed(1)}%`}><i style={{width: `${Math.min(100, worker.utilization)}%`}}/><b style={{width: `${Math.min(100, worker.cpu_utilization)}%`}}/></div>
<dl><div><dt></dt><dd>{worker.task_count}</dd></div><div><dt> /</dt><dd>{worker.current_queue_size}/{worker.peak_queue_size}</dd></div>
<dl><div><dt></dt><dd>{worker.task_count}</dd></div><div title="前一个数是当前活跃任务进入 Observer 时看到的 Worker 本地队列深度;空闲时显示 --。"><dt>/</dt><dd>{worker.active_task.time_ns ? worker.entry_queue_size : "--"}/{worker.peak_queue_size}</dd></div>
<div><dt></dt><dd>{nanoseconds(worker.max_task_time_ns)}</dd></div><div><dt></dt><dd>{worker.active_task.time_ns ? nanoseconds(worker.active_task.time_ns) : "空闲"}</dd></div>
<div><dt> CPU</dt><dd>{nanoseconds(worker.cpu_time_ns)}</dd></div><div><dt> CPU </dt><dd>{nanoseconds(worker.non_cpu_time_ns)}</dd></div></dl>
<div title="从该 Worker 首次进入任务起读取的原生线程 CPU。"><dt>线 CPU</dt><dd>{nanoseconds(worker.cpu_time_ns)}</dd></div><div><dt></dt><dd>{nanoseconds(worker.busy_time_ns)}</dd></div>
<div title="外层任务挂起期间 Worker 可以执行其他 Taskflow 任务;因此该值不是 Worker 空闲时间。"><dt></dt><dd>{worker.cooperative_wait_count.toLocaleString("zh-CN")} </dd></div><div><dt></dt><dd>{nanoseconds(worker.cooperative_wait_time_ns)}</dd></div></dl>
{worker.active_task.time_ns ? <code className="taskflowActiveTask" title={worker.active_task.native_id}>{worker.active_task.type} · {worker.active_task.native_id}</code> : null}
</article>)}</div></section>
<section className="taskflowRuntimeSection"><header><strong>Taskflow </strong><span> Observer TaskType </span></header>
@@ -2670,7 +2699,7 @@ const Plot_Card = memo(function Plot_Card({plot, selected, policy, gallery, on_p
<span>{gallery.pixel_surface ? "Canvas" : "解码"} {gallery.playback.frame_rate_fps.toFixed(1)} FPS</span>
<span> {metrics ? metrics.server_completion_ms.toFixed(1) : "--.-"} ms</span>
<span title={gallery.transport ? `采样 ${gallery.transport.compose_average_ms.toFixed(2)} ms · 发布 ${gallery.transport.publish_average_ms.toFixed(2)} ms` : "等待媒体流水线指标"}>{encoder_label} {gallery.transport ? (gallery.pixel_surface ? gallery.transport.pixel_pack_average_ms ?? 0 : gallery.transport.encode_average_ms).toFixed(1) : "--.-"} ms</span>
<span> {source_transport?.has_rendered_frame ? source_transport.clock_lag_ticks : "--"} tick</span>
<span title="本 Plot 最新逻辑完成序号减去最新真实画面序号;不再跨 Plot 比较全局时间轮 tick。"> {source_transport?.has_rendered_frame ? source_transport.frame_lag : "--"} </span>
</div>
</div>
</header>
@@ -2773,11 +2802,11 @@ function Gallery_Grid({plots, selected, policies, layout_scope, galleries, on_po
}
const workspace_layout_key = "aethera-flexlayout-v7";
const gallery_transport_key = "aethera-gallery-transport-v1";
const gallery_transport_key = "aethera-gallery-transport-v2";
function load_gallery_transport(): Gallery_Transport_Mode {
return localStorage.getItem(gallery_transport_key) === "ffmpeg"
? "ffmpeg" : "websocket_pixels";
return localStorage.getItem(gallery_transport_key) === "websocket_pixels"
? "websocket_pixels" : "ffmpeg";
}
const layout_labels: Record<I18nLabel, string> = {
[I18nLabel.Close_Tab]: "关闭标签",
@@ -2845,8 +2874,15 @@ function load_workspace_model() {
export function App() {
const [plots, set_plots] = useState<Plot[]>([]); const [category, set_category] = useState("全部"); const [selected, set_selected] = useState<Plot | null>(null);
const [gallery_transport, set_gallery_transport] = useState<Gallery_Transport_Mode>(load_gallery_transport);
const visible = useMemo(() => {
if (category === "2D" || category === "3D")
return plots.filter(plot => plot.dimension === category);
return plots;
}, [category, plots]);
use_selected_plot_diagnostics(selected);
const gallery_videos = use_gallery_videos(plots, gallery_transport);
/* 只订阅当前维度面板真正消费的 atlas。切到 2D 时不再在后台
* 同时编码四路 3D atlas,页面筛选本身就是媒体资源的权威需求源。 */
const gallery_videos = use_gallery_videos(visible, gallery_transport);
const [execution_policies, set_execution_policies] = useState<Plot_Execution_Policies>({});
const [schema, set_schema] = useState<Schema | null>(null);
const [schema_busy, set_schema_busy] = useState(false);
@@ -2890,16 +2926,12 @@ export function App() {
}, []);
const reset_frame_diagnostics = () => { if (selected) window.dispatchEvent(new CustomEvent("aethera-reset-frame-diagnostics", {detail: {plot_id: selected.id}})); };
const categories = ["全部", "2D", "3D"];
const visible = useMemo(() => {
if (category === "2D" || category === "3D") return plots.filter(plot => plot.dimension === category);
return plots;
}, [category, plots]);
const gallery = <section className="galleryPanel"><header className="topbar"><nav aria-label="图形维度筛选">{categories.map(value => <button key={value} className={category === value ? "active" : ""} onClick={() => set_category(value)}>{value}</button>)}</nav><div className="topbarActions">
{selected ? <span className="selectionName"> <strong>{selected.title}</strong></span> : <span className="muted"> DAG </span>}
<label className="transportSelector"><select value={gallery_transport} onChange={event => {
const value = event.target.value as Gallery_Transport_Mode;
localStorage.setItem(gallery_transport_key, value); set_gallery_transport(value);
}}><option value="websocket_pixels">WebSocket </option><option value="ffmpeg">FFmpeg H.264 / WebRTC</option></select></label>
}}><option value="ffmpeg">WebRTC H.264</option><option value="websocket_pixels">WebSocket </option></select></label>
<button onClick={() => { localStorage.removeItem(workspace_layout_key); localStorage.removeItem(gallery_layout_key);
set_layout_model(Model.fromJson(default_workspace_layout)); set_gallery_layout_revision(value => value + 1); }}></button></div></header>
<Gallery_Grid key={`${gallery_layout_revision}:${category}`} plots={visible} selected={selected} policies={execution_policies} layout_scope={category} galleries={gallery_videos} on_policy={update_execution_policy} on_select={set_selected}/></section>;