This commit is contained in:
2026-08-16 00:07:05 +08:00
parent 7a94264c13
commit 893be8d2c6
21 changed files with 363 additions and 211 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ install(TARGETS Renderive_Kernel
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
install(DIRECTORY "${Renderive_Kernel_source_dir}/renderive"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp")
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.inl")
if (RENDERIVE_BUILD_TESTS)
set(Renderive_Kernel_test_dir "${CMAKE_CURRENT_LIST_DIR}/tests")
append_glob_source(Renderive_Kernel_test_sources "${Renderive_Kernel_test_dir}")
@@ -224,7 +224,10 @@ Frame_Analysis analyze_frame(const Render_Plan& plan, const Frame_Snapshot& fram
}
auto& analysis = result.nodes[index];
const auto& execution = frame.node_executions[index];
analysis.dependency_ready_time_ns = execution.ready_time_ns;
analysis.dependency_ready_offset_ns =
execution.ready_time_ns >= frame.render_start_ns
? execution.ready_time_ns - frame.render_start_ns
: 0;
analysis.scheduler_wait_ns = execution.start_time_ns >= execution.ready_time_ns
? execution.start_time_ns - execution.ready_time_ns
: 0;
@@ -23,7 +23,7 @@ struct Node_Frame_Analysis {
std::uint64_t gpu_execution_duration_ns{};
std::uint64_t start_offset_ns{};
std::uint64_t end_offset_ns{};
std::uint64_t dependency_ready_time_ns{};
std::uint64_t dependency_ready_offset_ns{};
std::uint64_t scheduler_wait_ns{};
double work_contribution{};
double critical_path_contribution{};
@@ -64,8 +64,17 @@ struct Render_Graph_Runtime::State
}
void execute(std::span<Node_Execution* const> execution_slots,
Execute_Node execute) {
if (running.exchange(true, std::memory_order_acq_rel))
throw std::logic_error("render graph runtime is already executing");
{
std::lock_guard lock(lifecycle_mutex);
if (running.load(std::memory_order_acquire))
throw std::logic_error("render graph runtime is already executing");
cancellation_requested.store(false, std::memory_order_release);
{
std::lock_guard cancellation_lock(cancellation_mutex);
cancellation_exception = nullptr;
}
running.store(true, std::memory_order_release);
}
std::exception_ptr execution_error;
try {
@@ -119,23 +128,31 @@ struct Render_Graph_Runtime::State
std::fill(external_operations.begin(),
external_operations.end(), External_Operation{});
}
cancellation_requested.store(false, std::memory_order_release);
{
std::lock_guard lock(cancellation_mutex);
cancellation_exception = nullptr;
std::lock_guard lock(lifecycle_mutex);
cancellation_requested.store(false, std::memory_order_release);
{
std::lock_guard cancellation_lock(cancellation_mutex);
cancellation_exception = nullptr;
}
running.store(false, std::memory_order_release);
}
running.store(false, std::memory_order_release);
}
void cancel_pending(std::exception_ptr reason) noexcept {
auto error = make_cancellation_error(std::move(reason));
{
std::lock_guard lock(cancellation_mutex);
if (!cancellation_exception)
cancellation_exception = error;
else
error = cancellation_exception;
std::lock_guard lifecycle_lock(lifecycle_mutex);
if (!running.load(std::memory_order_acquire))
return;
{
std::lock_guard cancellation_lock(cancellation_mutex);
if (!cancellation_exception)
cancellation_exception = error;
else
error = cancellation_exception;
}
cancellation_requested.store(true, std::memory_order_release);
}
cancellation_requested.store(true, std::memory_order_release);
cancel_external_operations(error);
}
std::exception_ptr cancellation_error() noexcept {
@@ -291,6 +308,7 @@ struct Render_Graph_Runtime::State
std::vector<External_Operation> external_operations;
std::mutex cancellation_mutex;
std::exception_ptr cancellation_exception;
std::mutex lifecycle_mutex;
std::atomic_bool running{};
std::atomic_bool failed{};
std::atomic_bool cancellation_requested{};
+25 -56
View File
@@ -196,9 +196,11 @@ void Scene_Base::Renderable_Editor::attach(Renderable renderable) {
}
if (!scene_.dependency_resolver_.contains(id))
scene_.dependency_resolver_.attach(id);
if (auto* composition = scene_.composition_relationships())
composition->attach(id);
auto cache = scene_.make_renderable_color_cache();
if (scene_.raster_capabilities_)
scene_.raster_capabilities_->attach(id);
auto cache = scene_.raster_capabilities_
? scene_.raster_capabilities_->make_renderable_color_cache()
: std::shared_ptr<Color_Cache>{};
if (!scene_.renderables_.try_emplace(id, renderable).second)
throw std::logic_error("renderable id is already attached");
try {
@@ -275,7 +277,9 @@ Scene_2D_Base::Scene_2D_Base(std::pmr::memory_resource& memory_resource)
Scene_2D_Base::Scene_2D_Base(std::pmr::memory_resource& memory_resource,
std::unique_ptr<Scene_Base::Impl> impl)
: Scene_Base(memory_resource, std::move(impl)),
display_resolver_(this->memory_resource()) {}
display_resolver_(this->memory_resource()) {
bind_raster_capabilities(*this);
}
Scene_2D_Base::Attach_Builder Scene_2D_Base::attach_builder() {
return Attach_Builder(*this);
@@ -512,8 +516,8 @@ void Scene_Base::cleanup_detached_topology_locked() {
std::to_string(id));
dependency_resolver_.erase(id);
}
if (auto* composition = composition_relationships())
composition->cleanup();
if (raster_capabilities_)
raster_capabilities_->cleanup();
}
void Scene_Base::validate_structure_locked() {
for (const auto& [id, renderable] : renderables_) {
@@ -536,8 +540,8 @@ void Scene_Base::validate_structure_locked() {
"dependency graph references detached renderable " +
std::to_string(id));
}
if (const auto* composition = composition_relationships())
composition->validate();
if (raster_capabilities_)
raster_capabilities_->validate();
}
[[noreturn]] void Scene_Base::structure_fail_fast(const char* stage, std::exception_ptr exception) noexcept {
std::fprintf(stderr, "Renderive scene structure failure during %s", stage ? stage : "unknown stage");
@@ -641,9 +645,8 @@ Scene_Base::Topology_Snapshot Scene_Base::topology_snapshot() const {
std::lock_guard lock(model_mutex_);
Topology_Snapshot snapshot;
const auto dependency = dependency_resolver_.resolve();
const auto* composition = composition_relationships();
const auto display = composition
? composition->resolve()
const auto display = raster_capabilities_
? raster_capabilities_->resolve()
: renderive::scene::dependency::Resolution<
Renderable_Id>{};
const auto& ids = dependency.order;
@@ -675,9 +678,8 @@ Scene_Base::Topology_Snapshot Scene_Base::topology_snapshot() const {
std::vector<Scene_Base::Const_Renderable> Scene_Base::paint_order_snapshot() const {
std::lock_guard lock(model_mutex_);
std::vector<Const_Renderable> result;
const auto* composition = composition_relationships();
auto display = composition
? composition->resolve()
auto display = raster_capabilities_
? raster_capabilities_->resolve()
: renderive::scene::dependency::Resolution<
Renderable_Id>{};
if (display.order.empty())
@@ -691,9 +693,8 @@ std::shared_ptr<Frame_Render_Snapshot> Scene_Base::snapshot_live_model() {
auto snapshot = std::make_shared<Frame_Render_Snapshot>();
std::lock_guard lock(model_mutex_);
const auto dependency = dependency_resolver_.resolve();
const auto* composition = composition_relationships();
const auto display = composition
? composition->resolve()
const auto display = raster_capabilities_
? raster_capabilities_->resolve()
: renderive::scene::dependency::Resolution<
Renderable_Id>{};
const auto& dependency_order = dependency.order;
@@ -774,7 +775,8 @@ std::shared_ptr<Frame_Render_Snapshot> Scene_Base::capture_live_frame() {
auto snapshot = snapshot_live_model();
snapshot->scene_state_revision_ = acquire_scene_state();
capture_scene_state(*snapshot);
snapshot->viewport = frame_viewport();
if (raster_capabilities_)
snapshot->viewport = raster_capabilities_->frame_viewport();
return snapshot;
}
@@ -855,44 +857,11 @@ const Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy() const {
return frame_control_strategy_impl();
}
Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy_impl() {
throw std::logic_error("frame control strategy is not available");
}
const Frame_Control_Strategy_Base& Scene_Base::frame_control_strategy_impl() const {
throw std::logic_error("frame control strategy is not available");
}
std::shared_ptr<Color_Cache> Scene_Base::make_renderable_color_cache() {
return {};
}
Frame_Viewport Scene_Base::frame_viewport() const {
return {};
}
std::uint64_t Scene_Base::acquire_scene_state() {
return 0;
}
void Scene_Base::capture_scene_state(Frame_Render_Snapshot&) const {}
renderive::scene::detail::Composition_Relationships*
Scene_Base::composition_relationships() noexcept {
auto* scene_2d = dynamic_cast<Scene_2D_Base*>(this);
return scene_2d
? static_cast<
renderive::scene::detail::Composition_Relationships*>(
scene_2d)
: nullptr;
}
const renderive::scene::detail::Composition_Relationships*
Scene_Base::composition_relationships() const noexcept {
const auto* scene_2d = dynamic_cast<const Scene_2D_Base*>(this);
return scene_2d
? static_cast<const renderive::scene::detail::
Composition_Relationships*>(scene_2d)
: nullptr;
void Scene_Base::bind_raster_capabilities(
renderive::scene::detail::Raster_Capabilities& capabilities) noexcept {
if (raster_capabilities_ != nullptr)
std::terminate();
raster_capabilities_ = &capabilities;
}
void Scene_2D_Base::attach(Renderable_Id id) {
+15 -11
View File
@@ -44,6 +44,11 @@ struct Composition_Relationships {
virtual dependency::Resolution<Renderable_Id> resolve() const = 0;
};
struct Raster_Capabilities : Composition_Relationships {
virtual std::shared_ptr<Color_Cache> make_renderable_color_cache() = 0;
virtual Frame_Viewport frame_viewport() const = 0;
};
} // namespace renderive::scene::detail
class Scene_Compositor {
@@ -204,12 +209,10 @@ protected:
Impl& d_func() noexcept { return *impl_; }
const Impl& d_func() const noexcept { return *impl_; }
virtual Frame_Control_Strategy_Base& frame_control_strategy_impl();
virtual const Frame_Control_Strategy_Base& frame_control_strategy_impl() const;
virtual std::shared_ptr<Color_Cache> make_renderable_color_cache();
virtual Frame_Viewport frame_viewport() const;
virtual std::uint64_t acquire_scene_state();
virtual void capture_scene_state(Frame_Render_Snapshot& snapshot) const;
virtual Frame_Control_Strategy_Base& frame_control_strategy_impl() = 0;
virtual const Frame_Control_Strategy_Base& frame_control_strategy_impl() const = 0;
virtual std::uint64_t acquire_scene_state() = 0;
virtual void capture_scene_state(Frame_Render_Snapshot& snapshot) const = 0;
std::unique_lock<std::recursive_mutex> lock_render_idle();
[[nodiscard]] bool is_render_execution_context() const noexcept;
bool consume_model_dirty() noexcept;
@@ -271,10 +274,8 @@ private:
const Renderable& renderable) const;
void validate_structure_locked();
void cleanup_detached_topology_locked();
renderive::scene::detail::Composition_Relationships*
composition_relationships() noexcept;
const renderive::scene::detail::Composition_Relationships*
composition_relationships() const noexcept;
void bind_raster_capabilities(
renderive::scene::detail::Raster_Capabilities& capabilities) noexcept;
[[noreturn]] static void structure_fail_fast(const char* stage, std::exception_ptr exception = {}) noexcept;
inline static thread_local Scene_Base* active_execution_scene_{};
@@ -289,6 +290,7 @@ private:
std::pmr::unordered_map<Renderable_Id, Renderable> renderables_;
renderive::scene::dependency::Dependency_Resolver<Renderable_Id>
dependency_resolver_;
renderive::scene::detail::Raster_Capabilities* raster_capabilities_{};
std::pmr::unordered_map<Renderable_Id, std::shared_ptr<Color_Cache>> color_caches_;
mutable std::mutex model_mutex_;
@@ -313,7 +315,7 @@ private:
class Scene_2D_Base
: public Scene_Base,
private renderive::scene::detail::Composition_Relationships {
private renderive::scene::detail::Raster_Capabilities {
public:
class Renderable_Editor
: public renderive::scene_inheritance::Editor_Node<
@@ -366,6 +368,8 @@ protected:
};
Scene_2D_Base(std::pmr::memory_resource& memory_resource,
std::unique_ptr<Scene_Base::Impl> impl);
virtual std::shared_ptr<Color_Cache> make_renderable_color_cache() override = 0;
virtual Frame_Viewport frame_viewport() const override = 0;
private:
friend class Scene_Base;
@@ -89,7 +89,7 @@ TEST(render_dag_test, analysis_derives_wait_critical_path_and_parallel_overlap_f
const auto analysis = analyze_frame(*plan, snapshot);
ASSERT_EQ(analysis.nodes.size(), 3u);
EXPECT_EQ(analysis.nodes[2].dependency_ready_time_ns, 150u);
EXPECT_EQ(analysis.nodes[2].dependency_ready_offset_ns, 50u);
EXPECT_EQ(analysis.nodes[2].scheduler_wait_ns, 10u);
EXPECT_EQ(analysis.total_render_duration_ns, 100u);
EXPECT_EQ(analysis.total_work_duration_ns, 100u);
@@ -280,6 +280,25 @@ TEST(render_graph_runtime_test,
EXPECT_EQ(source.operation().status(), External_Operation_Status::cancelled);
}
TEST(render_graph_runtime_test, idle_cancellation_does_not_arm_next_execution) {
const auto plan = two_node_plan();
std::vector<Node_Execution> execution_storage;
auto slots = execution_slots(*plan, execution_storage);
renderive::render_graph::detail::Render_Graph_Runtime runtime(*plan);
runtime.cancel_pending();
std::atomic<int> executed{};
EXPECT_NO_THROW(runtime.execute(
slots, [&](std::size_t, Node_Execution_Metrics*) {
executed.fetch_add(1, std::memory_order_relaxed);
return Node_Execution_Result::completed();
}));
EXPECT_EQ(executed.load(std::memory_order_relaxed), 2);
EXPECT_EQ(execution_storage[0].status, Node_Execution_Status::complete);
EXPECT_EQ(execution_storage[1].status, Node_Execution_Status::complete);
}
TEST(external_operation_test, completion_commit_is_exclusive_with_cancellation) {
External_Operation_Source source;
const auto operation = source.operation();
+4
View File
@@ -26,7 +26,11 @@ if (@RENDERIVE_INSTALL_3D@)
find_dependency(tinyobjloader CONFIG)
find_dependency(ZLIB)
if (WIN32)
set(_Renderive_saved_module_path "${CMAKE_MODULE_PATH}")
list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules")
find_dependency(PThreads4W)
set(CMAKE_MODULE_PATH "${_Renderive_saved_module_path}")
unset(_Renderive_saved_module_path)
endif ()
endif ()
+6
View File
@@ -34,6 +34,12 @@ install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/RenderiveConfigVersion.cmake"
DESTINATION "${Renderive_install_cmake_dir}")
if (RENDERIVE_INSTALL_3D AND WIN32)
install(FILES
"${PROJECT_SOURCE_DIR}/render_3D/cmake/module/FindPThreads4W.cmake"
DESTINATION "${Renderive_install_cmake_dir}/modules")
endif ()
if (RENDERIVE_INSTALL_QT)
install(FILES "${PROJECT_SOURCE_DIR}/export.h"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
-37
View File
@@ -1,9 +1,5 @@
#pragma once
#include <algorithm>
#include <random>
#include <span>
#include <string_view>
#include <vector>
#include <QBrush>
#include <QByteArray>
#include <QColor>
@@ -147,37 +143,4 @@ inline Qt::Orientation to_qt_orientation(Orientation orientation) {
inline Time_Of_Day qt_to_time_of_day(const QTime& time) {
return {time.isValid() ? time.msecsSinceStartOfDay() : -1};
}
namespace detail {
inline std::default_random_engine& mock_data_random_engine() {
static thread_local std::default_random_engine engine(std::random_device{}());
return engine;
}
} // namespace detail
inline void fill_data(Range value_range, std::span<double> output) {
if (value_range.origin > value_range.target)
std::swap(value_range.origin, value_range.target);
std::uniform_real_distribution<double> dist(value_range.origin, value_range.target);
for (double& value : output)
value = dist(detail::mock_data_random_engine());
}
inline std::vector<double> get_data(Range value_range, int size) {
std::vector<double> ret(static_cast<std::size_t>(std::max(0, size)));
fill_data(value_range, ret);
return ret;
}
inline double get_data(Range value_range) {
if (value_range.origin > value_range.target)
std::swap(value_range.origin, value_range.target);
std::uniform_real_distribution<double> dist(value_range.origin, value_range.target);
return dist(detail::mock_data_random_engine());
}
inline std::vector<double> get_data(int value, int size) {
return std::vector<double>(size, value);
}
inline std::vector<double> get_colored_data(int size) {
std::vector<double> ret(size);
for (int i = 0; i < size; ++i)
ret[i] = i;
return ret;
}
} // namespace renderive
+73 -21
View File
@@ -1,5 +1,16 @@
include_guard(GLOBAL)
rcl_get_effective_install_dir(render_3D::PThreads4W PTHREADS4W_ROOT)
# Self-contained PThreads4W discovery used both by the source tree and by the
# installed Renderive package. The build infrastructure may define
# PTHREADS4W_ROOT, but consumers only need to provide PThreads4W_ROOT,
# PTHREADS4W_ROOT, the corresponding environment variable, or CMAKE_PREFIX_PATH.
set(_PThreads4W_roots)
foreach(_root IN ITEMS "${PThreads4W_ROOT}" "${PTHREADS4W_ROOT}" "$ENV{PThreads4W_ROOT}" "$ENV{PTHREADS4W_ROOT}")
if (_root)
list(APPEND _PThreads4W_roots "${_root}")
endif ()
endforeach()
if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64)$")
set(PThreads4W_ARCH "x86_64")
elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$")
@@ -9,36 +20,77 @@ elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86|i[3-6]86)$")
else ()
set(PThreads4W_ARCH "${CMAKE_SYSTEM_PROCESSOR}")
endif ()
set(PThreads4W_INSTALL_DIR "${PTHREADS4W_ROOT}/${PThreads4W_ARCH}/${CMAKE_BUILD_TYPE}")
find_path(PThreads4W_INCLUDE_DIR
NAMES pthread.h
PATHS "${PThreads4W_INSTALL_DIR}/include"
NO_DEFAULT_PATH
)
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(PThreads4W_LIBRARY_NAME pthreadVC3d)
HINTS ${_PThreads4W_roots}
PATH_SUFFIXES
"${PThreads4W_ARCH}/Debug/include"
"${PThreads4W_ARCH}/Release/include"
"${PThreads4W_ARCH}/RelWithDebInfo/include"
"${PThreads4W_ARCH}/MinSizeRel/include"
include)
find_library(PThreads4W_LIBRARY_DEBUG
NAMES pthreadVC3d
HINTS ${_PThreads4W_roots}
PATH_SUFFIXES "${PThreads4W_ARCH}/Debug/lib" Debug/lib lib)
find_library(PThreads4W_LIBRARY_RELEASE
NAMES pthreadVC3
HINTS ${_PThreads4W_roots}
PATH_SUFFIXES
"${PThreads4W_ARCH}/Release/lib"
"${PThreads4W_ARCH}/RelWithDebInfo/lib"
"${PThreads4W_ARCH}/MinSizeRel/lib"
Release/lib RelWithDebInfo/lib MinSizeRel/lib lib)
# Keep the conventional singular variable available for callers that inspect it.
if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND PThreads4W_LIBRARY_DEBUG)
set(PThreads4W_LIBRARY "${PThreads4W_LIBRARY_DEBUG}")
elseif (PThreads4W_LIBRARY_RELEASE)
set(PThreads4W_LIBRARY "${PThreads4W_LIBRARY_RELEASE}")
else ()
set(PThreads4W_LIBRARY_NAME pthreadVC3)
set(PThreads4W_LIBRARY "${PThreads4W_LIBRARY_DEBUG}")
endif ()
find_library(PThreads4W_LIBRARY
NAMES "${PThreads4W_LIBRARY_NAME}"
PATHS "${PThreads4W_INSTALL_DIR}/lib"
NO_DEFAULT_PATH
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(PThreads4W
REQUIRED_VARS
PThreads4W_INCLUDE_DIR
PThreads4W_LIBRARY
)
REQUIRED_VARS PThreads4W_INCLUDE_DIR PThreads4W_LIBRARY)
if (PThreads4W_FOUND AND NOT TARGET PThreads4W::PThreads4W)
add_library(PThreads4W::PThreads4W UNKNOWN IMPORTED)
set_target_properties(PThreads4W::PThreads4W PROPERTIES
IMPORTED_LOCATION "${PThreads4W_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${PThreads4W_INCLUDE_DIR}"
)
INTERFACE_INCLUDE_DIRECTORIES "${PThreads4W_INCLUDE_DIR}")
if (PThreads4W_LIBRARY_DEBUG)
set_property(TARGET PThreads4W::PThreads4W APPEND PROPERTY
IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(PThreads4W::PThreads4W PROPERTIES
IMPORTED_LOCATION_DEBUG "${PThreads4W_LIBRARY_DEBUG}")
endif ()
if (PThreads4W_LIBRARY_RELEASE)
set_property(TARGET PThreads4W::PThreads4W APPEND PROPERTY
IMPORTED_CONFIGURATIONS RELEASE RELWITHDEBINFO MINSIZEREL)
set_target_properties(PThreads4W::PThreads4W PROPERTIES
IMPORTED_LOCATION_RELEASE "${PThreads4W_LIBRARY_RELEASE}"
IMPORTED_LOCATION_RELWITHDEBINFO "${PThreads4W_LIBRARY_RELEASE}"
IMPORTED_LOCATION_MINSIZEREL "${PThreads4W_LIBRARY_RELEASE}")
endif ()
# A single available configuration remains usable for the other build types.
if (NOT PThreads4W_LIBRARY_RELEASE AND PThreads4W_LIBRARY_DEBUG)
set_target_properties(PThreads4W::PThreads4W PROPERTIES
MAP_IMPORTED_CONFIG_RELEASE DEBUG
MAP_IMPORTED_CONFIG_RELWITHDEBINFO DEBUG
MAP_IMPORTED_CONFIG_MINSIZEREL DEBUG)
elseif (NOT PThreads4W_LIBRARY_DEBUG AND PThreads4W_LIBRARY_RELEASE)
set_target_properties(PThreads4W::PThreads4W PROPERTIES
MAP_IMPORTED_CONFIG_DEBUG RELEASE)
endif ()
endif ()
mark_as_advanced(
PThreads4W_INCLUDE_DIR
PThreads4W_LIBRARY
)
PThreads4W_LIBRARY_DEBUG
PThreads4W_LIBRARY_RELEASE)
unset(_PThreads4W_roots)
unset(_root)
+43 -7
View File
@@ -124,8 +124,36 @@ struct Point_Backend_State final {
render_domain->invoke([this] { backend.reset(); });
}
detail::Datoviz_Visual_Backend& require_backend() {
if (backend)
return *backend;
if (backend_failure)
std::rethrow_exception(backend_failure);
throw std::runtime_error("Datoviz backend is unavailable");
}
void quarantine_backend(std::exception_ptr failure) noexcept {
if (!failure) {
try {
throw std::runtime_error("Datoviz backend was quarantined");
} catch (...) {
failure = std::current_exception();
}
}
backend_failure = std::move(failure);
// An abandoned fence means the driver never proved that the submitted
// work stopped touching the backend resources. Destroying or reusing
// them would be unsafe. Deliberately relinquish ownership of the whole
// backend and let the OS/driver reclaim it at process teardown. This is
// a fault-containment path only; all subsequent scene operations fail.
static_cast<void>(backend.release());
std::lock_guard lock(frame_mutex);
latest.reset();
}
std::shared_ptr<detail::Render_Domain> render_domain;
std::unique_ptr<detail::Datoviz_Visual_Backend> backend;
std::exception_ptr backend_failure;
mutable std::mutex frame_mutex;
std::shared_ptr<const Pixel_Frame> latest;
};
@@ -205,7 +233,7 @@ struct Basic_Point_Scene final
&detail::Scene_State::viewport>();
const auto state = backend_state;
state->render_domain->invoke([state, type, x, y, button, modifiers, viewport] {
state->backend->dispatch_pointer(type, x, y, button, modifiers, viewport);
state->require_backend().dispatch_pointer(type, x, y, button, modifiers, viewport);
});
this->notify_model_dirty();
}
@@ -218,7 +246,7 @@ struct Basic_Point_Scene final
const auto state = backend_state;
state->render_domain->invoke(
[state, x, y, delta_x, delta_y, modifiers, viewport] {
state->backend->dispatch_wheel(x, y, delta_x, delta_y, modifiers,
state->require_backend().dispatch_wheel(x, y, delta_x, delta_y, modifiers,
viewport);
});
this->notify_model_dirty();
@@ -227,7 +255,7 @@ struct Basic_Point_Scene final
void dispatch_key(const ::renderive::Key_Event& event) override {
const auto state = backend_state;
state->render_domain->invoke([state, event] {
state->backend->dispatch_key(event);
state->require_backend().dispatch_key(event);
});
this->notify_model_dirty();
}
@@ -328,7 +356,9 @@ struct Basic_Point_Scene final
{completion.capacity, completion.in_flight, completion.peak_in_flight,
completion.watched, completion.peak_watched, completion.backpressure_count,
completion.backpressure_wait_ns},
render.unhandled_exception_count
render.unhandled_exception_count,
completion.fault_count,
completion.abandoned_count
};
}
@@ -377,9 +407,15 @@ struct Basic_Point_Scene final
async_frame->pending.reset();
pending.trace.gpu_fence_wait_ns =
async_frame->completion.wait_duration_ns;
if (async_frame->completion.abandoned) {
auto failure = std::move(async_frame->completion.error);
state->quarantine_backend(failure);
static_cast<void>(source->fail(std::move(failure)));
return;
}
if (async_frame->completion.error) {
try {
state->backend->discard(std::move(pending));
state->require_backend().discard(std::move(pending));
static_cast<void>(source->fail(
std::move(async_frame->completion.error)));
} catch (...) {
@@ -389,7 +425,7 @@ struct Basic_Point_Scene final
return;
}
try {
auto completed = state->backend->collect(
auto completed = state->require_backend().collect(
std::move(pending));
auto frame = std::move(completed.frame);
auto trace = std::make_shared<detail::Datoviz_Frame_Trace>(
@@ -447,7 +483,7 @@ struct Basic_Point_Scene final
? static_cast<std::uint64_t>(queue_wait)
: 0;
}
auto pending = state->backend->submit(
auto pending = state->require_backend().submit(
scene_state, scene_revision, *prepared,
frame_sequence, observe);
if (!pending) {
+2
View File
@@ -82,6 +82,8 @@ struct Runtime_Statistics {
Runtime_Admission_Statistics render_domain;
Runtime_Admission_Statistics gpu_completion;
std::uint64_t render_domain_unhandled_exception_count{};
std::uint64_t gpu_completion_fault_count{};
std::uint64_t gpu_completion_abandoned_count{};
};
struct Frame_Status {
@@ -19,12 +19,14 @@ Gpu_Completion_Service::~Gpu_Completion_Service() {
if (thread_.joinable())
thread_.join();
}
Gpu_Completion_Service::Reservation::Reservation(std::shared_ptr<Pending_Fence> pending) noexcept : pending_(std::move(pending)) {}
Gpu_Completion_Service::Reservation::~Reservation() {
cancel();
}
Gpu_Completion_Service::Reservation::Reservation(Reservation&& other) noexcept : pending_(std::exchange(other.pending_, {})) {}
void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence) noexcept {
Gpu_Completion_Service::Reservation::Reservation(
std::shared_ptr<Pending_Fence> pending) noexcept
: pending_(std::move(pending)) {}
Gpu_Completion_Service::Reservation::~Reservation() { cancel(); }
Gpu_Completion_Service::Reservation::Reservation(Reservation&& other) noexcept
: pending_(std::exchange(other.pending_, {})) {}
void Gpu_Completion_Service::Reservation::watch(VkDevice device,
VkFence fence) noexcept {
if (!pending_ || device == VK_NULL_HANDLE || fence == VK_NULL_HANDLE)
std::terminate();
auto pending = std::exchange(pending_, {});
@@ -35,9 +37,11 @@ void Gpu_Completion_Service::Reservation::watch(VkDevice device, VkFence fence)
std::terminate();
pending->device = device;
pending->fence = fence;
if (pending->observe)
pending->watched_at = std::chrono::steady_clock::now();
const std::size_t watched = service->watched_.fetch_add(1, std::memory_order_relaxed) + 1;
// This timestamp is part of correctness, not only observability: it
// bounds the lifetime of a submission whose fence never signals.
pending->watched_at = std::chrono::steady_clock::now();
const std::size_t watched =
service->watched_.fetch_add(1, std::memory_order_relaxed) + 1;
update_peak(service->peak_watched_, watched);
pending->status = Pending_Fence::Status::watched;
}
@@ -50,11 +54,15 @@ void Gpu_Completion_Service::Reservation::cancel() noexcept {
cancel_reserved(pending);
pending->service->wake();
}
void Gpu_Completion_Service::update_peak(std::atomic_size_t& peak, std::size_t value) noexcept {
void Gpu_Completion_Service::update_peak(std::atomic_size_t& peak,
std::size_t value) noexcept {
std::size_t current = peak.load(std::memory_order_relaxed);
while (current < value && !peak.compare_exchange_weak(current, value, std::memory_order_relaxed)) {}
while (current < value &&
!peak.compare_exchange_weak(current, value,
std::memory_order_relaxed)) {}
}
void Gpu_Completion_Service::cancel_reserved(const std::shared_ptr<Pending_Fence>& pending) noexcept {
void Gpu_Completion_Service::cancel_reserved(
const std::shared_ptr<Pending_Fence>& pending) noexcept {
if (!pending)
return;
std::lock_guard lock(pending->mutex);
@@ -67,18 +75,23 @@ void Gpu_Completion_Service::acquire_slot() {
backpressure_count_.fetch_add(1, std::memory_order_relaxed);
slots_.acquire();
const auto waited = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - started).count();
std::chrono::steady_clock::now() - started)
.count();
if (waited > 0)
backpressure_wait_ns_.fetch_add(static_cast<std::uint64_t>(waited), std::memory_order_relaxed);
backpressure_wait_ns_.fetch_add(
static_cast<std::uint64_t>(waited),
std::memory_order_relaxed);
}
const std::size_t in_flight = in_flight_.fetch_add(1, std::memory_order_relaxed) + 1;
const std::size_t in_flight =
in_flight_.fetch_add(1, std::memory_order_relaxed) + 1;
update_peak(peak_in_flight_, in_flight);
}
void Gpu_Completion_Service::release_slot() noexcept {
in_flight_.fetch_sub(1, std::memory_order_relaxed);
slots_.release();
}
Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(Completion completion, bool observe) {
Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(
Completion completion, bool observe) {
if (!completion)
throw std::invalid_argument("GPU completion callback is empty");
if (stopping_.load(std::memory_order_acquire))
@@ -95,7 +108,8 @@ Gpu_Completion_Service::Reservation Gpu_Completion_Service::prepare(Completion c
wake();
return Reservation(std::move(pending));
}
Gpu_Completion_Service::Statistics Gpu_Completion_Service::statistics() const noexcept {
Gpu_Completion_Service::Statistics
Gpu_Completion_Service::statistics() const noexcept {
return {
static_cast<std::size_t>(default_capacity),
in_flight_.load(std::memory_order_relaxed),
@@ -103,8 +117,9 @@ Gpu_Completion_Service::Statistics Gpu_Completion_Service::statistics() const no
watched_.load(std::memory_order_relaxed),
peak_watched_.load(std::memory_order_relaxed),
backpressure_count_.load(std::memory_order_relaxed),
backpressure_wait_ns_.load(std::memory_order_relaxed)
};
backpressure_wait_ns_.load(std::memory_order_relaxed),
fault_count_.load(std::memory_order_relaxed),
abandoned_count_.load(std::memory_order_relaxed)};
}
void Gpu_Completion_Service::wake() noexcept {
wake_generation_.fetch_add(1, std::memory_order_release);
@@ -118,7 +133,17 @@ void Gpu_Completion_Service::run() noexcept {
std::vector<std::shared_ptr<Pending_Fence>> active;
active.reserve(static_cast<std::size_t>(default_capacity));
std::size_t wait_group_index{};
const auto finish = [this](const std::shared_ptr<Pending_Fence>& pending, VkResult result) {
const auto wait_age_ns = [](std::chrono::steady_clock::time_point started) {
const auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - started)
.count();
return elapsed > 0 ? static_cast<std::uint64_t>(elapsed) : 0ULL;
};
const auto finish = [this, &wait_age_ns](
const std::shared_ptr<Pending_Fence>& pending,
VkResult result, bool abandoned,
const char* abandonment_reason = nullptr) {
Completion completion;
std::chrono::steady_clock::time_point watched_at{};
bool observe{};
@@ -131,14 +156,25 @@ void Gpu_Completion_Service::run() noexcept {
}
watched_.fetch_sub(1, std::memory_order_relaxed);
Result completion_result;
if (observe) {
const auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - watched_at).count();
completion_result.wait_duration_ns = duration > 0 ? static_cast<std::uint64_t>(duration) : 0;
}
if (result != VK_SUCCESS) {
const std::uint64_t wait_ns = wait_age_ns(watched_at);
if (observe)
completion_result.wait_duration_ns = wait_ns;
completion_result.abandoned = abandoned;
if (abandoned || result != VK_SUCCESS) {
fault_count_.fetch_add(1, std::memory_order_relaxed);
if (abandoned)
abandoned_count_.fetch_add(1, std::memory_order_relaxed);
try {
throw std::runtime_error("GPU fence wait failed with Vulkan result " + std::to_string(static_cast<int>(result)));
if (abandoned) {
std::string message = abandonment_reason != nullptr
? abandonment_reason
: "GPU fence was abandoned";
message += " after " + std::to_string(wait_ns) + " ns";
throw std::runtime_error(std::move(message));
}
throw std::runtime_error(
"GPU fence wait failed with Vulkan result " +
std::to_string(static_cast<int>(result)));
} catch (...) {
completion_result.error = std::current_exception();
}
@@ -149,11 +185,14 @@ void Gpu_Completion_Service::run() noexcept {
}
release_slot();
};
for (;;) {
const std::uint64_t wake_generation = wake_generation_.load(std::memory_order_acquire);
const std::uint64_t wake_generation =
wake_generation_.load(std::memory_order_acquire);
std::shared_ptr<Pending_Fence> incoming;
while (pending_.try_pop(incoming))
active.push_back(std::move(incoming));
std::vector<Device_Fences> groups;
for (auto iterator = active.begin(); iterator != active.end();) {
Pending_Fence::Status status;
@@ -166,16 +205,19 @@ void Gpu_Completion_Service::run() noexcept {
fence = (*iterator)->fence;
}
if (status == Pending_Fence::Status::canceled ||
(status == Pending_Fence::Status::reserved && stopping_.load(std::memory_order_acquire))) {
(status == Pending_Fence::Status::reserved &&
stopping_.load(std::memory_order_acquire))) {
cancel_reserved(*iterator);
iterator = active.erase(iterator);
release_slot();
continue;
}
if (status == Pending_Fence::Status::watched) {
auto group = std::find_if(groups.begin(), groups.end(), [device](const Device_Fences& item) {
return item.device == device;
});
auto group = std::find_if(
groups.begin(), groups.end(),
[device](const Device_Fences& item) {
return item.device == device;
});
if (group == groups.end()) {
groups.push_back(Device_Fences{device, {}});
group = groups.end() - 1;
@@ -184,27 +226,47 @@ void Gpu_Completion_Service::run() noexcept {
}
++iterator;
}
if (stopping_.load(std::memory_order_acquire) && active.empty() && pending_.empty())
if (stopping_.load(std::memory_order_acquire) && active.empty() &&
pending_.empty())
return;
// Always probe every watched fence before entering a timed wait. The
// timed wait still prevents busy-spinning, while this full probe keeps
// completion latency bounded by one wait quantum instead of one quantum
// per active device group.
// Probe every watched fence first. A permanently unsignaled fence is
// converted into a logical failure after a bounded interval. The
// submission is explicitly marked abandoned so its owner can
// quarantine, rather than recycle, the referenced GPU resources.
bool completed_any = false;
for (auto iterator = active.begin(); iterator != active.end();) {
VkDevice device{VK_NULL_HANDLE};
VkFence fence{VK_NULL_HANDLE};
Pending_Fence::Status status;
std::chrono::steady_clock::time_point watched_at{};
{
std::lock_guard lock((*iterator)->mutex);
status = (*iterator)->status;
device = (*iterator)->device;
fence = (*iterator)->fence;
watched_at = (*iterator)->watched_at;
}
if (status != Pending_Fence::Status::watched) {
++iterator;
continue;
}
if (stopping_.load(std::memory_order_acquire)) {
auto pending = *iterator;
iterator = active.erase(iterator);
finish(pending, VK_TIMEOUT, true,
"GPU completion service stopped with an in-flight fence; submission quarantined");
completed_any = true;
continue;
}
if (wait_age_ns(watched_at) >= maximum_fence_age_ns) {
auto pending = *iterator;
iterator = active.erase(iterator);
finish(pending, VK_TIMEOUT, true,
"GPU fence exceeded the maximum completion age; submission quarantined");
completed_any = true;
continue;
}
const VkResult result = vkGetFenceStatus(device, fence);
if (result == VK_NOT_READY) {
++iterator;
@@ -212,25 +274,31 @@ void Gpu_Completion_Service::run() noexcept {
}
auto pending = *iterator;
iterator = active.erase(iterator);
finish(pending, result);
finish(pending, result, result != VK_SUCCESS,
result == VK_SUCCESS
? nullptr
: "GPU fence query returned an error; submission quarantined");
completed_any = true;
}
if (completed_any)
continue;
if (groups.empty()) {
std::unique_lock lock(wait_mutex_);
if (wake_generation_.load(std::memory_order_acquire) == wake_generation) {
if (wake_generation_.load(std::memory_order_acquire) ==
wake_generation) {
wake_condition_.wait(lock, [this, wake_generation] {
return wake_generation_.load(std::memory_order_acquire) != wake_generation;
return wake_generation_.load(std::memory_order_acquire) !=
wake_generation;
});
}
continue;
}
wait_group_index %= groups.size();
const Device_Fences& group = groups[wait_group_index++];
const VkResult wait_result = vkWaitForFences(
group.device, static_cast<std::uint32_t>(group.fences.size()), group.fences.data(), VK_FALSE,
fence_wait_timeout_ns);
group.device, static_cast<std::uint32_t>(group.fences.size()),
group.fences.data(), VK_FALSE, fence_wait_timeout_ns);
for (auto iterator = active.begin(); iterator != active.end();) {
VkDevice device{VK_NULL_HANDLE};
VkFence fence{VK_NULL_HANDLE};
@@ -241,7 +309,8 @@ void Gpu_Completion_Service::run() noexcept {
device = (*iterator)->device;
fence = (*iterator)->fence;
}
if (status != Pending_Fence::Status::watched || device != group.device) {
if (status != Pending_Fence::Status::watched ||
device != group.device) {
++iterator;
continue;
}
@@ -254,7 +323,10 @@ void Gpu_Completion_Service::run() noexcept {
}
auto pending = *iterator;
iterator = active.erase(iterator);
finish(pending, result);
finish(pending, result, result != VK_SUCCESS,
result == VK_SUCCESS
? nullptr
: "GPU fence wait returned an error; submission quarantined");
}
}
}
@@ -19,6 +19,11 @@ public:
struct Result {
std::exception_ptr error;
std::uint64_t wait_duration_ns{};
// True when the service cannot prove that the submission completed:
// a fence exceeded the bounded wait policy, Vulkan returned an error,
// or the service is shutting down. Callers must not recycle or destroy
// resources referenced by that submission.
bool abandoned{};
};
struct Statistics {
std::size_t capacity{};
@@ -28,6 +33,8 @@ public:
std::size_t peak_watched{};
std::uint64_t backpressure_count{};
std::uint64_t backpressure_wait_ns{};
std::uint64_t fault_count{};
std::uint64_t abandoned_count{};
};
using Completion = std::function<void(Result)>;
class Reservation final {
@@ -76,6 +83,7 @@ private:
void run() noexcept;
static constexpr std::ptrdiff_t default_capacity = 1024;
static constexpr std::uint64_t fence_wait_timeout_ns = 1'000'000;
static constexpr std::uint64_t maximum_fence_age_ns = 30'000'000'000ULL;
std::counting_semaphore<default_capacity> slots_{default_capacity};
oneapi::tbb::concurrent_bounded_queue<std::shared_ptr<Pending_Fence>> pending_;
std::mutex wait_mutex_;
@@ -87,6 +95,8 @@ private:
std::atomic_size_t peak_watched_{};
std::atomic_uint64_t backpressure_count_{};
std::atomic_uint64_t backpressure_wait_ns_{};
std::atomic_uint64_t fault_count_{};
std::atomic_uint64_t abandoned_count_{};
std::atomic_bool stopping_{};
std::thread thread_;
};
+1 -9
View File
@@ -157,12 +157,6 @@ inline Json execution_json(const Node_Execution& execution,
}
return {
{"node_id", execution.node_id},
{"ready_time_ns", execution.ready_time_ns},
{"start_time_ns", execution.start_time_ns},
{"cpu_end_time_ns", execution.cpu_end_time_ns},
{"external_start_time_ns", execution.external_start_time_ns},
{"external_end_time_ns", execution.external_end_time_ns},
{"end_time_ns", execution.end_time_ns},
{"start_offset_ns", execution.start_time_ns >= frame.render_start_ns
? execution.start_time_ns - frame.render_start_ns : 0},
{"cpu_end_offset_ns", execution.cpu_end_time_ns >= frame.render_start_ns
@@ -198,7 +192,7 @@ inline Json node_analysis_json(const Node_Frame_Analysis& node) {
{"gpu_execution_duration_ns", node.gpu_execution_duration_ns},
{"start_offset_ns", node.start_offset_ns},
{"end_offset_ns", node.end_offset_ns},
{"dependency_ready_time_ns", node.dependency_ready_time_ns},
{"dependency_ready_offset_ns", node.dependency_ready_offset_ns},
{"scheduler_wait_ns", node.scheduler_wait_ns},
{"work_contribution", node.work_contribution},
{"critical_path_contribution", node.critical_path_contribution},
@@ -222,8 +216,6 @@ inline Json frame_json(const Captured_Frame& frame) {
return {
{"frame_id", frame.snapshot->frame_id},
{"render_plan_version", frame.snapshot->render_plan_version},
{"render_start_ns", frame.snapshot->render_start_ns},
{"render_end_ns", frame.snapshot->render_end_ns},
{"render_duration_ns", frame.snapshot->render_duration_ns()},
{"node_executions", std::move(executions)},
{"analysis", {
+3 -1
View File
@@ -490,7 +490,9 @@ public:
{"queued", runtime.gpu_completion.queued},
{"peak_queued", runtime.gpu_completion.peak_queued},
{"backpressure_count", runtime.gpu_completion.backpressure_count},
{"backpressure_wait_ns", runtime.gpu_completion.backpressure_wait_ns}}}}},
{"backpressure_wait_ns", runtime.gpu_completion.backpressure_wait_ns},
{"fault_count", runtime.gpu_completion_fault_count},
{"abandoned_count", runtime.gpu_completion_abandoned_count}}}}},
{"session_id", session_id_},
{"performance_capture",
gallery_performance_capture_json(scene_.render_scene())},
+6 -4
View File
@@ -544,12 +544,14 @@ TEST(RenderiveWebGallery, PerformanceCaptureReturnsPlanFrameAnalysisAndStatistic
for (const auto& frame : captured.at("frames")) {
EXPECT_TRUE(frame.contains("frame_id"));
EXPECT_TRUE(frame.contains("render_plan_version"));
EXPECT_TRUE(frame.contains("render_start_ns"));
EXPECT_TRUE(frame.contains("render_end_ns"));
EXPECT_FALSE(frame.contains("render_start_ns"));
EXPECT_FALSE(frame.contains("render_end_ns"));
ASSERT_FALSE(frame.at("node_executions").empty());
const auto& execution = frame.at("node_executions").at(0);
EXPECT_TRUE(execution.contains("start_time_ns"));
EXPECT_TRUE(execution.contains("end_time_ns"));
EXPECT_FALSE(execution.contains("start_time_ns"));
EXPECT_FALSE(execution.contains("end_time_ns"));
EXPECT_TRUE(execution.contains("start_offset_ns"));
EXPECT_TRUE(execution.contains("end_offset_ns"));
EXPECT_TRUE(execution.contains("worker_id"));
EXPECT_TRUE(execution.contains("status"));
EXPECT_TRUE(execution.contains("attachments"));
+2 -2
View File
@@ -36,10 +36,10 @@ export interface Gallery_Renderable_Cache {owner_id: number; name: string; prepa
export interface Gallery_Render_Plan {version: number; nodes: Gallery_Render_Node[]; edges: Gallery_Render_Edge[]; renderables: Gallery_Renderable_Cache[];}
export interface Gallery_Node_Diagnostic_Attachment {type: string; content: string;}
export interface Gallery_Node_Execution {node_id: number; worker_id: number; start_offset_ns: number; cpu_end_offset_ns: number; external_start_offset_ns: number; external_end_offset_ns: number; end_offset_ns: number; duration_ns: number; cpu_duration_ns: number; external_duration_ns: number; status: "pending"|"ready"|"running"|"waiting_external"|"complete"|"failed"|"cancelled"; metrics?: Record<string, Json_Primitive>; attachments: Gallery_Node_Diagnostic_Attachment[];}
export interface Gallery_Node_Analysis {node_id: number; duration_ns: number; cpu_duration_ns: number; render_domain_cpu_duration_ns: number; total_cpu_work_duration_ns: number; external_duration_ns: number; render_domain_queue_wait_ns: number; gpu_completion_wait_ns: number; gpu_execution_duration_ns: number; start_offset_ns: number; end_offset_ns: number; dependency_ready_time_ns: number; scheduler_wait_ns: number; work_contribution: number; critical_path_contribution: number; on_critical_path: boolean;}
export interface Gallery_Node_Analysis {node_id: number; duration_ns: number; cpu_duration_ns: number; render_domain_cpu_duration_ns: number; total_cpu_work_duration_ns: number; external_duration_ns: number; render_domain_queue_wait_ns: number; gpu_completion_wait_ns: number; gpu_execution_duration_ns: number; start_offset_ns: number; end_offset_ns: number; dependency_ready_offset_ns: number; scheduler_wait_ns: number; work_contribution: number; critical_path_contribution: number; on_critical_path: boolean;}
export interface Gallery_Worker_Analysis {worker_id: number; work_duration_ns: number; utilization: number;}
export interface Gallery_Frame_Analysis {total_render_duration_ns: number; critical_path_duration_ns: number; total_work_duration_ns: number; scheduler_work_duration_ns: number; render_domain_work_duration_ns: number; total_external_duration_ns: number; total_render_domain_queue_wait_ns: number; total_gpu_completion_wait_ns: number; total_gpu_execution_duration_ns: number; scheduler_parallel_overlap_ns: number; scheduler_peak_parallelism: number; average_cpu_concurrency: number; scheduler_average_parallelism: number; render_domain_utilization: number; critical_path: number[]; bottleneck_nodes: number[]; nodes: Gallery_Node_Analysis[]; workers: Gallery_Worker_Analysis[];}
export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_start_ns: number; render_end_ns: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: Gallery_Frame_Analysis;}
export interface Gallery_Captured_Frame {frame_id: number; render_plan_version: number; render_duration_ns: number; node_executions: Gallery_Node_Execution[]; analysis?: Gallery_Frame_Analysis;}
export interface Gallery_Node_Statistics {node_id: number; execution_count: number; average_ns: number; moving_average_ns: number; p50_ns: number; p95_ns: number; p99_ns: number; minimum_ns: number; maximum_ns: number; average_cpu_ns: number; average_render_domain_cpu_ns: number; average_total_cpu_work_ns: number; average_external_ns: number; average_render_domain_queue_wait_ns: number; average_gpu_completion_wait_ns: number; average_gpu_execution_ns: number; p95_cpu_ns: number; p95_render_domain_cpu_ns: number; p95_external_ns: number; p95_gpu_execution_ns: number; critical_path_frequency: number; average_scheduler_wait_ns: number;}
export interface Gallery_Plan_Statistics {render_plan_version: number; frame_count: number; render_average_ns: number; render_p50_ns: number; render_p95_ns: number; render_maximum_ns: number; average_cpu_concurrency: number; scheduler_average_parallelism: number; render_domain_utilization: number; scheduler_peak_parallelism: number; scheduler_wait_average_ns: number; scheduler_wait_p95_ns: number; nodes: Gallery_Node_Statistics[];}
export interface Gallery_Capture_Session {session_id: number; active: boolean; requested_count: number; captured_count: number; frames: Gallery_Captured_Frame[]; node_statistics: Gallery_Node_Statistics[]; plan_statistics: Gallery_Plan_Statistics[]; summary?: Record<string, Json_Value>;}
+1 -3
View File
@@ -14,8 +14,6 @@ describe("DAG view model", () => {
const frame: Gallery_Captured_Frame = {
frame_id: 1,
render_plan_version: 2,
render_start_ns: 100,
render_end_ns: 110,
render_duration_ns: 10,
node_executions: [{
node_id: 7,
@@ -60,7 +58,7 @@ describe("DAG view model", () => {
gpu_execution_duration_ns: 0,
start_offset_ns: 0,
end_offset_ns: 10,
dependency_ready_time_ns: 0,
dependency_ready_offset_ns: 0,
scheduler_wait_ns: 0,
work_contribution: 1,
critical_path_contribution: 1,