优化
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 时间。 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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{}; /* 当前编码结果所有权。 */
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user