diff --git a/kernel/src/kernel/frame.cpp b/kernel/src/kernel/frame.cpp index 1cb2e67..9b90628 100644 --- a/kernel/src/kernel/frame.cpp +++ b/kernel/src/kernel/frame.cpp @@ -162,6 +162,13 @@ Taskflow_Frame_Trace Render_Frame::take_taskflow_trace() { result.markers.push_back(Frame_Trace_Point{ static_cast(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(index), + decode_present_value(encoded)}); + } result.graphs = std::move(d->taskflow_graphs); for (auto& worker : d->taskflow_workers) { for (auto& task : worker.tasks) diff --git a/kernel/src/kernel/frame.hpp b/kernel/src/kernel/frame.hpp index 1c422b3..92a0695 100644 --- a/kernel/src/kernel/frame.hpp +++ b/kernel/src/kernel/frame.hpp @@ -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 markers{}; /* 与该帧 DAG 共用时间原点的原始流水线时间点。 */ + std::vector measurements{}; /* 本帧原始耗时测量;不另建统计副本。 */ std::vector graphs{}; /* 本帧主动执行的业务 DAG 元信息。 */ std::vector tasks{}; /* 本帧窗口内原生 Observer 完成的任务执行。 */ }; diff --git a/kernel/src/test/render_test.cpp b/kernel/src/test/render_test.cpp index 2836d7f..d237fb4 100644 --- a/kernel/src/test/render_test.cpp +++ b/kernel/src/test/render_test.cpp @@ -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) { diff --git a/render_2D/render_2D/scene/Render_Scene_2D.ipp b/render_2D/render_2D/scene/Render_Scene_2D.ipp index ef69ff7..ae06a39 100644 --- a/render_2D/render_2D/scene/Render_Scene_2D.ipp +++ b/render_2D/render_2D/scene/Render_Scene_2D.ipp @@ -328,6 +328,10 @@ void Render_Scene_2D::Private::ensure_frame_taskflow(Object* object) { template std::expected 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(object); if (!prop.view_active) { release_admission(); diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index 47c1ef0..836b85b 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -281,6 +281,7 @@ void Render_Scene_3D::Private::ensure_frame_taskflow(Object* object) { template 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( double_buffer::detail::Internal_Access::current_prop(object)); if (!prop.view_active) return reject_frame(Render_Result::view_inactive); diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index 72ea63c..6777d49 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -205,6 +205,10 @@ nlohmann::json taskflow_trace_json( for (const auto& marker : trace.markers) markers[magic_enum::enum_name(marker.marker)] = static_cast(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(measurement.value_ns) / 1'000'000.0; nlohmann::json graphs = nlohmann::json::array(); std::unordered_map 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>(&managed->frame)) { + (*frame_2d)->begin(identity, Frame_2D::native_pixel_format); + logical_frame = frame_2d->get(); + } else { + auto& frame_3d = std::get>(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>(&scene)) { auto& output = *std::get>(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(tick.width), static_cast(tick.height)}); @@ -807,10 +826,6 @@ void Plot::Private::render_frame(Plot_Render_Tick tick) { return; } auto& output = *std::get>(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>(scene); scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{tick.width, tick.height}); diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index eab2014..b3fde7b 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -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; graphs: Taskflow_Graph_Trace[]; executions: Taskflow_Execution_Trace[]}; + markers?: Record; measurements?: Record; + 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}: { 协作等待 {sample ? milliseconds(sample.cooperative_wait_ms) : "--"} · 未归因墙时 {sample ? `${sample.cpu_time_coarse ? "≈ " : ""}${milliseconds(Math.max(0, duration - sample.cooperative_wait_ms - sample.cpu_duration_ms))}` : "--"} Observer {sample ? milliseconds(sample.observer_entry_ms + sample.observer_exit_ms) : "--"}} {Object.entries(node.attributes ?? {}).map(([key, value]) => {key}:{value})} - {state ?
event.stopPropagation()} onClick={event => event.stopPropagation()} - onWheel={event => event.stopPropagation()}>运行状态 JSON -
{JSON.stringify(state, null, 2)}
: null} ; } +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 ; + return ; +} + 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 = + elementsSelectable minZoom={.2} maxZoom={2.5} + onNodeClick={(_, node) => set_selected_node_id(node.id)}> ; return
@@ -1858,19 +1889,23 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_ 已执行,执行时间为主 真实排队时间大于执行时间} -
{fullscreen ?
{flow}
: { set_viewport_width(data.size.width); set_viewport_height(data.size.height); }}> -
{flow}
}
; +
+ set_selected_node_id("")}/> + {fullscreen ?
{flow}
: { set_viewport_width(data.size.width); set_viewport_height(data.size.height); }}> +
{flow}
} +
; } 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 & { - 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:
Topology - {graph.stage}
, 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:
+ {title}{detail}
, 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:
Topology + {graph.stage}
, 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
+ 帧生命周期 Topology 墙钟 Executor 排队 Observer @@ -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)))}/>%
- {selected_node ?
-
{taskflow_node_name(selected_node.name, selected_node)} - {selected_node.owner ? `${selected_node.owner.label} · ${selected_node.owner.component}` : "无组件归属"}
-
-

任务说明

{selected_node.name}

{selected_node.id}
-

捕获时属性 Prop

{JSON.stringify(selected_node.prop ?? {}, null, 2)}
-

捕获时状态 State

{JSON.stringify(selected_node.state ?? {}, null, 2)}
-
:
点击左侧任务名称,可查看该节点捕获时的 Prop / State。
} - {model.snapshots.length ?
时间线节点属性 / 状态({model.snapshots.length}) -
{model.snapshots.map(node =>
{taskflow_node_name(node.name, node)} - {node.owner?.label} · {node.owner?.component} -
{JSON.stringify({prop: node.prop ?? {}, state: node.state ?? {}}, null, 2)}
)}
-
: null} +
+ set_selected_node_id("")}/>
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}> @@ -2081,7 +2191,7 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful "相对帧创建时刻的偏移"}/> { const offset = (start.valueOf() - origin) / scale; - return `+${offset.toLocaleString("zh-CN", {maximumFractionDigits: 3})} ms`; + return `${offset >= 0 ? "+" : ""}${offset.toLocaleString("zh-CN", {maximumFractionDigits: 3})} ms`; }}/> @@ -2094,6 +2204,7 @@ function Taskflow_Timeline({graph, executions, frame, fullscreen: controlled_ful
+
; } @@ -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 ? set_fullscreen_view(value ? "timeline" : null)} on_fullscreen_view_change={switch_fullscreen_view} frame_navigation={frame_navigation}/> :
该帧没有所选 Taskflow 阶段切换到“多帧聚合拓扑”仍可查看其他帧中的同一业务阶段。
} diff --git a/webapp_gallery/src/styles.css b/webapp_gallery/src/styles.css index f864614..d4b2505 100644 --- a/webapp_gallery/src/styles.css +++ b/webapp_gallery/src/styles.css @@ -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; }