改方式
This commit is contained in:
+65
-33
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user