This commit is contained in:
2026-08-27 17:35:12 +08:00
parent f0f358af24
commit bc0bb24aea
8 changed files with 243 additions and 88 deletions
+7
View File
@@ -162,6 +162,13 @@ Taskflow_Frame_Trace Render_Frame::take_taskflow_trace() {
result.markers.push_back(Frame_Trace_Point{
static_cast<Frame_Trace_Marker>(index), decode_present_value(encoded)});
}
for (std::size_t index = 0; index < measurement_count; ++index) {
const auto encoded = d->measurements[index].load(std::memory_order_acquire);
if (!encoded) continue;
result.measurements.push_back(Frame_Trace_Value{
static_cast<Frame_Trace_Measurement>(index),
decode_present_value(encoded)});
}
result.graphs = std::move(d->taskflow_graphs);
for (auto& worker : d->taskflow_workers) {
for (auto& task : worker.tasks)
+6
View File
@@ -11,6 +11,11 @@ struct Taskflow_Frame_Access;
struct Frame_Statistics_Sample;
enum struct Frame_Trace_Marker : std::uint8_t {
created,
plot_update_started,
plot_update_finished,
scene_render_entered,
scene_advance_started,
scene_advance_finished,
scene_render_requested,
scene_render_started,
event_dispatch_started,
@@ -119,6 +124,7 @@ struct Taskflow_Frame_Trace {
std::uint64_t created_time_unix_ns{}; /* 帧创建 Unix 时间,单位纳秒。 */
std::size_t worker_count{}; /* 捕获时全局 Executor 的 worker 数。 */
std::vector<Frame_Trace_Point> markers{}; /* 与该帧 DAG 共用时间原点的原始流水线时间点。 */
std::vector<Frame_Trace_Value> measurements{}; /* 本帧原始耗时测量;不另建统计副本。 */
std::vector<Taskflow_Graph_Trace> graphs{}; /* 本帧主动执行的业务 DAG 元信息。 */
std::vector<Taskflow_Task_Trace> tasks{}; /* 本帧窗口内原生 Observer 完成的任务执行。 */
};
+7
View File
@@ -176,6 +176,7 @@ TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity)
frame.mark(aethera::Frame_Trace_Marker::paint_frame_target_started);
frame.mark(aethera::Frame_Trace_Marker::paint_frame_target_finished);
frame.mark(aethera::Frame_Trace_Marker::paint_finished);
frame.record(aethera::Frame_Trace_Measurement::plot_update_ns, 125'000);
frame.request_taskflow_trace();
ASSERT_TRUE(aethera::detail::begin_taskflow_trace(frame));
aethera::detail::run_taskflow(frame_graph, frame, "test.scene.paint");
@@ -189,6 +190,12 @@ TEST(task_graph_observer, business_dag_and_native_execution_share_node_identity)
trace.markers,
aethera::Frame_Trace_Marker::paint_frame_target_started,
&aethera::Frame_Trace_Point::marker));
const auto plot_update = std::ranges::find(
trace.measurements,
aethera::Frame_Trace_Measurement::plot_update_ns,
&aethera::Frame_Trace_Value::measurement);
ASSERT_NE(plot_update, trace.measurements.end());
EXPECT_EQ(plot_update->value_ns, 125'000u);
EXPECT_EQ(trace.graphs.front().stage, "test.scene.paint");
EXPECT_TRUE(trace.graphs.front().completed);
EXPECT_EQ(std::ranges::count_if(trace.graphs.front().nodes, [](const auto& node) {
@@ -328,6 +328,10 @@ void Render_Scene_2D::Private::ensure_frame_taskflow(Object* object) {
template <Attached Object>
std::expected<void, Render_Scene_2D::Render_Result>
Render_Scene_2D::Private::render(Object* object, Frame_2D* frame) {
if (!frame)
throw std::invalid_argument(
"Render_Scene_2D requires a non-null external frame");
frame->mark(Frame_Trace_Marker::scene_render_entered);
Frame_Callback callback;
Frame_Callback retired_callback;
{
@@ -353,7 +357,9 @@ Render_Scene_2D::Private::render(Object* object, Frame_2D* frame) {
* no Taskflow is running. The completion callback only publishes
* results; it must not modify a graph that is still executing.
*/
frame->mark(Frame_Trace_Marker::scene_advance_started);
double_buffer::detail::Internal_Access::advance(object);
frame->mark(Frame_Trace_Marker::scene_advance_finished);
const Prop& prop = double_buffer::detail::Internal_Access::current_prop_layer<Render_Scene_2D::Base_Tag>(object);
if (!prop.view_active) {
release_admission();
@@ -281,6 +281,7 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) {
template <Attached Object>
Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object, Frame_3D* frame) {
if (!frame) throw std::invalid_argument("Render_Scene_3D requires a non-null external frame");
frame->mark(Frame_Trace_Marker::scene_render_entered);
{
std::lock_guard lock(render_mutex);
if (!frame_callback)
@@ -301,7 +302,9 @@ Render_Scene_3D::Render_Result Render_Scene_3D::Private::render(Object* object,
* 在创建或执行 frame_taskflow 前一次性推进完整 Scene,确保公共
* Render_Graph_Tag 已构建并且 frame DAG 引用的是同一权威运行图。
*/
frame->mark(Frame_Trace_Marker::scene_advance_started);
double_buffer::detail::Internal_Access::advance(object);
frame->mark(Frame_Trace_Marker::scene_advance_finished);
const Prop& prop = static_cast<const Prop&>(
double_buffer::detail::Internal_Access::current_prop(object));
if (!prop.view_active) return reject_frame(Render_Result::view_inactive);
+24 -9
View File
@@ -205,6 +205,10 @@ nlohmann::json taskflow_trace_json(
for (const auto& marker : trace.markers)
markers[magic_enum::enum_name(marker.marker)] =
static_cast<double>(marker.elapsed_ns) / 1'000'000.0;
nlohmann::json measurements = nlohmann::json::object();
for (const auto& measurement : trace.measurements)
measurements[magic_enum::enum_name(measurement.measurement)] =
static_cast<double>(measurement.value_ns) / 1'000'000.0;
nlohmann::json graphs = nlohmann::json::array();
std::unordered_map<std::uint64_t, std::string> node_ids;
for (const auto& graph : trace.graphs) {
@@ -274,6 +278,7 @@ nlohmann::json taskflow_trace_json(
{"created_time_unix_ns", trace.created_time_unix_ns},
{"worker_count", trace.worker_count},
{"markers", std::move(markers)},
{"measurements", std::move(measurements)},
{"graphs", std::move(graphs)},
{"executions", std::move(executions)}};
}
@@ -761,14 +766,32 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
try {
tick.width = streams.width;
tick.height = streams.height;
const std::uint64_t sequence = next_frame_sequence++;
const Frame_Identity identity{
sequence, tick.sequence == 0 ? sequence : tick.sequence};
Render_Frame* logical_frame{};
if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&managed->frame)) {
(*frame_2d)->begin(identity, Frame_2D::native_pixel_format);
logical_frame = frame_2d->get();
} else {
auto& frame_3d = std::get<std::unique_ptr<Frame_3D>>(managed->frame);
frame_3d->begin(identity,
pacing.video_enabled ? Frame_3D_Output::pixels
: Frame_3D_Output::diagnostics,
Frame_3D::native_pixel_format);
logical_frame = frame_3d.get();
}
taskflow_trace_claimed = mark_taskflow_trace(*logical_frame);
/*
* 各图的采样、网格构造和属性快照都在 Plot 自己的准备域完成。
* 各图的采样、网格构造和属性读取都在 Plot 自己的准备域完成。
* 进入 Scene::render 后只剩已经准备好的 Visual 批次与轻量提交;
* 共享 Render Domain 不承担业务数据生成。
*/
logical_frame->mark(Frame_Trace_Marker::plot_update_started);
const auto update_started = std::chrono::steady_clock::now();
view->update(tick);
const auto update_elapsed = std::chrono::steady_clock::now() - update_started;
logical_frame->mark(Frame_Trace_Marker::plot_update_finished);
const auto tick_queue_elapsed = tick.issued_at.time_since_epoch().count() == 0
? std::chrono::steady_clock::duration::zero()
: update_started - tick.issued_at;
@@ -783,12 +806,8 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
nanoseconds(update_elapsed));
};
const std::uint64_t sequence = next_frame_sequence++;
const Frame_Identity identity{sequence, tick.sequence == 0 ? sequence : tick.sequence};
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&scene)) {
auto& output = *std::get<std::unique_ptr<Frame_2D>>(managed->frame);
output.begin(identity, Frame_2D::native_pixel_format);
taskflow_trace_claimed = mark_taskflow_trace(output);
record_plot_measurements(output);
(*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(
Size{static_cast<int>(tick.width), static_cast<int>(tick.height)});
@@ -807,10 +826,6 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) {
return;
}
auto& output = *std::get<std::unique_ptr<Frame_3D>>(managed->frame);
output.begin(identity, pacing.video_enabled ? Frame_3D_Output::pixels
: Frame_3D_Output::diagnostics,
Frame_3D::native_pixel_format);
taskflow_trace_claimed = mark_taskflow_trace(output);
record_plot_measurements(output);
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(scene);
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{tick.width, tick.height});
+164 -52
View File
@@ -85,7 +85,8 @@ type Taskflow_Execution_Trace = {native_id: string; node_id: string; worker_id:
cpu_time_coarse: boolean; observer_entry_ms: number; observer_exit_ms: number;
observer_entry_cpu_ms: number; observer_exit_cpu_ms: number; queue_wait_ms: number};
type Taskflow_Frame_Trace = {sequence: number; correlation_id: number; created_time_unix_ns: number; worker_count: number;
markers?: Record<string, number>; graphs: Taskflow_Graph_Trace[]; executions: Taskflow_Execution_Trace[]};
markers?: Record<string, number>; measurements?: Record<string, number>;
graphs: Taskflow_Graph_Trace[]; executions: Taskflow_Execution_Trace[]};
type Taskflow_Frame_Response = {protocol: "aethera.taskflow.frames"; version: 1; requested: number; remaining: number;
captured: number; complete: boolean; frames: Taskflow_Frame_Trace[]; media_requested?: number;
media_captured?: number; media_remaining?: number};
@@ -1306,13 +1307,35 @@ function Taskflow_Node_Label({node, sample, summary, state, level}: {
<span> {sample ? milliseconds(sample.cooperative_wait_ms) : "--"} · {sample ? `${sample.cpu_time_coarse ? "≈ " : ""}${milliseconds(Math.max(0, duration - sample.cooperative_wait_ms - sample.cpu_duration_ms))}` : "--"}</span>
<span>Observer {sample ? milliseconds(sample.observer_entry_ms + sample.observer_exit_ms) : "--"}</span></>}
{Object.entries(node.attributes ?? {}).map(([key, value]) => <span key={key}>{key}{value}</span>)}
{state ? <details className="taskflowNodeState nodrag nopan"
onPointerDown={event => event.stopPropagation()} onClick={event => event.stopPropagation()}
onWheel={event => event.stopPropagation()}><summary> JSON</summary>
<pre>{JSON.stringify(state, null, 2)}</pre></details> : null}
</div>;
}
function Taskflow_Node_Inspector({node, execution, state, on_clear}: {
node?: Taskflow_Node_Trace; execution?: Taskflow_Execution_Trace;
state?: unknown; on_clear?: () => void;
}) {
if (!node) return <aside className="taskflowNodeInspector taskflowNodeInspectorEmpty">
<strong></strong><p>线</p>
</aside>;
return <aside className="taskflowNodeInspector"><header><div>
<strong>{taskflow_node_name(node.name, node)}</strong>
<span>{node.owner ? `${node.owner.label} · ${node.owner.component}` : "无组件归属"}</span>
</div>{on_clear ? <button type="button" onClick={on_clear}></button> : null}</header>
<section><h4></h4><p>{node.name}</p><code>{node.id}</code>
<dl><div><dt></dt><dd>{node.type}</dd></div>
<div><dt>Worker</dt><dd>{execution ? `W${execution.worker_id}` : "--"}</dd></div>
<div><dt></dt><dd>{execution ? `+${execution.ready_ms.toFixed(3)} ms` : "--"}</dd></div>
<div><dt></dt><dd>{execution ? `+${execution.started_ms.toFixed(3)} ms` : "--"}</dd></div>
<div><dt></dt><dd>{execution ? milliseconds(execution.duration_ms) : "--"}</dd></div>
<div><dt></dt><dd>{execution ? milliseconds(execution.queue_wait_ms) : "--"}</dd></div></dl>
</section>
<section><h4> Prop</h4><pre>{JSON.stringify(node.prop ?? {}, null, 2)}</pre></section>
<section><h4> State</h4><pre>{JSON.stringify(node.state ?? state ?? {}, null, 2)}</pre></section>
{Object.keys(node.attributes ?? {}).length ? <section><h4></h4>
<pre>{JSON.stringify(node.attributes, null, 2)}</pre></section> : null}
</aside>;
}
function taskflow_graph_analysis(graph: Taskflow_Graph_Trace, executions: Taskflow_Execution_Trace[]) {
const native_ids = new Set(graph.nodes.map(node => node.native_id));
const rows = executions.filter(value => native_ids.has(value.native_id));
@@ -1730,11 +1753,17 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
const [viewport_width, set_viewport_width] = useState(1000);
const [viewport_height, set_viewport_height] = useState(720);
const [local_fullscreen, set_local_fullscreen] = useState(false);
const [selected_node_id, set_selected_node_id] = useState("");
const fullscreen = controlled_fullscreen ?? local_fullscreen;
const set_fullscreen = (value: boolean) => on_fullscreen_change ? on_fullscreen_change(value) : set_local_fullscreen(value);
const displayed_graph = useMemo(() => expanded_taskflow_graph(graph), [graph]);
const selected_node = displayed_graph.nodes.find(node => node.id === selected_node_id);
const selected_execution = selected_node
? executions.find(value => value.native_id === selected_node.native_id) : undefined;
const selected_state = selected_node
? taskflow_node_state(selected_node, components, frame, gallery_state) : undefined;
useEffect(() => {
let cancelled = false;
const displayed_graph = expanded_taskflow_graph(graph);
const node_id = new Map(displayed_graph.nodes.map(node => [node.native_id, node.id]));
const execution = new Map(executions.map(value => [value.native_id, value]));
const analysis = taskflow_graph_analysis(displayed_graph, executions);
@@ -1772,7 +1801,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
},
children: displayed_graph.nodes.map(node => {
const attributes = Object.keys(node.attributes ?? {}).length;
const base_height = taskflow_node_state(node, components, frame, gallery_state) ? 266 : aggregate ? 198 : 188;
const base_height = aggregate ? 198 : 188;
return {id: node.id, width: 300, height: base_height + Math.min(attributes, 6) * 16};
}),
edges: flow_edges.map(edge => ({id: edge.id, sources: [edge.source], targets: [edge.target]}))
@@ -1803,7 +1832,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
set_edges(flow_edges);
});
return () => { cancelled = true; };
}, [graph, executions, frame, aggregate, components, gallery_state]);
}, [displayed_graph, executions, frame, aggregate, components, gallery_state]);
const copy_topology = async () => {
const native_ids = new Set(graph.nodes.map(node => node.native_id));
try {
@@ -1811,6 +1840,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
sequence: frame?.sequence,
correlation_id: frame?.correlation_id,
markers: frame?.markers ?? {},
measurements: frame?.measurements ?? {},
graph,
expanded_graph: expanded_taskflow_graph(graph),
/* 保留当前选中拓扑,同时把本帧其余真实 Task_Graph 一并复制。
@@ -1835,7 +1865,8 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
const flow = <ReactFlow key={viewport_key} nodes={nodes} edges={edges}
fitView fitViewOptions={{padding: .12, maxZoom: 1}} nodesDraggable={false} nodesConnectable={false}
zoomOnScroll={false} preventScrolling={false}
elementsSelectable={false} minZoom={.2} maxZoom={2.5}>
elementsSelectable minZoom={.2} maxZoom={2.5}
onNodeClick={(_, node) => set_selected_node_id(node.id)}>
<Background color="#233956" gap={22}/><MiniMap pannable zoomable/><Controls showInteractive={false}/>
</ReactFlow>;
return <section className={`taskflowDagSection${fullscreen ? " taskflowDagFullscreen" : ""}`}><header className="taskflowDagToolbar">
@@ -1858,19 +1889,23 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
<span><i className="taskflowLegendExecuted"/></span>
<span><i className="taskflowLegendWaiting"/></span></>}
</div>
</header>{fullscreen ? <div className="taskflowDag taskflowDagFill">{flow}</div> : <ResizableBox className="taskflowResizable taskflowDagResizable"
width={viewport_width} height={viewport_height} axis="both" minConstraints={[640, 420]}
maxConstraints={[12000, 12000]} resizeHandles={["e", "s", "se"]}
onResize={(_, data) => { set_viewport_width(data.size.width); set_viewport_height(data.size.height); }}>
<div className="taskflowDag">{flow}</div></ResizableBox>}</section>;
</header><div className="taskflowInspectionLayout">
<Taskflow_Node_Inspector node={selected_node} execution={selected_execution}
state={selected_state} on_clear={() => set_selected_node_id("")}/>
{fullscreen ? <div className="taskflowDag taskflowDagFill">{flow}</div> : <ResizableBox className="taskflowResizable taskflowDagResizable"
width={viewport_width} height={viewport_height} axis="both" minConstraints={[640, 420]}
maxConstraints={[12000, 12000]} resizeHandles={["e", "s", "se"]}
onResize={(_, data) => { set_viewport_width(data.size.width); set_viewport_height(data.size.height); }}>
<div className="taskflowDag">{flow}</div></ResizableBox>}
</div></section>;
}
type Taskflow_Timeline_Group = TimelineGroupBase & {
node_name: string; worker_id?: number; started_ms?: number; type?: string;
};
type Taskflow_Timeline_Phase = "topology" | "queue" | "observer_entry" | "body" | "observer_exit" | "completion_tail";
type Taskflow_Timeline_Phase = "lifecycle" | "topology" | "queue" | "observer_entry" | "body" | "observer_exit" | "completion_tail";
type Taskflow_Timeline_Item = TimelineItemBase<number> & {
phase: Taskflow_Timeline_Phase;
phase: Taskflow_Timeline_Phase; node_id?: string;
};
const taskflow_timeline_keys = {
groupIdKey: "id", groupTitleKey: "title", groupLabelKey: "title",
@@ -1886,8 +1921,9 @@ const taskflow_timeline_time_steps = {
* react-calendar-timeline 使用日历毫秒。把 1 帧内毫秒放大为 1 日历秒,
* 既保留微小任务的可缩放宽度,又始终以 Render_Frame 创建时刻为零点显示。
*/
function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_fullscreen, on_fullscreen_change, on_fullscreen_view_change, frame_navigation}: {
function Taskflow_Timeline({graph, executions, frame, components, gallery_state, fullscreen: controlled_fullscreen, on_fullscreen_change, on_fullscreen_view_change, frame_navigation}: {
graph: Taskflow_Graph_Trace; executions: Taskflow_Execution_Trace[]; frame?: Taskflow_Frame_Trace; fullscreen?: boolean;
components: Component[]; gallery_state?: Gallery_Pipeline_State | null;
on_fullscreen_change?: (value: boolean) => void; on_fullscreen_view_change?: (view: Taskflow_Fullscreen_View) => void;
frame_navigation?: Taskflow_Frame_Navigation;
}) {
@@ -1914,23 +1950,97 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
if (!(type_visibility[type] ?? type !== "condition")) return false;
return !hot_only || execution.duration_ms >= maximum_duration * hot_threshold_percent / 100;
}).sort((left, right) => left.started_ms - right.started_ms || left.worker_id - right.worker_id);
const groups: Taskflow_Timeline_Group[] = [{
id: "topology", title: <div className="taskflowTimelineGroupTitle"><strong>Topology</strong>
<span>{graph.stage}</span></div>, node_name: graph.name, type: "topology"
}];
const groups: Taskflow_Timeline_Group[] = [];
const items: Taskflow_Timeline_Item[] = [];
const encode_time = (milliseconds: number) => origin + milliseconds * scale;
const add_item = (id: string, group: string, phase: Taskflow_Timeline_Phase,
start: number, end: number, label: string, task_name: string, detail: string) => {
start: number, end: number, label: string, task_name: string, detail: string,
node_id?: string) => {
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return;
const visible_end = Math.max(end, start + .002);
const topology_offset = start - graph.submitted_ms;
items.push({id, group, phase, title: label,
items.push({id, group, phase, node_id, title: label,
start_time: encode_time(start), end_time: encode_time(visible_end),
canMove: false, canResize: false, canChangeGroup: false,
className: `taskflowTimelineItem taskflowTimelineItem-${phase}`,
itemProps: {title: `任务名称:${task_name}\n时间起点:+${start.toFixed(3)} ms(相对帧创建)\n相对 Topology${topology_offset >= 0 ? "+" : ""}${topology_offset.toFixed(3)} ms\n时间终点:+${end.toFixed(3)} ms\n持续时间:${milliseconds(Math.max(0, end - start))}\n${detail}`}});
};
const markers = frame?.markers ?? {};
const measurements = frame?.measurements ?? {};
const marker = (key: string) => markers[key];
const add_lifecycle_group = (id: string, title: string, detail: string) =>
groups.push({id, title: <div className="taskflowTimelineGroupTitle taskflowTimelineLifecycleTitle">
<strong>{title}</strong><span>{detail}</span></div>, node_name: id, type: "lifecycle"});
if (frame) {
add_lifecycle_group("lifecycle-plot", "Plot 调度与更新", "tick → 获得帧槽 → view.update");
const update_started = marker("plot_update_started") ?? 0;
const tick_queue = measurements.plot_tick_queue_ns ?? 0;
add_item("lifecycle-tick-queue", "lifecycle-plot", "lifecycle",
update_started - tick_queue, update_started,
`Tick 排队 ${milliseconds(tick_queue)}`, "Plot tick 排队", "时钟 tick 发出到 Plot 开始更新");
add_item("lifecycle-update", "lifecycle-plot", "lifecycle", update_started,
marker("plot_update_finished") ?? update_started,
`数据更新 ${milliseconds(measurements.plot_update_ns ?? 0)}`, "Plot view.update", "数据生成、属性写入和网格准备");
add_lifecycle_group("lifecycle-scene", "Scene render 准入", "render 入口 → advance → Taskflow 提交");
const scene_entered = marker("scene_render_entered") ?? marker("plot_update_finished") ?? graph.submitted_ms;
const advance_started = marker("scene_advance_started");
const advance_finished = marker("scene_advance_finished");
if (advance_started !== undefined) {
add_item("lifecycle-scene-admission", "lifecycle-scene", "lifecycle",
scene_entered, advance_started,
`Scene 准入 ${milliseconds(Math.max(0, advance_started - scene_entered))}`,
"Scene render 准入", "进入 Scene::render 到开始推进 Scene");
}
if (advance_started !== undefined && advance_finished !== undefined) {
add_item("lifecycle-scene-advance", "lifecycle-scene", "lifecycle",
advance_started, advance_finished,
`Scene advance ${milliseconds(advance_finished - advance_started)}`,
"Scene advance", "推进 Scene/Renderable 属性、状态和任务图");
}
const submit_started = advance_finished ?? advance_started ?? scene_entered;
add_item("lifecycle-scene-submit", "lifecycle-scene", "lifecycle",
submit_started, graph.submitted_ms,
`构图/提交 ${milliseconds(Math.max(0, graph.submitted_ms - submit_started))}`,
"Scene 构图与提交", "advance 完成到 Taskflow graph submitted");
add_lifecycle_group("lifecycle-render", "Scene CPU 阶段", "事件 → Prepare → Paint");
const lifecycle_intervals: Array<[string, string, string, string]> = [
["event_dispatch_started", "event_dispatch_finished", "事件分发", "event"],
["prepare_started", "prepare_finished", "Prepare", "prepare"],
["paint_started", "paint_finished", "Paint", "paint"]
];
lifecycle_intervals.forEach(([start_key, end_key, label, id]) => {
const start = marker(start_key); const end = marker(end_key);
if (start !== undefined && end !== undefined)
add_item(`lifecycle-${id}`, "lifecycle-render", "lifecycle", start, end,
`${label} ${milliseconds(end - start)}`, label, `${start_key}${end_key}`);
});
add_lifecycle_group("lifecycle-backend", "后端与 GPU", "准备 → 队列 → GPU → 回读");
const backend_intervals: Array<[string, string, string, string]> = [
["backend_prepare_started", "backend_prepare_finished", "后端准备", "backend-prepare"],
["backend_queue_entered", "backend_queue_left", "提交队列", "backend-queue"],
["gpu_submitted", "gpu_completed", "GPU 执行", "gpu"],
["readback_started", "readback_finished", "像素回读", "readback"]
];
backend_intervals.forEach(([start_key, end_key, label, id]) => {
const start = marker(start_key); const end = marker(end_key);
if (start !== undefined && end !== undefined)
add_item(`lifecycle-${id}`, "lifecycle-backend", "lifecycle", start, end,
`${label} ${milliseconds(end - start)}`, label, `${start_key}${end_key}`);
});
add_lifecycle_group("lifecycle-publish", "完成与发布", "Scene 完成 → callback → frame ready");
const callback_started = marker("callback_started");
const callback_finished = marker("callback_finished");
if (callback_started !== undefined && callback_finished !== undefined)
add_item("lifecycle-callback", "lifecycle-publish", "lifecycle", callback_started, callback_finished,
`完成回调 ${milliseconds(callback_finished - callback_started)}`, "完成帧回调",
`其中 Plot publish 测量 ${milliseconds(measurements.plot_publish_ns ?? 0)}`);
}
groups.push({id: "topology", title: <div className="taskflowTimelineGroupTitle"><strong>Topology</strong>
<span>{graph.stage}</span></div>, node_name: graph.name, type: "topology"});
add_item("topology", "topology", "topology", graph.submitted_ms,
graph.finished_ms, `Topology ${milliseconds(graph.finished_ms - graph.submitted_ms)}`,
graph.name || graph.stage, `${graph.stage} · Topology 墙钟`);
@@ -1965,33 +2075,37 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
if (diagnostic) {
add_item(`${group}-runtime`, group, "completion_tail", execution.started_ms,
execution.finished_ms, `${taskflow_node_name(node_name, node)} ${milliseconds(execution.duration_ms)}`,
node_name, diagnostic_detail);
node_name, diagnostic_detail, node?.id);
return;
}
add_item(`${group}-queue`, group, "queue", execution.ready_ms, execution.entered_ms,
`排队 ${milliseconds(execution.queue_wait_ms)}`, node_name, diagnostic_detail);
`排队 ${milliseconds(execution.queue_wait_ms)}`, node_name, diagnostic_detail, node?.id);
add_item(`${group}-entry`, group, "observer_entry", execution.entered_ms, execution.started_ms,
`entry ${milliseconds(execution.observer_entry_ms)}`, node_name, diagnostic_detail);
`entry ${milliseconds(execution.observer_entry_ms)}`, node_name, diagnostic_detail, node?.id);
add_item(`${group}-body`, group, "body", execution.started_ms, execution.finished_ms,
`执行 ${milliseconds(execution.duration_ms)}`, node_name, diagnostic_detail);
`执行 ${milliseconds(execution.duration_ms)}`, node_name, diagnostic_detail, node?.id);
add_item(`${group}-exit`, group, "observer_exit", execution.finished_ms, execution.completed_ms,
`exit ${milliseconds(execution.observer_exit_ms)}`, node_name, diagnostic_detail);
`exit ${milliseconds(execution.observer_exit_ms)}`, node_name, diagnostic_detail, node?.id);
});
const extent = Math.max(.1, graph.finished_ms,
...rows.flatMap(row => [row.completed_ms, row.finished_ms]));
const snapshots = [...new Map(rows.map(row => node_by_native_id.get(row.native_id))
.filter((node): node is Taskflow_Node_Trace => Boolean(node?.owner))
.map(node => [node.id, node])).values()];
return {groups, items, end: origin + extent * 1.06 * scale,
span: Math.max(.1, extent * 1.06) * scale, executions: rows.length,
last_completed, last_task_completed, snapshots};
}, [graph, executions, type_visibility, hot_only, hot_threshold_percent, selected_node_id]);
...rows.flatMap(row => [row.completed_ms, row.finished_ms]),
...items.map(item => (item.end_time - origin) / scale));
const earliest = Math.min(0, ...items.map(item => (item.start_time - origin) / scale));
return {groups, items, start: origin + earliest * 1.06 * scale,
end: origin + extent * 1.06 * scale,
span: Math.max(.1, (extent - earliest) * 1.06) * scale, executions: rows.length,
last_completed, last_task_completed};
}, [graph, executions, frame, type_visibility, hot_only, hot_threshold_percent, selected_node_id]);
const selected_node = graph.nodes.find(node => node.id === selected_node_id);
const selected_execution = selected_node
? executions.find(value => value.native_id === selected_node.native_id) : undefined;
const selected_state = selected_node
? taskflow_node_state(selected_node, components, frame, gallery_state) : undefined;
const timeline_view_key = `${graph.stage}:${graph.name}`;
const visible_range = visible_ranges[timeline_view_key];
const timeline_time_props = visible_range
? {visibleTimeStart: visible_range.start, visibleTimeEnd: visible_range.end}
: {defaultTimeStart: origin, defaultTimeEnd: model.end};
: {defaultTimeStart: model.start, defaultTimeEnd: model.end};
const copy_timeline = async () => {
const native_ids = new Set(graph.nodes.map(node => node.native_id));
try {
@@ -1999,6 +2113,7 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
sequence: frame?.sequence,
correlation_id: frame?.correlation_id,
markers: frame?.markers ?? {},
measurements: frame?.measurements ?? {},
graph,
timeline: {origin: "render_frame_created", submitted_ms: graph.submitted_ms, finished_ms: graph.finished_ms,
wall_time_ms: Math.max(0, graph.finished_ms - graph.submitted_ms), last_execution_completed_ms: model.last_completed,
@@ -2027,6 +2142,7 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
<button onClick={() => void copy_timeline()}>{copy_state}</button>
</div>
<div className="taskflowLegend" aria-label="Taskflow 时间轴颜色说明">
<span><i className="taskflowTimelineLegendLifecycle"/></span>
<span><i className="taskflowTimelineLegendTopology"/>Topology </span>
<span><i className="taskflowTimelineLegendQueue"/>Executor </span>
<span><i className="taskflowTimelineLegendObserver"/>Observer</span>
@@ -2047,19 +2163,9 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
onChange={event => set_hot_threshold_percent(Math.min(100, Math.max(1, Number(event.target.value) || 1)))}/>%
</label>
</div>
{selected_node ? <section className="taskflowSelectedCapture">
<header><div><strong>{taskflow_node_name(selected_node.name, selected_node)}</strong>
<span>{selected_node.owner ? `${selected_node.owner.label} · ${selected_node.owner.component}` : "无组件归属"}</span></div>
<button type="button" onClick={() => set_selected_node_id("")}></button></header>
<div><article><h4></h4><p>{selected_node.name}</p><code>{selected_node.id}</code></article>
<article><h4> Prop</h4><pre>{JSON.stringify(selected_node.prop ?? {}, null, 2)}</pre></article>
<article><h4> State</h4><pre>{JSON.stringify(selected_node.state ?? {}, null, 2)}</pre></article></div>
</section> : <div className="taskflowTimelineSelectionHint"> Prop / State</div>}
{model.snapshots.length ? <details className="taskflowTimelineSnapshots"><summary>线 / {model.snapshots.length}</summary>
<div>{model.snapshots.map(node => <article key={node.id}><strong>{taskflow_node_name(node.name, node)}</strong>
<span>{node.owner?.label} · {node.owner?.component}</span>
<pre>{JSON.stringify({prop: node.prop ?? {}, state: node.state ?? {}}, null, 2)}</pre></article>)}</div>
</details> : null}
<div className="taskflowInspectionLayout taskflowTimelineInspectionLayout">
<Taskflow_Node_Inspector node={selected_node} execution={selected_execution}
state={selected_state} on_clear={() => set_selected_node_id("")}/>
<div className="taskflowTimelineViewport">
<Timeline<Taskflow_Timeline_Item, Taskflow_Timeline_Group>
key={timeline_view_key} groups={model.groups} items={model.items} keys={taskflow_timeline_keys}
@@ -2072,7 +2178,11 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
lineHeight={line_height} itemHeightRatio={.68} itemVerticalGap={5}
minZoom={10} maxZoom={Math.max(model.span * 20, 1000)} buffer={1}
canMove={false} canResize={false} canChangeGroup={false}
canSelect={false} stackItems={false} traditionalZoom
canSelect stackItems={false} traditionalZoom
onItemSelect={item_id => {
const item = model.items.find(value => String(value.id) === String(item_id));
if (item?.node_id) set_selected_node_id(item.node_id);
}}
timeSteps={taskflow_timeline_time_steps}
groupRenderer={({group}) => group.title}>
<TimelineHeaders className="taskflowTimelineHeaders">
@@ -2081,7 +2191,7 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
<DateHeader unit="primaryHeader" labelFormat={() => "相对帧创建时刻的偏移"}/>
<DateHeader labelFormat={([start]) => {
const offset = (start.valueOf() - origin) / scale;
return `+${offset.toLocaleString("zh-CN", {maximumFractionDigits: 3})} ms`;
return `${offset >= 0 ? "+" : ""}${offset.toLocaleString("zh-CN", {maximumFractionDigits: 3})} ms`;
}}/>
</TimelineHeaders>
<TimelineMarkers>
@@ -2094,6 +2204,7 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
</TimelineMarkers>
</Timeline>
</div>
</div>
</section>;
}
@@ -2260,6 +2371,7 @@ function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Compon
on_fullscreen_view_change={switch_fullscreen_view} frame_navigation={frame_navigation}/></>
: displayed_view_mode === "timeline" && graph
? <Taskflow_Timeline graph={graph} executions={frame.executions} frame={frame}
components={components} gallery_state={gallery_state}
fullscreen={fullscreen_view === "timeline"} on_fullscreen_change={value => set_fullscreen_view(value ? "timeline" : null)}
on_fullscreen_view_change={switch_fullscreen_view} frame_navigation={frame_navigation}/>
: <div className="diagnosticEmpty"><strong> Taskflow </strong><span></span></div>}
+26 -27
View File
@@ -178,27 +178,23 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
.taskflowTimelineFilters input { accent-color: #5ce4c2; }
.taskflowHotThreshold input[type="range"] { width: 110px; }
.taskflowHotThreshold input[type="number"] { width: 54px; padding: 3px 5px; color: #dce8f8; border: 1px solid #29435e; border-radius: 5px; background: #08111e; }
.taskflowTimelineSelectionHint { padding: 8px 11px; color: #7189a8; border-left: 2px solid #29435e; font-size: 11px; }
.taskflowSelectedCapture { overflow: hidden; border: 1px solid #37607f; border-radius: 9px; background: #091321; }
.taskflowSelectedCapture > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 11px; border-bottom: 1px solid #203651; background: #0d1b2c; }
.taskflowSelectedCapture > header > div { display: grid; gap: 3px; }
.taskflowSelectedCapture > header strong { color: #dce8f8; }
.taskflowSelectedCapture > header span { color: #7189a8; font-size: 10px; }
.taskflowSelectedCapture > header button { padding: 5px 9px; color: #9db1cb; border: 1px solid #29435e; border-radius: 6px; background: #08111e; cursor: pointer; }
.taskflowSelectedCapture > div { display: grid; grid-template-columns: minmax(180px, .7fr) repeat(2, minmax(240px, 1fr)); }
.taskflowSelectedCapture article { min-width: 0; padding: 10px 11px; border-right: 1px solid #203651; }
.taskflowSelectedCapture article:last-child { border-right: 0; }
.taskflowSelectedCapture h4 { margin: 0 0 7px; color: #8fa7c4; font-size: 10px; font-weight: 600; }
.taskflowSelectedCapture p { margin: 0 0 6px; color: #dce8f8; font: 11px/1.4 ui-monospace, monospace; }
.taskflowSelectedCapture code { color: #6f87a6; font: 9px/1.4 ui-monospace, monospace; overflow-wrap: anywhere; }
.taskflowSelectedCapture pre { max-height: 210px; overflow: auto; margin: 0; color: #b9cce3; font: 10px/1.45 ui-monospace, monospace; }
.taskflowTimelineSnapshots { padding: 9px 11px; color: #91a5c0; border: 1px solid #29435e; border-radius: 8px; background: #091321; }
.taskflowTimelineSnapshots > summary { cursor: pointer; }
.taskflowTimelineSnapshots > div { display: grid; gap: 8px; margin-top: 9px; }
.taskflowTimelineSnapshots article { display: grid; gap: 4px; padding: 8px; border: 1px solid #203651; border-radius: 7px; background: #08111e; }
.taskflowTimelineSnapshots article strong { color: #dce8f8; }
.taskflowTimelineSnapshots article span { color: #6f87a6; font-size: 10px; }
.taskflowTimelineSnapshots pre { overflow: auto; margin: 3px 0 0; color: #b9cce3; font: 10px/1.45 ui-monospace, monospace; }
.taskflowInspectionLayout { display: grid; grid-template-columns: 330px minmax(0, 1fr); min-width: 0; min-height: 0; }
.taskflowNodeInspector { min-width: 0; max-height: 760px; overflow: auto; padding: 11px; border-right: 1px solid #213653; background: #091321; scrollbar-gutter: stable; }
.taskflowNodeInspector > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 9px; padding-bottom: 10px; border-bottom: 1px solid #203651; }
.taskflowNodeInspector > header > div { display: grid; gap: 4px; min-width: 0; }
.taskflowNodeInspector > header strong, .taskflowNodeInspectorEmpty > strong { color: #dce8f8; font-size: 12px; line-height: 1.35; overflow-wrap: anywhere; }
.taskflowNodeInspector > header span { color: #7189a8; font-size: 10px; }
.taskflowNodeInspector > header button { flex: none; padding: 5px 8px; color: #9db1cb; border: 1px solid #29435e; border-radius: 6px; background: #08111e; cursor: pointer; }
.taskflowNodeInspector > section { min-width: 0; padding: 10px 0; border-bottom: 1px solid #1b2d45; }
.taskflowNodeInspector h4 { margin: 0 0 7px; color: #70d9c0; font-size: 10px; font-weight: 600; }
.taskflowNodeInspector p, .taskflowNodeInspectorEmpty p { margin: 0 0 6px; color: #a9bbd1; font: 10px/1.5 ui-monospace, monospace; overflow-wrap: anywhere; }
.taskflowNodeInspector code { color: #6f87a6; font: 9px/1.4 ui-monospace, monospace; overflow-wrap: anywhere; }
.taskflowNodeInspector dl { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 10px 0 0; }
.taskflowNodeInspector dl > div { min-width: 0; padding: 6px; border: 1px solid #203651; border-radius: 5px; background: #08111e; }
.taskflowNodeInspector dt { color: #7189a8; font-size: 8px; }
.taskflowNodeInspector dd { margin: 3px 0 0; color: #d0dced; font: 9px/1.3 ui-monospace, monospace; overflow-wrap: anywhere; }
.taskflowNodeInspector pre { max-height: 230px; overflow: auto; margin: 0; color: #b9cce3; font: 9px/1.45 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
.taskflowNodeInspectorEmpty { display: grid; align-content: start; gap: 8px; color: #7189a8; }
.taskflowError { margin: 0; padding: 10px 12px; color: #ff9bae; border: 1px solid #71334a; border-radius: 8px; background: #27101a; }
.taskflowGraphSummary, .taskflowRuntimeSummary { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; margin: 0; }
.taskflowGraphSummary > div, .taskflowRuntimeSummary > div { min-width: 0; padding: 10px; border: 1px solid #213653; border-radius: 9px; background: #0a1422; }
@@ -239,9 +235,6 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
.taskflowNodeLabel header i { flex: none; padding: 2px 5px; color: #74d8c0; border: 1px solid #2d675d; border-radius: 99px; font: 9px/1 ui-monospace, monospace; font-style: normal; }
.taskflowNodeLabel code { color: #6f89aa; font: 9px/1.25 ui-monospace, monospace; overflow-wrap: anywhere; white-space: normal; user-select: text !important; }
.taskflowNodeLabel span { color: #91a5c0; font: 9px/1.25 ui-monospace, monospace; }
.taskflowNodeState { overflow: hidden; margin-top: 3px; border: 1px solid #29435e; border-radius: 5px; background: #07101c; pointer-events: auto; }
.taskflowNodeState summary { padding: 4px 6px; color: #70d9c0; cursor: pointer; font: 9px/1.2 ui-monospace, monospace; user-select: text; }
.taskflowNodeState pre { max-height: 86px; overflow: auto; margin: 0; padding: 6px; color: #b8cae0; border-top: 1px solid #213653; font: 8px/1.35 ui-monospace, monospace; white-space: pre-wrap; user-select: text; }
.react-flow__edge-path { stroke: #557594; stroke-width: 1.5; }
.react-flow__controls button { color: #dce8f8; border-color: #29435e; background: #101d2f; }
.react-flow__minimap { border: 1px solid #29435e; background: #0a1422; }
@@ -271,6 +264,8 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
.taskflowDagFullscreen { position: fixed; inset: 0; z-index: 10000; display: flex; flex-direction: column; width: auto; height: auto; min-width: 0; min-height: 0; margin: 0; overflow: hidden; border: 0; border-radius: 0; background: #07101c; }
.taskflowDagFullscreen .taskflowDagToolbar { flex: none; }
.taskflowDagFullscreen .taskflowInspectionLayout { flex: 1; min-height: 0; }
.taskflowDagFullscreen .taskflowNodeInspector { max-height: none; }
.taskflowDagFullscreen .taskflowDagFill { flex: 1; min-height: 0; cursor: zoom-out; }
.taskflowDagSection:not(.taskflowDagFullscreen) .taskflowDag { cursor: zoom-in; }
.taskflowDagFill { flex: 1; min-height: 0; }
@@ -283,7 +278,7 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
.taskflowTimelineGroupTitle.active { border-left-color: #5ce4c2; background: #132b3d; }
.taskflowTimelineGroupTitle strong { color: #dce8f8; font: 10px/1.2 ui-monospace, monospace; }
.taskflowTimelineGroupTitle span { color: #7189a8; font: 8px/1.2 ui-monospace, monospace; }
@media (max-width: 900px) { .taskflowSelectedCapture > div { grid-template-columns: 1fr; } .taskflowSelectedCapture article { border-right: 0; border-bottom: 1px solid #203651; } }
@media (max-width: 900px) { .taskflowInspectionLayout { grid-template-columns: 260px minmax(0, 1fr); } }
.taskflowTimelineSidebarHeader { display: flex; align-items: center; height: 100%; padding: 0 10px; color: #91a5c0; background: #101d2f; font: 10px/1.2 ui-monospace, monospace; }
.taskflowTimelineViewport .rct-sidebar { border-color: #29405e; background: #091321; }
.taskflowTimelineViewport .rct-sidebar .rct-sidebar-row { overflow: visible; border-color: #1d304a; }
@@ -296,6 +291,7 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
.taskflowTimelineViewport .rct-item { border: 0; border-radius: 5px; box-shadow: none; }
.taskflowTimelineViewport .rct-item .rct-item-content { padding: 0 5px; color: #e6f0fb; font: 8px/1 ui-monospace, monospace; }
.taskflowTimelineItem-topology { background: #265979 !important; }
.taskflowTimelineItem-lifecycle { background: #315172 !important; }
.taskflowTimelineItem-queue { background: #8b5c24 !important; }
.taskflowTimelineItem-observer_entry, .taskflowTimelineItem-observer_exit { background: #67468b !important; }
.taskflowTimelineItem-body { background: #197462 !important; }
@@ -304,12 +300,15 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
.taskflowTimelineMarkerSubmitted { background: #e0a65d; }
.taskflowTimelineMarkerFinished { background: #ef6a82; }
.taskflowTimelineLegendTopology { border-color: #397da4 !important; background: #265979 !important; }
.taskflowTimelineLegendLifecycle { border-color: #557da4 !important; background: #315172 !important; }
.taskflowTimelineLegendQueue { border-color: #c0843e !important; background: #8b5c24 !important; }
.taskflowTimelineLegendObserver { border-color: #9d74c8 !important; background: #67468b !important; }
.taskflowTimelineLegendBody { border-color: #35ad92 !important; background: #197462 !important; }
.taskflowTimelineLegendCompletion { border-color: #b75d79 !important; background: #78354b !important; }
.taskflowTimelineFullscreen { position: fixed; inset: 0; z-index: 10000; display: flex; flex-direction: column; min-width: 0; overflow: hidden; border: 0; border-radius: 0; background: #07101c; }
.taskflowTimelineFullscreen .taskflowTimelineViewport { flex: 1; max-height: none; min-height: 0; }
.taskflowTimelineFullscreen .taskflowInspectionLayout { flex: 1; min-height: 0; }
.taskflowTimelineFullscreen .taskflowNodeInspector { max-height: none; }
.taskflowTimelineFullscreen .taskflowTimelineViewport { max-height: none; min-height: 0; }
.componentCard { margin-bottom: 13px; overflow: hidden; border: 1px solid #213653; border-radius: 11px; background: #0a1422; }
@@ -369,7 +368,7 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
/* Taskflow diagnostic nodes: text selection + explicit node copying. */
.taskflowNodeLabel, .taskflowNodeLabel code, .taskflowNodeLabel span,
.taskflowNodeLabel strong, .taskflowNodeState pre, .taskflowNodeState summary {
.taskflowNodeLabel strong {
-webkit-user-select: text !important;
user-select: text !important;
}