This commit is contained in:
2026-08-26 14:11:26 +08:00
parent 513f838874
commit 6a6a106dda
9 changed files with 412 additions and 210 deletions
+3 -1
View File
@@ -23,7 +23,9 @@ struct Taskflow_Frame_Access {
std::size_t queue_size, std::size_t queue_capacity,
Clock::time_point entered, Clock::time_point started,
Clock::time_point finished, std::uint64_t cpu_entered_ns,
std::uint64_t cpu_started_ns, std::uint64_t cpu_finished_ns);
std::uint64_t cpu_started_ns, std::uint64_t cpu_duration_ns,
std::uint64_t cpu_cycles, std::uint64_t cooperative_wait_ns,
bool cpu_time_coarse);
static void finish_task_observer(
Render_Frame& frame, std::size_t worker, std::size_t task,
Clock::time_point completed, std::uint64_t cpu_finished_ns,
+7 -4
View File
@@ -434,7 +434,9 @@ std::size_t detail::Taskflow_Frame_Access::append_task(
std::size_t queue_size, std::size_t queue_capacity,
Clock::time_point entered, Clock::time_point started,
Clock::time_point finished, std::uint64_t cpu_entered_ns,
std::uint64_t cpu_started_ns, std::uint64_t cpu_finished_ns) {
std::uint64_t cpu_started_ns, std::uint64_t cpu_duration_ns,
std::uint64_t cpu_cycles, std::uint64_t cooperative_wait_ns,
bool cpu_time_coarse) {
auto& data = *frame.d;
if (worker >= data.taskflow_workers.size())
return std::numeric_limits<std::size_t>::max();
@@ -451,9 +453,10 @@ std::size_t detail::Taskflow_Frame_Access::append_task(
trace.finished_ms = elapsed_ms(finished);
trace.completed_ms = trace.finished_ms;
trace.duration_ms = std::max(0.0, trace.finished_ms - trace.started_ms);
trace.cpu_duration_ms = cpu_finished_ns >= cpu_started_ns
? static_cast<double>(cpu_finished_ns - cpu_started_ns) / 1'000'000.0
: 0.0;
trace.cpu_duration_ms = static_cast<double>(cpu_duration_ns) / 1'000'000.0;
trace.cpu_cycles = cpu_cycles;
trace.cooperative_wait_ms = static_cast<double>(cooperative_wait_ns) / 1'000'000.0;
trace.cpu_time_coarse = cpu_time_coarse;
trace.observer_entry_ms = std::max(0.0, trace.started_ms - trace.entered_ms);
trace.observer_entry_cpu_ms = cpu_started_ns >= cpu_entered_ns
? static_cast<double>(cpu_started_ns - cpu_entered_ns) / 1'000'000.0
+4 -1
View File
@@ -100,7 +100,10 @@ struct Taskflow_Task_Trace {
double finished_ms{}; /* Observer on_exit 进入,即任务体已经结束的时间。 */
double completed_ms{}; /* Observer on_exit 与按帧追踪写入全部结束的时间。 */
double duration_ms{}; /* 仅任务体 started 到 finished 的持续时间。 */
double cpu_duration_ms{}; /* 任务体当前 worker 线程上实际消耗的 CPU 时间。 */
double cpu_duration_ms{}; /* 任务体独占当前 worker 片段的线程 CPU 时间;cooperative corun 期间不计入。 */
std::uint64_t cpu_cycles{}; /* Windows QueryThreadCycleTime 的独占 CPU 周期;用于短任务 CPU 活动判定,不直接换算秒。 */
double cooperative_wait_ms{}; /* Task_Graph::corun/corun_until 主动让出 Worker 的墙钟时间。 */
bool cpu_time_coarse{}; /* 当前平台的线程 CPU 时间源是否为低分辨率计费时钟(Windows GetThreadTimes)。 */
double observer_entry_ms{}; /* on_entry 诊断本身的耗时。 */
double observer_exit_ms{}; /* on_exit 诊断与按帧追踪写入的耗时。 */
double observer_entry_cpu_ms{}; /* on_entry 诊断实际消耗的 worker CPU 时间。 */
+110 -18
View File
@@ -70,6 +70,20 @@ std::uint64_t current_thread_cpu_ns() noexcept {
return 0;
#endif
}
std::uint64_t current_thread_cpu_cycles() noexcept {
#if defined(_WIN32)
return thread_cpu_cycles(GetCurrentThread());
#else
return 0;
#endif
}
constexpr bool thread_cpu_time_is_coarse() noexcept {
#if defined(_WIN32)
return true;
#else
return false;
#endif
}
struct Task_Observer : public tf::ObserverInterface {
private:
using Clock = std::chrono::steady_clock;
@@ -122,7 +136,14 @@ private:
Clock::time_point segment_started{}; /* 当前连续独占 Worker 片段的起点。 */
std::uint64_t maximum_segment_ns{}; /* 已结束连续独占片段的最大墙钟。 */
std::uint64_t cpu_entered_ns{}; /* on_entry 进入时的 worker CPU 时间。 */
std::uint64_t cpu_started_ns{}; /* 任务体开始前的 worker CPU 时间。 */
std::uint64_t cpu_started_ns{}; /* 任务体开始前的 worker CPU 时间,仅用于 Observer entry 统计。 */
std::uint64_t cpu_segment_started_ns{}; /* 当前任务独占 Worker 片段的线程 CPU 起点。 */
bool cpu_segment_active{}; /* CPU 累计值本身允许为 0,不能拿 0 当未启动哨兵。 */
std::uint64_t cpu_duration_ns{}; /* 已累计的任务独占 Worker CPU;嵌套 corun/子任务不计入。 */
std::uint64_t cpu_cycle_segment_started{}; /* Windows 当前独占片段的 QueryThreadCycleTime 起点。 */
std::uint64_t cpu_cycles{}; /* Windows 已累计的任务独占 CPU 周期。 */
Clock::time_point cooperative_wait_started{}; /* 主动 corun 让出 Worker 的墙钟起点。 */
std::uint64_t cooperative_wait_ns{}; /* 已累计 cooperative wait 墙钟。 */
Render_Frame* frame{}; /* 进入任务时唯一活动的按帧捕获。 */
std::size_t queue_size{}; /* 进入任务时 worker 队列深度。 */
std::size_t queue_capacity{}; /* 进入任务时 worker 队列容量。 */
@@ -245,6 +266,41 @@ private:
}
}
}
static void close_cpu_segment(
Start_Record& active, std::uint64_t cpu_now_ns,
std::uint64_t cycle_now) noexcept {
if (!active.frame || !active.cpu_segment_active) return;
if (cpu_now_ns >= active.cpu_segment_started_ns)
active.cpu_duration_ns +=
cpu_now_ns - active.cpu_segment_started_ns;
#if defined(_WIN32)
if (cycle_now >= active.cpu_cycle_segment_started)
active.cpu_cycles += cycle_now - active.cpu_cycle_segment_started;
#else
static_cast<void>(cycle_now);
#endif
active.cpu_segment_started_ns = 0;
active.cpu_cycle_segment_started = 0;
active.cpu_segment_active = false;
}
static void close_cpu_segment(Start_Record& active) noexcept {
if (!active.frame || !active.cpu_segment_active) return;
close_cpu_segment(active, current_thread_cpu_ns(),
current_thread_cpu_cycles());
}
static void open_cpu_segment(
Start_Record& active, std::uint64_t cpu_now_ns,
std::uint64_t cycle_now) noexcept {
if (!active.frame) return;
active.cpu_segment_started_ns = cpu_now_ns;
active.cpu_cycle_segment_started = cycle_now;
active.cpu_segment_active = true;
}
static void open_cpu_segment(Start_Record& active) noexcept {
if (!active.frame) return;
open_cpu_segment(active, current_thread_cpu_ns(),
current_thread_cpu_cycles());
}
void pause_worker(std::size_t worker) noexcept {
if (worker >= starts.size() || starts[worker].empty()) return;
const auto now = Clock::now();
@@ -256,7 +312,10 @@ private:
active.maximum_segment_ns = std::max(
active.maximum_segment_ns, elapsed);
}
close_cpu_segment(active);
active.segment_started = {};
if (active.cooperative_wait_started == Clock::time_point{})
active.cooperative_wait_started = now;
active.cooperatively_suspended = true;
worker_statistics[worker].active_segment_started_ns.store(
0, std::memory_order_release);
@@ -273,8 +332,15 @@ private:
if (worker >= starts.size() || starts[worker].empty()) return;
const auto now = Clock::now();
auto& active = starts[worker].back();
if (active.cooperative_wait_started != Clock::time_point{}) {
active.cooperative_wait_ns += static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
now - active.cooperative_wait_started).count());
active.cooperative_wait_started = {};
}
active.cooperatively_suspended = false;
active.segment_started = now;
open_cpu_segment(active);
worker_statistics[worker].active_segment_started_ns.store(
clock_ns(now), std::memory_order_release);
worker_statistics[worker].active_segment_cpu_started_ns.store(
@@ -351,6 +417,7 @@ public:
now - parent.segment_started).count());
parent.maximum_segment_ns = std::max(
parent.maximum_segment_ns, elapsed);
close_cpu_segment(parent);
parent.segment_started = {};
}
}
@@ -358,11 +425,14 @@ public:
worker_busy_starts[worker.id()] = now;
worker_cpu_starts[worker.id()] = current_thread_cpu_ns();
}
worker_starts.push_back(Start_Record{
now, {}, {}, 0, current_thread_cpu_ns(), 0, nullptr,
worker.queue_size(), worker.queue_capacity(),
static_cast<std::uint64_t>(task.hash_value()), task.type(), false
});
Start_Record record{};
record.entered = now;
record.cpu_entered_ns = current_thread_cpu_ns();
record.queue_size = worker.queue_size();
record.queue_capacity = worker.queue_capacity();
record.native_id = static_cast<std::uint64_t>(task.hash_value());
record.type = task.type();
worker_starts.push_back(std::move(record));
auto* frame = trace_frame.load(std::memory_order_acquire);
/*
* 帧租约从 on_entry 持续到对应 on_exit。只在退出时登记写入者会留下
@@ -413,7 +483,15 @@ public:
task.num_weak_dependencies());
if (!task.name().empty()) worker_state.named_task_count.fetch_add(1, std::memory_order_relaxed);
update_first(worker_state.first_task_time_ns, clock_ns(now));
worker_starts.back().cpu_started_ns = current_thread_cpu_ns();
const auto task_cpu_started_ns = current_thread_cpu_ns();
const auto task_cpu_cycle_started = worker_starts.back().frame
? current_thread_cpu_cycles() : 0;
worker_starts.back().cpu_started_ns = task_cpu_started_ns;
worker_starts.back().cpu_segment_started_ns = task_cpu_started_ns;
worker_starts.back().cpu_cycle_segment_started =
task_cpu_cycle_started;
worker_starts.back().cpu_segment_active =
worker_starts.back().frame != nullptr;
worker_starts.back().started = Clock::now();
worker_starts.back().segment_started = worker_starts.back().started;
}
@@ -421,7 +499,17 @@ public:
const auto finished = Clock::now();
const auto cpu_finished_ns = current_thread_cpu_ns();
auto& worker_starts = starts[worker.id()];
auto start = worker_starts.back();
auto& active = worker_starts.back();
const auto cpu_finished_cycles = active.frame
? current_thread_cpu_cycles() : 0;
close_cpu_segment(active, cpu_finished_ns, cpu_finished_cycles);
if (active.cooperative_wait_started != Clock::time_point{}) {
active.cooperative_wait_ns += static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
finished - active.cooperative_wait_started).count());
active.cooperative_wait_started = {};
}
auto start = active;
worker_starts.pop_back();
const auto elapsed = static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
@@ -467,7 +555,9 @@ public:
static_cast<std::uint64_t>(task.hash_value()),
start.queue_size, start.queue_capacity, start.entered,
start.started, finished, start.cpu_entered_ns,
start.cpu_started_ns, cpu_finished_ns);
start.cpu_started_ns, start.cpu_duration_ns,
start.cpu_cycles, start.cooperative_wait_ns,
thread_cpu_time_is_coarse());
}
catch (...) {
/* Observer 不能让按需诊断分配失败改变渲染任务的完成语义。 */
@@ -526,20 +616,22 @@ public:
#endif
}
else {
parent.segment_started = completed;
const auto parent_cpu_started_ns = current_thread_cpu_ns();
const auto parent_cpu_cycle_started = parent.frame
? current_thread_cpu_cycles() : 0;
const auto parent_resumed = Clock::now();
parent.segment_started = parent_resumed;
open_cpu_segment(parent, parent_cpu_started_ns,
parent_cpu_cycle_started);
worker_state.active_segment_started_ns.store(
clock_ns(completed), std::memory_order_release);
clock_ns(parent_resumed), std::memory_order_release);
worker_state.active_segment_cpu_started_ns.store(
cpu_completed_ns, std::memory_order_release);
parent_cpu_started_ns, std::memory_order_release);
#if defined(_WIN32)
const auto native_handle =
worker_state.native_thread_handle.load(
std::memory_order_acquire);
worker_state.active_cpu_cycles.store(
thread_cpu_cycles(reinterpret_cast<HANDLE>(native_handle)),
std::memory_order_release);
parent_cpu_cycle_started, std::memory_order_release);
worker_state.active_cpu_progress_ns.store(
clock_ns(completed), std::memory_order_release);
clock_ns(parent_resumed), std::memory_order_release);
#endif
}
worker_state.active_task_type.store(parent.type, std::memory_order_relaxed);
+32 -91
View File
@@ -2,17 +2,14 @@
#include <frame_statistics.hpp>
#include <render_common.hpp>
#include "detail/Gallery_Frame_Atlas.hpp"
#include <concurrentqueue-1.0.5/blockingconcurrentqueue.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <future>
#include <optional>
#include <stdexcept>
#include <string>
#include <thread>
#include <utility>
namespace aethera::web {
namespace {
@@ -46,71 +43,7 @@ std::string exception_description(const std::exception_ptr& failure) {
return "empty gallery video failure";
}
}
namespace detail {
/*
* FFmpeg 硬件编码 API 可能在驱动内部等待硬件队列。它仍是 Scene completion
* DAG 的一个业务节点,但不能占住 Kernel Taskflow Worker。每个图集只有一个
* H264_Encoder,因此用一个串行编码域维持 codec context 的唯一执行位置;
* 调用节点通过 Task_Graph::corun_until 协作让出,完成后恢复同一帧 DAG。
*/
struct Video_Encode_Domain final {
struct Work {
std::function<void()> function;
std::shared_ptr<std::promise<void>> completion;
};
public:
Video_Encode_Domain() : thread_([this] {
run();
}) {}
~Video_Encode_Domain() {
stopping_.store(true, std::memory_order_release);
if (thread_.joinable()) thread_.join();
}
Video_Encode_Domain(const Video_Encode_Domain&) = delete;
Video_Encode_Domain& operator=(const Video_Encode_Domain&) = delete;
void invoke(std::function<void()> function) {
if (!function) throw std::invalid_argument("video encode work is empty");
if (stopping_.load(std::memory_order_acquire)) throw std::runtime_error("video encode domain is stopping");
auto completion = std::make_shared<std::promise<void>>();
auto completed = completion->get_future();
auto work = std::make_unique<Work>(
Work{std::move(function), std::move(completion)});
if (!queue_.enqueue(std::move(work))) throw std::bad_alloc{};
Task_Graph::corun_until([&completed] {
return completed.wait_for(std::chrono::seconds(0)) ==
std::future_status::ready;
});
completed.get();
}
private:
void run() noexcept {
for (;;) {
std::unique_ptr<Work> work;
const bool received = queue_.wait_dequeue_timed(
work, std::chrono::milliseconds(1));
if (received && work) {
try {
work->function();
work->completion->set_value();
}
catch (...) {
try {
work->completion->set_exception(
std::current_exception());
}
catch (...) {}
}
}
if (!received && stopping_.load(std::memory_order_acquire)) return;
}
}
moodycamel::BlockingConcurrentQueue<std::unique_ptr<Work>> queue_{8};
std::atomic_bool stopping_{};
std::thread thread_;
};
}
Gallery_Video_Stream::Private::Private() : encoder(gallery_frame_rate),
encode_domain(std::make_unique<detail::Video_Encode_Domain>()) {}
Gallery_Video_Stream::Private::Private() : encoder(gallery_frame_rate) {}
Gallery_Video_Stream::Private::~Private() = default;
void Gallery_Video_Stream::Private::initialize(
std::vector<Plot_Entry> plots) {
@@ -331,27 +264,32 @@ void Gallery_Video_Stream::Private::compose_media_frame() {
}
void Gallery_Video_Stream::Private::encode_media_frame() {
if (!active_composition) return;
encode_domain->invoke([this] {
const auto started = std::chrono::steady_clock::now();
if (key_frame_requested.exchange(false,
std::memory_order_acq_rel))
encoder.request_key_frame();
active_video = encoder.encode(
active_composition->pixels, active_composition->width,
active_composition->height,
active_composition->layout == Plot_Pixel_Layout::bgra8
? Video_Pixel_Layout::bgra
: Video_Pixel_Layout::rgba,
active_encode_tick.sequence,
std::chrono::microseconds{
static_cast<std::int64_t>(
std::llround(active_encode_tick.time_milliseconds *
1'000.0))
});
static_cast<void>(encode_ms.submit(
std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - started).count()));
});
/*
* 直接在 FFmpeg.H264.encode Taskflow 节点中调用 FFmpeg。这样实际
* avcodec_send_frame/avcodec_receive_packet(以及硬件后端等待)全部落在
* 同一个业务节点的 wall/CPU 统计中,不再由额外编码线程隐藏真实耗时。
* gallery.video.frame 本身串行 compose -> encode -> publish,页面唯一
* H264_Encoder 因而仍只有一个执行位置,不需要额外 mutex 或专用线程。
*/
const auto started = std::chrono::steady_clock::now();
if (key_frame_requested.exchange(false,
std::memory_order_acq_rel))
encoder.request_key_frame();
active_video = encoder.encode(
active_composition->pixels, active_composition->width,
active_composition->height,
active_composition->layout == Plot_Pixel_Layout::bgra8
? Video_Pixel_Layout::bgra
: Video_Pixel_Layout::rgba,
active_encode_tick.sequence,
std::chrono::microseconds{
static_cast<std::int64_t>(
std::llround(active_encode_tick.time_milliseconds *
1'000.0))
});
static_cast<void>(encode_ms.submit(
std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - started).count()));
}
void Gallery_Video_Stream::Private::publish_media_frame() {
if (!active_video || !active_composition) return;
@@ -418,7 +356,7 @@ void Gallery_Video_Stream::bind_plots() {
});
compose.describe("owner", "gallery")
.describe("stage", "latest completed Plot frames to atlas");
auto encode = media->add("gallery.h264.encode", [weak] {
auto encode = media->add("FFmpeg.H264.encode", [weak] {
if (const auto owner = weak.lock()) {
auto& owner_data = static_cast<Private&>(*owner->d);
try {
@@ -430,7 +368,10 @@ void Gallery_Video_Stream::bind_plots() {
}
});
encode.describe("owner", "gallery")
.describe("stage", "H.264 encode in Scene completion pipeline");
.describe("stage", "FFmpeg H.264 encode in Scene completion pipeline")
.describe("backend", "FFmpeg")
.describe("codec", "H.264")
.describe("execution", "Taskflow worker");
auto publish = media->add("gallery.webrtc.publish", [weak] {
if (const auto owner = weak.lock()) {
auto& owner_data = static_cast<Private&>(*owner->d);
-4
View File
@@ -4,9 +4,6 @@
#include <atomic>
#include <chrono>
namespace aethera::web {
namespace detail {
struct Video_Encode_Domain;
}
struct Gallery_Video_Stream::Private : Prev_Private {
using Object = Impl<Gallery_Video_Stream>;
struct Source {
@@ -28,7 +25,6 @@ struct Gallery_Video_Stream::Private : Prev_Private {
std::vector<Source> sources{}; /* 已按业务标识排序的稳定图集来源。 */
std::unique_ptr<detail::Gallery_Frame_Atlas> atlas{}; /* 最近完成帧与 RGBA 图集的唯一状态源。 */
H264_Encoder encoder; /* 页面唯一 H.264 编码器。 */
std::unique_ptr<detail::Video_Encode_Domain> encode_domain{}; /* FFmpeg 驱动调用域。 */
Plot_Render_Tick active_encode_tick{}; /* 当前媒体 DAG 的输入时钟。 */
std::optional<detail::Gallery_Atlas_Composition> active_composition{}; /* 当前合成结果所有权。 */
std::optional<Encoded_Video_Frame> active_video{}; /* 当前编码结果所有权。 */
+3
View File
@@ -243,6 +243,9 @@ nlohmann::json taskflow_trace_json(const Taskflow_Frame_Trace& trace) {
{"completed_ms", task.completed_ms},
{"duration_ms", task.duration_ms},
{"cpu_duration_ms", task.cpu_duration_ms},
{"cpu_cycles", task.cpu_cycles},
{"cooperative_wait_ms", task.cooperative_wait_ms},
{"cpu_time_coarse", task.cpu_time_coarse},
{"observer_entry_ms", task.observer_entry_ms},
{"observer_exit_ms", task.observer_exit_ms},
{"observer_entry_cpu_ms", task.observer_entry_cpu_ms},
+226 -82
View File
@@ -4,7 +4,7 @@ import {Responsive, useContainerWidth, type LayoutItem, type ResponsiveLayouts}
import {ResizableBox} from "react-resizable";
import ReconnectingWebSocket from "reconnecting-websocket";
import ELK from "elkjs/lib/elk.bundled.js";
import {Background, Controls, MarkerType, MiniMap, ReactFlow,
import {Background, Controls, MarkerType, MiniMap, Position, ReactFlow,
type Edge as Flow_Edge, type Node as Flow_Node} from "@xyflow/react";
import * as echarts from "echarts/core";
import {LineChart} from "echarts/charts";
@@ -79,7 +79,8 @@ type Taskflow_Graph_Trace = {stage: string; name: string; submitted_ms: number;
completed: boolean; nodes: Taskflow_Node_Trace[]};
type Taskflow_Execution_Trace = {native_id: string; node_id: string; worker_id: number; worker_queue_size: number;
worker_queue_capacity: number; ready_ms: number; entered_ms: number; started_ms: number; finished_ms: number;
completed_ms: number; duration_ms: number; cpu_duration_ms: number; observer_entry_ms: number; observer_exit_ms: number;
completed_ms: number; duration_ms: number; cpu_duration_ms: number; cpu_cycles: number; cooperative_wait_ms: number;
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[]};
@@ -1137,20 +1138,160 @@ function nanoseconds(value: number) {
return milliseconds(value / 1_000_000);
}
const taskflow_operation_names: Record<string, string> = {
condition: "执行条件",
data: "绘制",
graph: "子图",
extension: "扩展",
complete: "提交组件 State",
cache_composite: "缓存合成"
};
function cpu_cycles(value: number) {
if (!Number.isFinite(value) || value <= 0) return "--";
if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(2)} Gcy`;
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(2)} Mcy`;
if (value >= 1_000) return `${(value / 1_000).toFixed(1)} Kcy`;
return `${Math.round(value)} cy`;
}
function task_cpu_label(sample: Taskflow_Execution_Trace) {
if (sample.cpu_time_coarse && sample.cpu_duration_ms === 0 && sample.cpu_cycles > 0)
return "低于系统 CPU 计时分辨率";
return milliseconds(sample.cpu_duration_ms);
}
type Taskflow_Node_Phase = "prepare" | "paint" | "control" | "completion" | "other";
function taskflow_node_phase(name: string): Taskflow_Node_Phase {
if (name.includes(".prepare.")) return "prepare";
if (name.includes(".paint.")) return "paint";
if (name === "scene.paint" || name === "scene.paint.setup" || name === "scene.paint.complete") return "paint";
if (name.includes(".completion") || name.includes(".publish")) return "completion";
if (name.startsWith("scene.")) return "control";
return "other";
}
function taskflow_node_name(name: string) {
const parts = name.split(".").filter(part => part && part !== "paint");
if (parts.length === 0) return name;
const operation = taskflow_operation_names[parts.at(-1) ?? ""] ?? parts.at(-1);
return parts.length === 1 ? operation ?? name : `${parts[0]} · ${operation}`;
const parts = name.split(".").filter(Boolean);
if (!parts.length) return name;
if (name === "scene.begin") return "Scene · 帧开始";
if (name === "scene.prepare") return "Scene · 数据准备子图";
if (name === "scene.paint.setup") return "Scene · 绘制目标准备";
if (name === "scene.paint") return "Scene · 绘制子图";
if (name === "scene.paint.complete") return "Scene · 像素绘制完成";
if (name === "scene.completion") return "Scene · 帧完成处理";
if (name === "plot.frame.publish") return "Plot · 发布完成帧";
if (name === "gallery.atlas.compose") return "Gallery · 图集合成";
if (name === "FFmpeg.H264.encode" || name === "gallery.h264.encode") return "FFmpeg H.264 · 编码";
if (name === "gallery.webrtc.publish") return "WebRTC · 视频帧发布";
const owner = parts[0];
const stage = parts[1];
const operation = parts[2] ?? parts[1];
if (stage === "prepare") {
const labels: Record<string, string> = {
condition: "准备条件", data: "数据准备", graph: "准备子图",
extension: "准备扩展", complete: "准备完成"
};
return `${owner} · ${labels[operation] ?? `准备 / ${operation}`}`;
}
if (stage === "paint") {
const labels: Record<string, string> = {
condition: "绘制条件", data: "实际绘制", graph: "绘制子图",
extension: "绘制扩展", complete: "绘制完成", cache_composite: "缓存合成"
};
return `${owner} · ${labels[operation] ?? `绘制 / ${operation}`}`;
}
const labels: Record<string, string> = {
condition: "执行条件", data: "执行数据", graph: "子图", extension: "扩展",
complete: "完成", cache_composite: "缓存合成"
};
return parts.length === 1 ? (labels[parts[0]] ?? name)
: `${owner} · ${labels[parts.at(-1) ?? ""] ?? parts.at(-1)}`;
}
function taskflow_phase_label(name: string) {
const phase = taskflow_node_phase(name);
if (name === "FFmpeg.H264.encode" || name === "gallery.h264.encode") return "FFMPEG";
if (name.includes(".paint.data")) return "PAINT";
if (name.includes(".prepare.data")) return "PREP";
if (phase === "paint") return "绘制阶段";
if (phase === "prepare") return "准备阶段";
if (phase === "completion") return "完成阶段";
return null;
}
async function copy_text(text: string) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const copied = document.execCommand("copy");
textarea.remove();
if (!copied) throw new Error("copy failed");
}
function Taskflow_Node_Label({node, sample, summary, state, level}: {
node: Taskflow_Node_Trace; sample?: Taskflow_Execution_Trace; summary?: Taskflow_Node_Aggregate;
state: unknown; level: number;
}) {
const [copied, set_copied] = useState(false);
const wait = sample?.queue_wait_ms ?? 0;
const duration = sample?.duration_ms ?? 0;
const phase = taskflow_node_phase(node.name);
const phase_label = taskflow_phase_label(node.name);
const copy_node = async () => {
try {
await copy_text(JSON.stringify({
node, execution: sample ?? null,
aggregate: summary ? {
present: summary.present, executed: summary.executed, frames: summary.frames,
duration: summary.duration, queue: summary.queue, cpu: summary.cpu, observer: summary.observer,
workers: summary.workers, stability: summary.stability
} : null,
state: state ?? null
}, null, 2));
set_copied(true);
window.setTimeout(() => set_copied(false), 1000);
} catch {
set_copied(false);
}
};
return <div className={`taskflowNodeLabel nodrag nopan taskflowNodeLabel-${phase}`} title={`${node.name}\n${node.id}`}
onPointerDown={event => event.stopPropagation()} onWheel={event => event.stopPropagation()}>
<header>
<div className="taskflowNodeTitle">
<strong>{taskflow_node_name(node.name)}</strong>
{phase_label ? <b className={`taskflowPhaseBadge taskflowPhaseBadge-${phase}`}>{phase_label}</b> : null}
</div>
<div className="taskflowNodeHeaderActions">
<button type="button" className="taskflowNodeCopy nodrag nopan"
onPointerDown={event => event.stopPropagation()}
onClick={event => { event.stopPropagation(); void copy_node(); }}>
{copied ? "已复制" : "复制节点"}
</button>
<i> {level + 1}</i>
</div>
</header>
<code>{node.name}</code>
{summary ? <>
<span>{node.type} · {summary.executed}/{summary.frames} · W {summary.workers.join(", ") || "--"}</span>
<span> {milliseconds(summary.duration.average)} ± {milliseconds(summary.duration.variability)} · P95 {milliseconds(summary.duration.p95)}</span>
<span> {milliseconds(summary.queue.average)} ± {milliseconds(summary.queue.variability)} · P99 {milliseconds(summary.queue.p99)}</span>
<span>线 CPU{summary.cpu_time_coarse ? "(低分辨率)" : ""} {milliseconds(summary.cpu.average)} · CPU {cpu_cycles(summary.cycles.average)}</span>
<span> {milliseconds(summary.cooperative_wait.average)} · Observer {milliseconds(summary.observer.average)}</span>
</> : <><span>{node.type} · W{sample?.worker_id ?? "--"}</span>
<span> {sample ? milliseconds(duration) : "未执行"} · {sample ? milliseconds(wait) : "--"}</span>
<span>线 CPU {sample ? task_cpu_label(sample) : "--"}{sample?.cpu_time_coarse ? "(低分辨率)" : ""} · CPU {sample ? cpu_cycles(sample.cpu_cycles) : "--"}</span>
<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_graph_analysis(graph: Taskflow_Graph_Trace, executions: Taskflow_Execution_Trace[]) {
@@ -1167,7 +1308,10 @@ function taskflow_graph_analysis(graph: Taskflow_Graph_Trace, executions: Taskfl
const execution_time = leaf_rows.reduce((sum, row) => sum + row.duration_ms, 0);
const module_envelope_time = module_rows.reduce((sum, row) => sum + row.duration_ms, 0);
const cpu_execution_time = leaf_rows.reduce((sum, row) => sum + row.cpu_duration_ms, 0);
const descheduled_time = Math.max(0, execution_time - cpu_execution_time);
const cooperative_wait_time = leaf_rows.reduce((sum, row) => sum + row.cooperative_wait_ms, 0);
const cpu_cycles_total = leaf_rows.reduce((sum, row) => sum + row.cpu_cycles, 0);
const cpu_time_coarse = leaf_rows.some(row => row.cpu_time_coarse);
const unattributed_wall_time = Math.max(0, execution_time - cooperative_wait_time - cpu_execution_time);
const observer_entry_time = rows.reduce((sum, row) => sum + row.observer_entry_ms, 0);
const observer_exit_time = rows.reduce((sum, row) => sum + row.observer_exit_ms, 0);
const observer_cpu_time = rows.reduce((sum, row) => sum + row.observer_entry_cpu_ms + row.observer_exit_cpu_ms, 0);
@@ -1176,9 +1320,11 @@ function taskflow_graph_analysis(graph: Taskflow_Graph_Trace, executions: Taskfl
const last_completed = rows.length ? Math.max(...rows.map(row => row.completed_ms)) : graph.finished_ms;
const longest = rows.reduce<Taskflow_Execution_Trace | null>(
(result, row) => !result || row.duration_ms > result.duration_ms ? row : result, null);
const longest_non_cpu = rows.reduce<Taskflow_Execution_Trace | null>((result, row) => {
const value = Math.max(0, row.duration_ms - row.cpu_duration_ms);
const previous = result ? Math.max(0, result.duration_ms - result.cpu_duration_ms) : -1;
const longest_unattributed = rows.reduce<Taskflow_Execution_Trace | null>((result, row) => {
const value = Math.max(0, row.duration_ms - row.cooperative_wait_ms - row.cpu_duration_ms);
const previous = result
? Math.max(0, result.duration_ms - result.cooperative_wait_ms - result.cpu_duration_ms)
: -1;
return value > previous ? row : result;
}, null);
const events = rows.flatMap(row => [
@@ -1233,8 +1379,8 @@ function taskflow_graph_analysis(graph: Taskflow_Graph_Trace, executions: Taskfl
for (const level of levels.values()) width_by_level.set(level, (width_by_level.get(level) ?? 0) + 1);
return {
rows, leaf_rows, module_rows, levels, wall_time, execution_time,
module_envelope_time, cpu_execution_time,
descheduled_time, observer_entry_time, observer_cpu_time,
module_envelope_time, cpu_execution_time, cooperative_wait_time, cpu_cycles_total, cpu_time_coarse,
unattributed_wall_time, observer_entry_time, observer_cpu_time,
observer_exit_time, queue_time, body_wall_time, observer_wall_time,
idle_wall_time, initial_wait, completion_tail,
internal_idle_time: Math.max(0, idle_wall_time - initial_wait - completion_tail),
@@ -1242,7 +1388,7 @@ function taskflow_graph_analysis(graph: Taskflow_Graph_Trace, executions: Taskfl
maximum_parallelism,
layer_count: width_by_level.size,
parallel_layer_count: [...width_by_level.values()].filter(width => width > 1).length,
longest, longest_non_cpu
longest, longest_unattributed
};
}
@@ -1251,6 +1397,7 @@ type Taskflow_Node_Stability = "stable" | "variable" | "missing" | "long_tail";
type Taskflow_Node_Aggregate = {
node: Taskflow_Node_Trace; present: number; executed: number; frames: number;
duration: Distribution_Statistics; queue: Distribution_Statistics; cpu: Distribution_Statistics;
cycles: Distribution_Statistics; cooperative_wait: Distribution_Statistics; cpu_time_coarse: boolean;
observer: Distribution_Statistics; workers: number[]; stability: Taskflow_Node_Stability;
};
type Taskflow_Graph_Aggregate = {
@@ -1283,7 +1430,8 @@ function aggregate_taskflow_graph(frames: Taskflow_Frame_Trace[], key: string):
.map(graph => ({frame, graph})));
if (!samples.length) return null;
type Mutable_Node = {template: Taskflow_Node_Trace; present: number; executed: number; duration: number[];
queue: number[]; cpu: number[]; observer: number[]; workers: Set<number>};
queue: number[]; cpu: number[]; cycles: number[]; cooperative_wait: number[]; cpu_time_coarse: boolean;
observer: number[]; workers: Set<number>};
const accumulated = new Map<string, Mutable_Node>();
const edges = new Map<string, number>();
const topology_signatures = new Set<string>();
@@ -1295,7 +1443,8 @@ function aggregate_taskflow_graph(frames: Taskflow_Frame_Trace[], key: string):
wall.push(Math.max(0, graph.finished_ms - graph.submitted_ms));
for (const node of graph.nodes) {
const current = accumulated.get(node.id) ?? {template: node, present: 0, executed: 0,
duration: [], queue: [], cpu: [], observer: [], workers: new Set<number>()};
duration: [], queue: [], cpu: [], cycles: [], cooperative_wait: [], cpu_time_coarse: false,
observer: [], workers: new Set<number>()};
++current.present;
const sample = execution.get(node.native_id);
if (sample) {
@@ -1303,6 +1452,9 @@ function aggregate_taskflow_graph(frames: Taskflow_Frame_Trace[], key: string):
current.duration.push(sample.duration_ms);
current.queue.push(sample.queue_wait_ms);
current.cpu.push(sample.cpu_duration_ms);
current.cycles.push(sample.cpu_cycles);
current.cooperative_wait.push(sample.cooperative_wait_ms);
current.cpu_time_coarse = current.cpu_time_coarse || sample.cpu_time_coarse;
current.observer.push(sample.observer_entry_ms + sample.observer_exit_ms);
current.workers.add(sample.worker_id);
}
@@ -1340,7 +1492,9 @@ function aggregate_taskflow_graph(frames: Taskflow_Frame_Trace[], key: string):
const node = {...value.template, native_id: id, id,
predecessors: predecessors.get(id) ?? [], successors: successors.get(id) ?? []};
nodes.set(id, {node, present: value.present, executed: value.executed, frames: frames.length,
duration, queue, cpu: distribution_statistics(value.cpu), observer: distribution_statistics(value.observer),
duration, queue, cpu: distribution_statistics(value.cpu), cycles: distribution_statistics(value.cycles),
cooperative_wait: distribution_statistics(value.cooperative_wait), cpu_time_coarse: value.cpu_time_coarse,
observer: distribution_statistics(value.observer),
workers: [...value.workers].sort((left, right) => left - right), stability});
return node;
});
@@ -1390,26 +1544,39 @@ function taskflow_node_state(node: Taskflow_Node_Trace, components: Component[],
return taskflow_render_domain_state(node, frame);
}
function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_state, standalone = false}: {graph: Taskflow_Graph_Trace; executions: Taskflow_Execution_Trace[];
function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_state}: {graph: Taskflow_Graph_Trace; executions: Taskflow_Execution_Trace[];
frame?: Taskflow_Frame_Trace; aggregate?: Taskflow_Graph_Aggregate | null; components: Component[];
gallery_state?: Gallery_Pipeline_State | null; standalone?: boolean}) {
gallery_state?: Gallery_Pipeline_State | null}) {
const [nodes, set_nodes] = useState<Flow_Node[]>([]);
const [edges, set_edges] = useState<Flow_Edge[]>([]);
const [copy_state, set_copy_state] = useState("复制拓扑 JSON");
const [viewport_width, set_viewport_width] = useState(1000);
const [viewport_height, set_viewport_height] = useState(720);
const [fullscreen, set_fullscreen] = useState(false);
useEffect(() => {
let cancelled = false;
const node_id = new Map(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(graph, executions);
const predecessor_count = new Map<string, number>();
for (const node of graph.nodes) predecessor_count.set(node.id, 0);
for (const node of graph.nodes) for (const successor of node.successors) {
const target = node_id.get(successor);
if (target) predecessor_count.set(target, (predecessor_count.get(target) ?? 0) + 1);
}
const flow_edges: Flow_Edge[] = [];
for (const node of graph.nodes) for (const successor of node.successors) {
const target = node_id.get(successor);
if (!target) continue;
const id = `${node.id}->${target}`;
const presence = aggregate?.edge_presence.get(id) ?? 1;
flow_edges.push({id, source: node.id, target, type: "smoothstep",
// Keep simple 1 -> 1 dependencies as direct vertical segments.
// Fan-out/fan-in and cross-column dependencies use React Flow's
// bezier edge instead of SmoothStep. SmoothStep can route unrelated
// edges through the same horizontal corridor and visually form a
// misleading rectangle/loop even though the graph is acyclic.
const serial_edge = node.successors.length === 1 && (predecessor_count.get(target) ?? 0) === 1;
flow_edges.push({id, source: node.id, target, type: serial_edge ? "straight" : "default",
markerEnd: {type: MarkerType.ArrowClosed}, animated: false,
style: aggregate && presence < aggregate.frames ? {strokeDasharray: "7 5", opacity: .5 + .5 * presence / aggregate.frames} : undefined});
}
@@ -1421,7 +1588,11 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
"elk.layered.spacing.nodeNodeBetweenLayers": "68", "elk.spacing.nodeNode": "42",
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF"
},
children: graph.nodes.map(node => ({id: node.id, width: 300, height: taskflow_node_state(node, components, frame, gallery_state) ? 230 : aggregate ? 174 : 142})),
children: graph.nodes.map(node => {
const attributes = Object.keys(node.attributes ?? {}).length;
const base_height = taskflow_node_state(node, components, frame, gallery_state) ? 250 : aggregate ? 198 : 172;
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]}))
}).then(layout => {
if (cancelled) return;
@@ -1436,29 +1607,15 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
return {
id: node.id,
position: {x: position?.x ?? 0, y: position?.y ?? 0},
data: {label: <div className="taskflowNodeLabel nodrag nopan" title={`${node.name}\n${node.id}`}>
<header><strong>{taskflow_node_name(node.name)}</strong><i> {level + 1}</i></header>
<code>{node.name}</code>
{summary ? <>
<span>{node.type} · {summary.executed}/{summary.frames} · W {summary.workers.join(", ") || "--"}</span>
<span> {milliseconds(summary.duration.average)} ± {milliseconds(summary.duration.variability)} · P95 {milliseconds(summary.duration.p95)}</span>
<span> {milliseconds(summary.queue.average)} ± {milliseconds(summary.queue.variability)} · P99 {milliseconds(summary.queue.p99)}</span>
<span>CPU {milliseconds(summary.cpu.average)} · Observer {milliseconds(summary.observer.average)}</span>
</> : <><span>{node.type} · W{sample?.worker_id ?? "--"}</span>
<span> {sample ? milliseconds(duration) : "未执行"} · {sample ? milliseconds(wait) : "--"}</span>
<span>CPU {sample ? milliseconds(sample.cpu_duration_ms) : "--"} · {sample ? milliseconds(Math.max(0, duration - 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>},
className: summary ? `taskflowNode ${{stable: "taskflowNodeStable", variable: "taskflowNodeVariable",
sourcePosition: Position.Bottom,
targetPosition: Position.Top,
data: {label: <Taskflow_Node_Label node={node} sample={sample} summary={summary}
state={state} level={level}/>},
className: `${summary ? `taskflowNode ${{stable: "taskflowNodeStable", variable: "taskflowNodeVariable",
missing: "taskflowNodeMissing", long_tail: "taskflowNodeLongTail"}[summary.stability]}`
: sample ? wait > duration && wait > .1 ? "taskflowNode taskflowNodeWaiting" : "taskflowNode taskflowNodeExecuted" : "taskflowNode"
: sample ? wait > duration && wait > .1 ? "taskflowNode taskflowNodeWaiting" : "taskflowNode taskflowNodeExecuted" : "taskflowNode"}
${node.name.includes(".paint.data") ? "taskflowNodeActualPaint" : ""}
${node.name.includes(".prepare.data") ? "taskflowNodePrepareData" : ""}`.trim()
};
}));
set_edges(flow_edges);
@@ -1468,7 +1625,7 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
const copy_topology = async () => {
const native_ids = new Set(graph.nodes.map(node => node.native_id));
try {
await navigator.clipboard.writeText(JSON.stringify({
await copy_text(JSON.stringify({
sequence: frame?.sequence,
correlation_id: frame?.correlation_id,
markers: frame?.markers ?? {},
@@ -1483,13 +1640,13 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
}
catch { set_copy_state("复制失败"); }
};
const flow = <ReactFlow key={`${graph.stage}:${graph.submitted_ms}`} nodes={nodes} edges={edges}
const flow = <ReactFlow key={`${graph.stage}:${graph.submitted_ms}:${fullscreen ? "fullscreen" : "normal"}`} 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}>
<Background color="#233956" gap={22}/><MiniMap pannable zoomable/><Controls showInteractive={false}/>
</ReactFlow>;
return <section className={`taskflowDagSection${standalone ? " taskflowDagSectionFill" : ""}`}><header className="taskflowDagToolbar">
return <section className={`taskflowDagSection${fullscreen ? " taskflowDagFullscreen" : ""}`}><header className="taskflowDagToolbar">
<div className="taskflowLegend" aria-label="Taskflow 节点颜色说明">
{aggregate ? <><span><i className="taskflowLegendStable"/></span>
<span><i className="taskflowLegendVariable"/></span>
@@ -1499,15 +1656,18 @@ function Taskflow_Dag({graph, executions, frame, aggregate, components, gallery_
<span><i className="taskflowLegendExecuted"/></span>
<span><i className="taskflowLegendWaiting"/></span></>}
</div>
<button onClick={() => void copy_topology()}>{copy_state}</button>
</header>{standalone ? <div className="taskflowDag taskflowDagFill">{flow}</div> : <ResizableBox className="taskflowResizable taskflowDagResizable"
<div className="taskflowDagActions">
<button onClick={() => set_fullscreen(value => !value)}>{fullscreen ? "退出全屏" : "全屏"}</button>
<button onClick={() => void copy_topology()}>{copy_state}</button>
</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>;
}
function Taskflow_Frame_Pane({plot, components, standalone = false}: {plot: Plot; components: Component[]; standalone?: boolean}) {
function Taskflow_Frame_Pane({plot, components}: {plot: Plot; components: Component[]}) {
const [frame_count, set_frame_count] = useState(8);
const [scene_response, set_scene_response] = useState<Taskflow_Frame_Response | null>(null);
const [frame_index, set_frame_index] = useState(0);
@@ -1594,12 +1754,11 @@ function Taskflow_Frame_Pane({plot, components, standalone = false}: {plot: Plot
return {total, frame_target, background, cache_targets, taskflow,
coordination: Math.max(0, total - frame_target - background - cache_targets - taskflow)};
}, [frame]);
return <section className={`workspacePane${standalone ? " taskflowStandalonePane" : ""}`}><Workspace_Header plot={plot} label="Taskflow 帧分析" count={response?.captured ?? 0} busy={busy} on_refresh={() => void load()}/>
return <section className="workspacePane"><Workspace_Header plot={plot} label="Taskflow 帧分析" count={response?.captured ?? 0} busy={busy} on_refresh={() => void load()}/>
<div className="workspaceBody taskflowFramePane">
<section className="taskflowCaptureBar"><label><input type="number" min={1} max={120} value={frame_count}
onChange={event => set_frame_count(Math.max(1, Math.min(120, Number(event.target.value) || 1)))}/></label>
<button disabled={busy || Boolean(scene_response && scene_response.requested > 0 && !scene_response.complete)} onClick={() => void capture()}>{busy ? "提交中…" : "捕获完整 Scene 帧流水线"}</button>
<button onClick={() => window.open(`/taskflow/${encodeURIComponent(plot.id)}`, "_blank", "noopener")}></button>
<span>{response ? `${response.captured}/${response.requested}${response.complete ? " · 已完成" : ` · 还需 ${response.remaining}`}` : "按需捕获,未请求时 Observer 不写入逐帧数据"}</span></section>
{error ? <p className="taskflowError">{error}</p> : null}
{frame ? <>
@@ -1623,14 +1782,16 @@ function Taskflow_Frame_Pane({plot, components, standalone = false}: {plot: Plot
<div><dt></dt><dd>{[...aggregate.nodes.values()].filter(node => node.stability === "long_tail").length}</dd></div>
<div><dt>Topology ± </dt><dd>{milliseconds(aggregate.wall.average)} ± {milliseconds(aggregate.wall.variability)}</dd></div>
<div><dt>Topology P95 / P99</dt><dd>{milliseconds(aggregate.wall.p95)} / {milliseconds(aggregate.wall.p99)}</dd></div>
</dl><Taskflow_Dag graph={aggregate.graph} executions={[]} aggregate={aggregate} components={components} gallery_state={gallery_state} standalone={standalone}/></>
</dl><Taskflow_Dag graph={aggregate.graph} executions={[]} aggregate={aggregate} components={components} gallery_state={gallery_state}/></>
: view_mode === "single" && graph && graph_analysis ? <><dl className="taskflowGraphSummary">
<div><dt></dt><dd>{graph.stage}</dd></div><div><dt>DAG </dt><dd>{graph.nodes.length}</dd></div>
<div title="提交 Taskflow 到整个 Topology 完成的墙钟时间,包含 Executor 排队。"><dt>Topology </dt><dd>{milliseconds(graph_analysis.wall_time)}</dd></div>
<div title="只累计叶子任务;Taskflow Module 是包住子图的区间,不能与子节点重复相加。"><dt></dt><dd>{milliseconds(graph_analysis.execution_time)}</dd></div>
<div title="所有 Module Observer 区间之和,仅显示子图包络;不计入叶子任务总和。"><dt>Module </dt><dd>{milliseconds(graph_analysis.module_envelope_time)}</dd></div>
<div title="叶子任务通过 worker 线程 CPU 时钟测得,不包含 OS 抢占和任务内睡眠。"><dt> CPU</dt><dd>{milliseconds(graph_analysis.cpu_execution_time)}</dd></div>
<div title="任务体墙钟减去 Windows 线程 CPU 计费时间。短任务受约 15.625 ms 计费粒度影响,只能视为估算,不能单独证明锁等待或 OS 抢占。"><dt> CPU </dt><dd>{milliseconds(graph_analysis.descheduled_time)}</dd></div>
<div title="叶子任务独占 Taskflow Worker 片段的线程 CPU 时间;corun/corun_until 期间执行的其他任务不会归到外层节点。Windows GetThreadTimes 仍是低分辨率计费时钟。"><dt>线 CPU{graph_analysis.cpu_time_coarse ? "(低分辨率)" : ""}</dt><dd>{milliseconds(graph_analysis.cpu_execution_time)}</dd></div>
<div title="Windows 使用 QueryThreadCycleTime 记录独占片段 CPU 活动量。周期数不直接换算为秒,但短任务即使 GetThreadTimes 显示 0 也能确认确实消耗过 CPU。"><dt> CPU </dt><dd>{cpu_cycles(graph_analysis.cpu_cycles_total)}</dd></div>
<div title="Task_Graph::corun/corun_until 主动让出当前 Worker、由同一 Worker 协作执行其他 Taskflow 工作的墙钟时间。"><dt></dt><dd>{milliseconds(graph_analysis.cooperative_wait_time)}</dd></div>
<div title="任务体墙钟减去显式 cooperative wait 和线程 CPU 时间。它可能包含锁/驱动/系统调用等待以及 Windows CPU 计时量化误差,因此不是 OS 抢占时间。"><dt>{graph_analysis.cpu_time_coarse ? "(估算)" : ""}</dt><dd>{milliseconds(graph_analysis.unattributed_wall_time)}</dd></div>
<div title="Topology 墙钟中至少有一个节点任务体正在执行的区间并集。"><dt></dt><dd>{milliseconds(graph_analysis.body_wall_time)}</dd></div>
<div title="Topology 墙钟中只有 Observer on_entry/on_exit 在执行、没有任务体执行的区间。"><dt>Observer </dt><dd>{milliseconds(graph_analysis.observer_wall_time)}</dd></div>
<div title="所有节点 Observer entry 墙钟耗时之和,包含并行重叠。"><dt>Observer entry </dt><dd>{milliseconds(graph_analysis.observer_entry_time)}</dd></div>
@@ -1645,7 +1806,7 @@ function Taskflow_Frame_Pane({plot, components, standalone = false}: {plot: Plot
<div title="Observer 时间区间内同时执行的最大节点数量。"><dt></dt><dd>{graph_analysis.maximum_parallelism}</dd></div>
<div title="同一层节点没有相互依赖,可以并行;串行图的并行层数量为零。"><dt> / </dt><dd>{graph_analysis.layer_count} / {graph_analysis.parallel_layer_count}</dd></div>
<div title={graph_analysis.longest?.node_id}><dt></dt><dd>{graph_analysis.longest ? milliseconds(graph_analysis.longest.duration_ms) : "--"}</dd></div>
<div title={graph_analysis.longest_non_cpu?.node_id}><dt> CPU </dt><dd>{graph_analysis.longest_non_cpu ? `${taskflow_node_name(graph_analysis.longest_non_cpu.node_id.split("/").at(-1) ?? graph_analysis.longest_non_cpu.node_id)} · ${milliseconds(Math.max(0, graph_analysis.longest_non_cpu.duration_ms - graph_analysis.longest_non_cpu.cpu_duration_ms))}` : "--"}</dd></div>
<div title={graph_analysis.longest_unattributed?.node_id}><dt></dt><dd>{graph_analysis.longest_unattributed ? `${taskflow_node_name(graph_analysis.longest_unattributed.node_id.split("/").at(-1) ?? graph_analysis.longest_unattributed.node_id)} · ${graph_analysis.longest_unattributed.cpu_time_coarse ? "≈ " : ""}${milliseconds(Math.max(0, graph_analysis.longest_unattributed.duration_ms - graph_analysis.longest_unattributed.cooperative_wait_ms - graph_analysis.longest_unattributed.cpu_duration_ms))}` : "--"}</dd></div>
{graph.stage === "render_2d.paint" && paint_analysis ? <>
<div title="同一物理帧 paint_started 到 paint_finished 的完整区间。"><dt> Paint </dt><dd>{milliseconds(paint_analysis.total)}</dd></div>
<div title="主帧 ensure_size 与完整像素清屏。"><dt></dt><dd>{milliseconds(paint_analysis.frame_target)}</dd></div>
@@ -1655,7 +1816,7 @@ function Taskflow_Frame_Pane({plot, components, standalone = false}: {plot: Plot
<div title="Paint 总墙钟减去其余四个互斥区间。"><dt>Paint </dt><dd>{milliseconds(paint_analysis.coordination)}</dd></div>
</> : null}
<div><dt></dt><dd>{graph.completed ? "完成" : "未完成"}</dd></div>
</dl><Taskflow_Dag graph={graph} executions={frame.executions} frame={frame} components={components} gallery_state={gallery_state} standalone={standalone}/></> : <div className="diagnosticEmpty"><strong> Taskflow </strong><span></span></div>}
</dl><Taskflow_Dag graph={graph} executions={frame.executions} frame={frame} components={components} gallery_state={gallery_state}/></> : <div className="diagnosticEmpty"><strong> Taskflow </strong><span></span></div>}
</> : <div className="diagnosticEmpty"><strong> Taskflow </strong><span> N DAG Observer </span></div>}
</div></section>;
}
@@ -1912,13 +2073,9 @@ function load_workspace_model() {
}
export function App() {
const taskflow_route = /^\/taskflow\/([^/]+)\/?$/.exec(window.location.pathname);
const taskflow_route_id = taskflow_route ? decodeURIComponent(taskflow_route[1]) : null;
const [plots, set_plots] = useState<Plot[]>([]); const [category, set_category] = useState("全部"); const [selected, set_selected] = useState<Plot | null>(null);
use_selected_plot_diagnostics(selected);
const gallery_videos = use_gallery_videos(taskflow_route_id
? plots.filter(plot => plot.id === taskflow_route_id)
: plots);
const gallery_videos = use_gallery_videos(plots);
const [execution_policies, set_execution_policies] = useState<Plot_Execution_Policies>({});
const [schema, set_schema] = useState<Schema | null>(null);
const [frame_diagnostics, set_frame_diagnostics] = useState<Frame_Diagnostics | null>(null);
@@ -1934,13 +2091,8 @@ export function App() {
])));
}); }, []);
useEffect(() => {
if (taskflow_route_id) {
const target = plots.find(plot => plot.id === taskflow_route_id);
if (target && selected?.id !== target.id) set_selected(target);
return;
}
if (!selected && plots[0]) set_selected(plots[0]);
}, [plots, selected?.id, taskflow_route_id]);
}, [plots, selected?.id]);
useEffect(() => {
set_frame_diagnostics(null);
const receive = (event: Event) => {
@@ -1986,14 +2138,6 @@ export function App() {
if (category === "2D" || category === "3D") return plots.filter(plot => plot.dimension === category);
return plots;
}, [category, plots]);
if (taskflow_route_id) return <main className="standaloneTaskflowPage">
<header className="standaloneTaskflowHeader"><div><span className="eyebrow">AETHERA TASKFLOW</span><h1></h1></div>
<label><select value={selected?.id ?? taskflow_route_id} onChange={event => window.location.assign(`/taskflow/${encodeURIComponent(event.target.value)}`)}>
{plots.map(plot => <option key={plot.id} value={plot.id}>{plot.dimension} · {plot_labels[plot.id] ?? plot.title}</option>)}</select></label>
<button onClick={() => window.location.assign("/")}></button></header>
{selected ? <Taskflow_Frame_Pane plot={selected} components={schema?.components ?? []} standalone/>
: <div className="diagnosticEmpty"><strong></strong></div>}
</main>;
const gallery = <section className="galleryPanel"><header className="topbar"><div><span className="eyebrow">AETHERA </span><h1></h1></div><div className="topbarActions">
{selected ? <span className="selectionName"> <strong>{plot_labels[selected.id] ?? selected.title}</strong></span> : <span className="muted"> DAG </span>}
<button onClick={() => { localStorage.removeItem(workspace_layout_key); localStorage.removeItem(gallery_layout_key);
+27 -9
View File
@@ -202,6 +202,7 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
.taskflowDagSection { min-width: 640px; overflow: visible; border: 1px solid #213653; border-radius: 10px; background: #07101c; }
.taskflowDagToolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 11px; border-bottom: 1px solid #213653; background: #0c1727; }
.taskflowDagToolbar button { flex: none; padding: 6px 9px; color: #b9cce3; border: 1px solid #36516f; border-radius: 6px; background: #132238; cursor: pointer; }
.taskflowDagActions { display: flex; flex: none; align-items: center; gap: 9px; }
.taskflowLegend { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; color: #8298b4; font-size: 10px; }
.taskflowLegend span { display: inline-flex; align-items: center; gap: 5px; }
.taskflowLegend i { width: 10px; height: 10px; border: 1px solid #304766; border-radius: 3px; background: #0e1a2b; }
@@ -260,16 +261,11 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
.taskflowTable th { color: #71839e; font-weight: 500; }
.taskflowTable td { color: #c7d5e7; font-family: ui-monospace, monospace; }
.standaloneTaskflowPage { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; overflow: hidden; background: #050912; }
.standaloneTaskflowPage > .workspacePane { flex: 1; min-height: 0; }
.taskflowStandalonePane .taskflowFramePane { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.taskflowDagSectionFill { display: flex; flex: 1; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
.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 .taskflowDagFill { flex: 1; min-height: 0; cursor: zoom-out; }
.taskflowDagSection:not(.taskflowDagFullscreen) .taskflowDag { cursor: zoom-in; }
.taskflowDagFill { flex: 1; min-height: 0; }
.standaloneTaskflowHeader { display: flex; flex: none; align-items: center; gap: 18px; padding: 14px 18px; border-bottom: 1px solid #24324a; background: #07101b; }
.standaloneTaskflowHeader h1 { font-size: 26px; }
.standaloneTaskflowHeader label { display: flex; align-items: center; gap: 8px; margin-left: auto; color: #91a5c0; font-size: 11px; }
.standaloneTaskflowHeader select, .standaloneTaskflowHeader button { padding: 8px 10px; color: #dce8f8; border: 1px solid #304664; border-radius: 7px; background: #101c2d; }
.standaloneTaskflowHeader button { cursor: pointer; }
.componentCard { margin-bottom: 13px; overflow: hidden; border: 1px solid #213653; border-radius: 11px; background: #0a1422; }
@@ -326,3 +322,25 @@ canvas { display: block; width: 100%; height: 100%; background: #070d18; }
.stateList > div { grid-template-columns: 1fr; gap: 6px; }
.stateList dd { text-align: left; }
}
/* Taskflow diagnostic nodes: text selection + explicit node copying. */
.taskflowNodeLabel, .taskflowNodeLabel code, .taskflowNodeLabel span,
.taskflowNodeLabel strong, .taskflowNodeState pre, .taskflowNodeState summary {
-webkit-user-select: text !important;
user-select: text !important;
}
.taskflowNodeTitle { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 6px; }
.taskflowNodeTitle strong { min-width: 0; }
.taskflowNodeHeaderActions { display: flex; flex: none; align-items: center; gap: 5px; }
.taskflowNodeCopy { padding: 2px 6px; color: #90a8c4; border: 1px solid #304766; border-radius: 5px; background: #091522; cursor: pointer; font: 8px/1.25 ui-monospace, monospace; user-select: none !important; }
.taskflowNodeCopy:hover { color: #dce8f8; border-color: #5ce4c2; }
.taskflowPhaseBadge { flex: none; padding: 2px 5px; border: 1px solid #3b526d; border-radius: 99px; color: #91a5c0; background: #0a1523; font: 700 8px/1 ui-monospace, monospace; letter-spacing: .04em; user-select: none !important; }
.taskflowPhaseBadge-prepare { color: #e4bd72; border-color: #70552d; background: #241a0d; }
.taskflowPhaseBadge-paint { color: #72e3c8; border-color: #276b5b; background: #071f1a; }
.taskflowPhaseBadge-completion { color: #b9a4ef; border-color: #564783; background: #18132a; }
.taskflowNodeActualPaint { border-width: 2px !important; border-color: #35bda0 !important; box-shadow: inset 0 0 0 1px #35bda026, 0 8px 22px #0006 !important; }
.taskflowNodeActualPaint .taskflowNodeTitle strong { color: #e8fff9 !important; font-size: 13px; }
.taskflowNodeActualPaint .taskflowPhaseBadge-paint { color: #07110f; border-color: #5ce4c2; background: #5ce4c2; }
.taskflowNodePrepareData .taskflowNodeTitle strong { color: #f0d29a !important; }
.taskflowDagFullscreen .taskflowDagFill,
.taskflowDagSection:not(.taskflowDagFullscreen) .taskflowDag { cursor: default; }