大更新
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
namespace aethera::observation {
|
||||
struct Identity {
|
||||
std::uint64_t value{};
|
||||
friend constexpr bool operator==(Identity, Identity) noexcept = default;
|
||||
};
|
||||
struct Context {
|
||||
Identity root{};
|
||||
Identity scope{};
|
||||
Identity parent{};
|
||||
Identity correlation{};
|
||||
};
|
||||
[[nodiscard]] inline Identity make_identity() noexcept {
|
||||
static std::atomic<std::uint64_t> next{1};
|
||||
return Identity{next.fetch_add(1, std::memory_order_relaxed)};
|
||||
}
|
||||
[[nodiscard]] inline Context make_root_context() noexcept {
|
||||
const auto identity = make_identity();
|
||||
return Context{identity, identity, {}, {}};
|
||||
}
|
||||
[[nodiscard]] inline Context make_child_context(const Context& parent) noexcept {
|
||||
return Context{parent.root, make_identity(), parent.scope, {}};
|
||||
}
|
||||
[[nodiscard]] inline Context make_correlation_context(const Context& parent) noexcept {
|
||||
const auto identity = make_identity();
|
||||
return Context{parent.root, identity, parent.scope, identity};
|
||||
}
|
||||
[[nodiscard]] inline Context make_correlated_context(const Context& source) noexcept {
|
||||
const auto correlation = source.correlation.value != 0 ? source.correlation : source.scope;
|
||||
return Context{source.root, make_identity(), {}, correlation};
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Context& value) {
|
||||
if (value.root.value != 0) sink.field("root", value.root.value);
|
||||
if (value.scope.value != 0) sink.field("scope", value.scope.value);
|
||||
if (value.parent.value != 0) sink.field("parent", value.parent.value);
|
||||
if (value.correlation.value != 0) sink.field("correlation", value.correlation.value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "Context.hpp"
|
||||
#include "Tracy.hpp"
|
||||
#include <cstdint>
|
||||
namespace aethera::observation {
|
||||
struct Frame_Context {
|
||||
Context context{};
|
||||
std::uint64_t frame{};
|
||||
std::uint64_t generation{};
|
||||
};
|
||||
[[nodiscard]] inline Frame_Context make_frame_context(std::uint64_t frame, std::uint64_t generation) noexcept {
|
||||
return Frame_Context{make_root_context(), frame, generation};
|
||||
}
|
||||
inline void trace_frame(const Frame_Context& value) noexcept {
|
||||
AETHERA_TRACE_FRAME("Aethera.Frame");
|
||||
AETHERA_TRACE_MESSAGE_TAGS(value);
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Frame_Context& value) {
|
||||
sink.field("event", "frame");
|
||||
trace_serialize(sink, value.context);
|
||||
sink.field("frame", value.frame);
|
||||
sink.field("generation", value.generation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
#include "Frame_Timing.hpp"
|
||||
#include "GPU_Completion.hpp"
|
||||
#include "Frame_Policy.hpp"
|
||||
#include "Tracy.hpp"
|
||||
#include "Vulkan.hpp"
|
||||
#include <cstdint>
|
||||
namespace aethera::observation {
|
||||
struct Frame_Attention {
|
||||
Frame_Timing timing{};
|
||||
std::uint64_t gpu_render_ns{};
|
||||
std::uint64_t gpu_total_ns{};
|
||||
std::uint64_t uploaded_bytes{};
|
||||
std::uint64_t readback_bytes{};
|
||||
std::uint64_t upload_ns{};
|
||||
std::uint64_t upload_queue_wait_ns{};
|
||||
std::uint64_t upload_fence_wait_ns{};
|
||||
std::uint64_t shader_compile_ns{};
|
||||
std::uint64_t pipeline_create_ns{};
|
||||
std::uint32_t pipeline_create_count{};
|
||||
Vulkan_State vulkan{};
|
||||
GPU_Completion_State gpu_completion{};
|
||||
};
|
||||
struct Frame_Attention_Index {
|
||||
Frame_Context frame{};
|
||||
bool resource_activity{};
|
||||
bool upload_wait{};
|
||||
bool pipeline_build{};
|
||||
bool frame_overlap{};
|
||||
bool non_frame_submission{};
|
||||
bool completion_queue_gap{};
|
||||
bool completion_saturated{};
|
||||
};
|
||||
[[nodiscard]] inline Frame_Attention_Index make_frame_attention_index(const Frame_Attention& value) noexcept {
|
||||
return Frame_Attention_Index{
|
||||
value.timing.frame,
|
||||
value.uploaded_bytes != 0 || value.readback_bytes != 0 || value.upload_ns != 0 || value.shader_compile_ns != 0 || value.pipeline_create_ns != 0 || value.pipeline_create_count != 0,
|
||||
value.upload_queue_wait_ns != 0 || value.upload_fence_wait_ns != 0,
|
||||
value.shader_compile_ns != 0 || value.pipeline_create_ns != 0 || value.pipeline_create_count != 0,
|
||||
value.vulkan.frames_in_flight > 1,
|
||||
value.vulkan.pending_submissions > value.vulkan.frames_in_flight,
|
||||
value.gpu_completion.in_flight > value.gpu_completion.active,
|
||||
value.gpu_completion.capacity != 0 && value.gpu_completion.in_flight >= value.gpu_completion.capacity};
|
||||
}
|
||||
[[nodiscard]] inline bool has_frame_attention_index(const Frame_Attention_Index& value) noexcept {
|
||||
return value.resource_activity || value.upload_wait || value.pipeline_build || value.frame_overlap || value.non_frame_submission || value.completion_queue_gap || value.completion_saturated;
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Frame_Attention& value) {
|
||||
sink.field("event", "frame_attention");
|
||||
trace_serialize(sink, value.timing.frame.context);
|
||||
sink.field("frame", value.timing.frame.frame);
|
||||
sink.field("generation", value.timing.frame.generation);
|
||||
sink.field("render_latency_ns", value.timing.render_latency_ns);
|
||||
sink.field("graph_start_latency_ns", value.timing.graph_start_latency_ns);
|
||||
sink.field("graph_to_submit_ns", value.timing.graph_to_submit_ns);
|
||||
sink.field("backend_prepare_ns", value.timing.backend_prepare_ns);
|
||||
sink.field("submit_path_ns", value.timing.submit_path_ns);
|
||||
sink.field("completion_tail_ns", value.timing.completion_tail_ns);
|
||||
sink.field("gpu_render_ns", value.gpu_render_ns);
|
||||
sink.field("gpu_total_ns", value.gpu_total_ns);
|
||||
sink.field("uploaded_bytes", value.uploaded_bytes);
|
||||
sink.field("readback_bytes", value.readback_bytes);
|
||||
sink.field("upload_ns", value.upload_ns);
|
||||
sink.field("upload_queue_wait_ns", value.upload_queue_wait_ns);
|
||||
sink.field("upload_fence_wait_ns", value.upload_fence_wait_ns);
|
||||
sink.field("shader_compile_ns", value.shader_compile_ns);
|
||||
sink.field("pipeline_create_ns", value.pipeline_create_ns);
|
||||
sink.field("pipeline_create_count", value.pipeline_create_count);
|
||||
sink.field("vulkan_pending", value.vulkan.pending_submissions);
|
||||
sink.field("frames_in_flight", value.vulkan.frames_in_flight);
|
||||
sink.field("completion_in_flight", value.gpu_completion.in_flight);
|
||||
sink.field("completion_active", value.gpu_completion.active);
|
||||
sink.field("completion_capacity", value.gpu_completion.capacity);
|
||||
sink.field("completion_capacity_exhausted", value.gpu_completion.capacity_exhausted_count);
|
||||
sink.field("completion_errors", value.gpu_completion.error_count);
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Frame_Attention_Index& value) {
|
||||
sink.field("event", "frame_attention_index");
|
||||
trace_serialize(sink, value.frame.context);
|
||||
sink.field("frame", value.frame.frame);
|
||||
sink.field("generation", value.frame.generation);
|
||||
if (value.resource_activity) sink.field("resource_activity", true);
|
||||
if (value.upload_wait) sink.field("upload_wait", true);
|
||||
if (value.pipeline_build) sink.field("pipeline_build", true);
|
||||
if (value.frame_overlap) sink.field("frame_overlap", true);
|
||||
if (value.non_frame_submission) sink.field("non_frame_submission", true);
|
||||
if (value.completion_queue_gap) sink.field("completion_queue_gap", true);
|
||||
if (value.completion_saturated) sink.field("completion_saturated", true);
|
||||
}
|
||||
inline void trace_frame_attention(const Frame_Attention& value) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
if (!tracy_connected()) return;
|
||||
AETHERA_TRACE_MESSAGE_TAGS(value);
|
||||
trace_frame_policy_context(value.timing.frame);
|
||||
const auto index = make_frame_attention_index(value);
|
||||
if (has_frame_attention_index(index)) AETHERA_TRACE_MESSAGE_TAGS(index);
|
||||
#else
|
||||
static_cast<void>(value);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
#include "Frame.hpp"
|
||||
#include "Tracy.hpp"
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
namespace aethera::observation {
|
||||
struct Frame_Policy_Statistics {
|
||||
std::uint64_t sample_count{};
|
||||
double average_ns{};
|
||||
double p95_ns{};
|
||||
double standard_deviation_ns{};
|
||||
};
|
||||
struct Frame_Policy_State {
|
||||
std::uint64_t timer_ticks{};
|
||||
std::uint64_t dropped_timer_ticks{};
|
||||
std::uint64_t completed_frames{};
|
||||
std::size_t idle_slots{};
|
||||
std::size_t rendering_slots{};
|
||||
std::size_t ready_slots{};
|
||||
std::size_t sending_slots{};
|
||||
double effective_frames_per_second{};
|
||||
double render_capacity_frames_per_second{};
|
||||
double send_capacity_frames_per_second{};
|
||||
Frame_Policy_Statistics render{};
|
||||
Frame_Policy_Statistics send{};
|
||||
Frame_Policy_Statistics end_to_end{};
|
||||
};
|
||||
struct Frame_Policy_Snapshot {
|
||||
std::uint64_t revision{};
|
||||
std::uint64_t timer_ticks{};
|
||||
std::uint64_t dropped_timer_ticks{};
|
||||
std::uint64_t completed_frames{};
|
||||
std::size_t idle_slots{};
|
||||
std::size_t rendering_slots{};
|
||||
std::size_t ready_slots{};
|
||||
std::size_t sending_slots{};
|
||||
};
|
||||
struct Frame_Policy_Context {
|
||||
Frame_Context frame{};
|
||||
Frame_Policy_Snapshot policy{};
|
||||
};
|
||||
namespace detail {
|
||||
struct Frame_Policy_State_Mirror {
|
||||
std::atomic_uint64_t revision{};
|
||||
std::atomic_uint64_t timer_ticks{};
|
||||
std::atomic_uint64_t dropped_timer_ticks{};
|
||||
std::atomic_uint64_t completed_frames{};
|
||||
std::atomic_size_t idle_slots{};
|
||||
std::atomic_size_t rendering_slots{};
|
||||
std::atomic_size_t ready_slots{};
|
||||
std::atomic_size_t sending_slots{};
|
||||
};
|
||||
inline Frame_Policy_State_Mirror& frame_policy_state_mirror() noexcept {
|
||||
static Frame_Policy_State_Mirror value;
|
||||
return value;
|
||||
}
|
||||
inline void store_frame_policy_snapshot(const Frame_Policy_State& value) noexcept {
|
||||
auto& mirror = frame_policy_state_mirror();
|
||||
mirror.revision.fetch_add(1, std::memory_order_acq_rel);
|
||||
mirror.timer_ticks.store(value.timer_ticks, std::memory_order_relaxed);
|
||||
mirror.dropped_timer_ticks.store(value.dropped_timer_ticks, std::memory_order_relaxed);
|
||||
mirror.completed_frames.store(value.completed_frames, std::memory_order_relaxed);
|
||||
mirror.idle_slots.store(value.idle_slots, std::memory_order_relaxed);
|
||||
mirror.rendering_slots.store(value.rendering_slots, std::memory_order_relaxed);
|
||||
mirror.ready_slots.store(value.ready_slots, std::memory_order_relaxed);
|
||||
mirror.sending_slots.store(value.sending_slots, std::memory_order_relaxed);
|
||||
mirror.revision.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
[[nodiscard]] inline Frame_Policy_Snapshot frame_policy_snapshot() noexcept {
|
||||
const auto& mirror = detail::frame_policy_state_mirror();
|
||||
const auto begin = mirror.revision.load(std::memory_order_acquire);
|
||||
if (begin == 0 || (begin & 1) != 0) return {};
|
||||
const Frame_Policy_Snapshot value{begin / 2, mirror.timer_ticks.load(std::memory_order_relaxed), mirror.dropped_timer_ticks.load(std::memory_order_relaxed), mirror.completed_frames.load(std::memory_order_relaxed), mirror.idle_slots.load(std::memory_order_relaxed), mirror.rendering_slots.load(std::memory_order_relaxed), mirror.ready_slots.load(std::memory_order_relaxed), mirror.sending_slots.load(std::memory_order_relaxed)};
|
||||
if (mirror.revision.load(std::memory_order_acquire) != begin) return {};
|
||||
return value;
|
||||
}
|
||||
inline void trace_frame_policy_state(const Frame_Policy_State& value) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
if (!tracy_connected()) return;
|
||||
detail::store_frame_policy_snapshot(value);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.FramePolicy.TimerTicks", value.timer_ticks);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.FramePolicy.DroppedTimerTicks", value.dropped_timer_ticks);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.FramePolicy.CompletedFrames", value.completed_frames);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.FramePolicy.IdleSlots", value.idle_slots);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.FramePolicy.RenderingSlots", value.rendering_slots);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.FramePolicy.ReadySlots", value.ready_slots);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.FramePolicy.SendingSlots", value.sending_slots);
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.EffectiveFPS", value.effective_frames_per_second);
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.RenderCapacityFPS", value.render_capacity_frames_per_second);
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.SendCapacityFPS", value.send_capacity_frames_per_second);
|
||||
if (value.render.sample_count != 0) {
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.RenderAverageNs", value.render.average_ns);
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.RenderP95Ns", value.render.p95_ns);
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.RenderStdDevNs", value.render.standard_deviation_ns);
|
||||
}
|
||||
if (value.send.sample_count != 0) {
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.SendAverageNs", value.send.average_ns);
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.SendP95Ns", value.send.p95_ns);
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.SendStdDevNs", value.send.standard_deviation_ns);
|
||||
}
|
||||
if (value.end_to_end.sample_count != 0) {
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.EndToEndAverageNs", value.end_to_end.average_ns);
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.EndToEndP95Ns", value.end_to_end.p95_ns);
|
||||
AETHERA_TRACE_PLOT_D("Aethera.FramePolicy.EndToEndStdDevNs", value.end_to_end.standard_deviation_ns);
|
||||
}
|
||||
#else
|
||||
static_cast<void>(value);
|
||||
#endif
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Frame_Policy_Context& value) {
|
||||
sink.field("event", "frame_policy_context");
|
||||
trace_serialize(sink, value.frame.context);
|
||||
sink.field("frame", value.frame.frame);
|
||||
sink.field("generation", value.frame.generation);
|
||||
sink.field("policy_revision", value.policy.revision);
|
||||
sink.field("timer_ticks", value.policy.timer_ticks);
|
||||
sink.field("dropped_timer_ticks", value.policy.dropped_timer_ticks);
|
||||
sink.field("completed_frames", value.policy.completed_frames);
|
||||
sink.field("idle_slots", value.policy.idle_slots);
|
||||
sink.field("rendering_slots", value.policy.rendering_slots);
|
||||
sink.field("ready_slots", value.policy.ready_slots);
|
||||
sink.field("sending_slots", value.policy.sending_slots);
|
||||
}
|
||||
inline void trace_frame_policy_context(const Frame_Context& frame) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
if (!tracy_connected()) return;
|
||||
const auto policy = frame_policy_snapshot();
|
||||
if (policy.revision == 0) return;
|
||||
AETHERA_TRACE_MESSAGE_TAGS(Frame_Policy_Context{frame, policy});
|
||||
#else
|
||||
static_cast<void>(frame);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include "Frame.hpp"
|
||||
#include "Tracy.hpp"
|
||||
#include <cstdint>
|
||||
namespace aethera::observation {
|
||||
struct Frame_Timing {
|
||||
Frame_Context frame{};
|
||||
std::uint64_t render_latency_ns{};
|
||||
std::uint64_t graph_start_latency_ns{};
|
||||
std::uint64_t graph_to_submit_ns{};
|
||||
std::uint64_t backend_prepare_ns{};
|
||||
std::uint64_t submit_path_ns{};
|
||||
std::uint64_t completion_tail_ns{};
|
||||
};
|
||||
inline void trace_frame_timing(const Frame_Timing& value) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Frame.RenderLatencyNs", value.render_latency_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Frame.GraphStartLatencyNs", value.graph_start_latency_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Frame.GraphToSubmitNs", value.graph_to_submit_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Frame.BackendPrepareNs", value.backend_prepare_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Frame.SubmitPathNs", value.submit_path_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Frame.CompletionTailNs", value.completion_tail_ns);
|
||||
AETHERA_TRACE_MESSAGE_TAGS(value);
|
||||
#else
|
||||
static_cast<void>(value);
|
||||
#endif
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Frame_Timing& value) {
|
||||
sink.field("event", "frame_timing");
|
||||
trace_serialize(sink, value.frame);
|
||||
sink.field("render_latency_ns", value.render_latency_ns);
|
||||
sink.field("graph_start_latency_ns", value.graph_start_latency_ns);
|
||||
sink.field("graph_to_submit_ns", value.graph_to_submit_ns);
|
||||
sink.field("backend_prepare_ns", value.backend_prepare_ns);
|
||||
sink.field("submit_path_ns", value.submit_path_ns);
|
||||
sink.field("completion_tail_ns", value.completion_tail_ns);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
#pragma once
|
||||
#include "Tracy.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
namespace aethera::observation {
|
||||
enum struct GPU_Completion_Event : std::uint8_t {
|
||||
capacity_exhausted,
|
||||
canceled,
|
||||
completed
|
||||
};
|
||||
struct GPU_Completion_State {
|
||||
std::size_t in_flight{};
|
||||
std::size_t active{};
|
||||
std::size_t capacity{};
|
||||
std::uint64_t capacity_exhausted_count{};
|
||||
std::uint64_t error_count{};
|
||||
};
|
||||
struct GPU_Completion_Metrics {
|
||||
GPU_Completion_Event event{};
|
||||
std::size_t in_flight{};
|
||||
std::size_t active{};
|
||||
std::size_t capacity{};
|
||||
std::uint64_t wait_ns{};
|
||||
std::uint64_t capacity_exhausted_count{};
|
||||
std::uint64_t error_count{};
|
||||
std::int32_t error{};
|
||||
std::int32_t backend_result{};
|
||||
};
|
||||
namespace detail {
|
||||
inline std::atomic_uint64_t& gpu_completion_capacity_exhausted_count() noexcept {
|
||||
static std::atomic_uint64_t value{};
|
||||
return value;
|
||||
}
|
||||
inline std::atomic_uint64_t& gpu_completion_error_count() noexcept {
|
||||
static std::atomic_uint64_t value{};
|
||||
return value;
|
||||
}
|
||||
inline std::atomic_size_t& gpu_completion_in_flight() noexcept {
|
||||
static std::atomic_size_t value{};
|
||||
return value;
|
||||
}
|
||||
inline std::atomic_size_t& gpu_completion_active_count() noexcept {
|
||||
static std::atomic_size_t value{};
|
||||
return value;
|
||||
}
|
||||
inline std::atomic_size_t& gpu_completion_capacity() noexcept {
|
||||
static std::atomic_size_t value{};
|
||||
return value;
|
||||
}
|
||||
}
|
||||
[[nodiscard]] inline GPU_Completion_State gpu_completion_state() noexcept {
|
||||
return GPU_Completion_State{detail::gpu_completion_in_flight().load(std::memory_order_relaxed), detail::gpu_completion_active_count().load(std::memory_order_relaxed), detail::gpu_completion_capacity().load(std::memory_order_relaxed), detail::gpu_completion_capacity_exhausted_count().load(std::memory_order_relaxed), detail::gpu_completion_error_count().load(std::memory_order_relaxed)};
|
||||
}
|
||||
inline void trace_gpu_completion_admitted(std::size_t in_flight, std::size_t capacity) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
if (!tracy_connected()) return;
|
||||
detail::gpu_completion_in_flight().store(in_flight, std::memory_order_relaxed);
|
||||
detail::gpu_completion_capacity().store(capacity, std::memory_order_relaxed);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.InFlight", in_flight);
|
||||
#else
|
||||
static_cast<void>(in_flight);
|
||||
static_cast<void>(capacity);
|
||||
#endif
|
||||
}
|
||||
inline void trace_gpu_completion_capacity_exhausted(std::size_t in_flight, std::size_t capacity) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
if (!tracy_connected()) return;
|
||||
detail::gpu_completion_in_flight().store(in_flight, std::memory_order_relaxed);
|
||||
detail::gpu_completion_capacity().store(capacity, std::memory_order_relaxed);
|
||||
const auto count = detail::gpu_completion_capacity_exhausted_count().fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.CapacityExhausted", count);
|
||||
AETHERA_TRACE_MESSAGE_TAGS(GPU_Completion_Metrics{GPU_Completion_Event::capacity_exhausted, in_flight, 0, capacity, 0, count});
|
||||
#else
|
||||
static_cast<void>(in_flight);
|
||||
static_cast<void>(capacity);
|
||||
#endif
|
||||
}
|
||||
inline void trace_gpu_completion_active(std::size_t in_flight, std::size_t active) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
if (!tracy_connected()) return;
|
||||
detail::gpu_completion_in_flight().store(in_flight, std::memory_order_relaxed);
|
||||
detail::gpu_completion_active_count().store(active, std::memory_order_relaxed);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.InFlight", in_flight);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.Active", active);
|
||||
#else
|
||||
static_cast<void>(in_flight);
|
||||
static_cast<void>(active);
|
||||
#endif
|
||||
}
|
||||
inline void trace_gpu_completion_canceled(std::size_t in_flight, std::size_t active, std::size_t capacity) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
if (!tracy_connected()) return;
|
||||
detail::gpu_completion_in_flight().store(in_flight, std::memory_order_relaxed);
|
||||
detail::gpu_completion_active_count().store(active, std::memory_order_relaxed);
|
||||
detail::gpu_completion_capacity().store(capacity, std::memory_order_relaxed);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.InFlight", in_flight);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.Active", active);
|
||||
AETHERA_TRACE_MESSAGE_TAGS(GPU_Completion_Metrics{GPU_Completion_Event::canceled, in_flight, active, capacity});
|
||||
#else
|
||||
static_cast<void>(in_flight);
|
||||
static_cast<void>(active);
|
||||
static_cast<void>(capacity);
|
||||
#endif
|
||||
}
|
||||
inline void trace_gpu_completion_completed(std::size_t in_flight, std::size_t active, std::size_t capacity, std::chrono::steady_clock::time_point watched_at, std::int32_t error, std::int32_t backend_result) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
if (!tracy_connected()) return;
|
||||
detail::gpu_completion_in_flight().store(in_flight, std::memory_order_relaxed);
|
||||
detail::gpu_completion_active_count().store(active, std::memory_order_relaxed);
|
||||
detail::gpu_completion_capacity().store(capacity, std::memory_order_relaxed);
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - watched_at).count();
|
||||
const auto wait_ns = elapsed > 0 ? static_cast<std::uint64_t>(elapsed) : 0;
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.InFlight", in_flight);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.Active", active);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.WaitNs", wait_ns);
|
||||
if (error == 0) return;
|
||||
const auto error_count = detail::gpu_completion_error_count().fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
AETHERA_TRACE_PLOT_I("Aethera.GPUCompletion.Errors", error_count);
|
||||
AETHERA_TRACE_MESSAGE_TAGS(GPU_Completion_Metrics{GPU_Completion_Event::completed, in_flight, active, capacity, wait_ns, detail::gpu_completion_capacity_exhausted_count().load(std::memory_order_relaxed), error_count, error, backend_result});
|
||||
#else
|
||||
static_cast<void>(in_flight);
|
||||
static_cast<void>(active);
|
||||
static_cast<void>(capacity);
|
||||
static_cast<void>(watched_at);
|
||||
static_cast<void>(error);
|
||||
static_cast<void>(backend_result);
|
||||
#endif
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const GPU_Completion_Metrics& value) {
|
||||
sink.field("event", value.event);
|
||||
sink.field("in_flight", value.in_flight);
|
||||
sink.field("active", value.active);
|
||||
sink.field("capacity", value.capacity);
|
||||
if (value.wait_ns != 0) sink.field("wait_ns", value.wait_ns);
|
||||
if (value.capacity_exhausted_count != 0) sink.field("capacity_exhausted_count", value.capacity_exhausted_count);
|
||||
if (value.error_count != 0) sink.field("error_count", value.error_count);
|
||||
if (value.error != 0) sink.field("error", value.error);
|
||||
if (value.backend_result != 0) sink.field("backend_result", value.backend_result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
#include "Context.hpp"
|
||||
#include "Frame.hpp"
|
||||
#include "Tracy.hpp"
|
||||
#include <cstdint>
|
||||
namespace aethera::observation {
|
||||
struct Frame_Resource_Context {
|
||||
Context context{};
|
||||
std::uint64_t frame{};
|
||||
std::uint64_t generation{};
|
||||
};
|
||||
struct Frame_Resource_Metrics {
|
||||
Frame_Resource_Context resource{};
|
||||
std::uint64_t uploaded_bytes{};
|
||||
std::uint64_t readback_bytes{};
|
||||
std::uint64_t buffer_create_ns{};
|
||||
std::uint64_t texture_create_ns{};
|
||||
std::uint64_t shader_compile_ns{};
|
||||
std::uint64_t pipeline_create_ns{};
|
||||
std::uint64_t upload_ns{};
|
||||
std::uint64_t staging_allocate_ns{};
|
||||
std::uint64_t host_copy_ns{};
|
||||
std::uint64_t submit_queue_wait_ns{};
|
||||
std::uint64_t fence_wait_ns{};
|
||||
std::uint64_t transfer_ns{};
|
||||
std::uint32_t pipeline_create_count{};
|
||||
};
|
||||
[[nodiscard]] inline Frame_Resource_Context make_frame_resource_context(const Frame_Context& frame) noexcept {
|
||||
return Frame_Resource_Context{make_child_context(frame.context), frame.frame, frame.generation};
|
||||
}
|
||||
[[nodiscard]] inline bool has_resource_activity(const Frame_Resource_Metrics& value) noexcept {
|
||||
return value.uploaded_bytes != 0 || value.readback_bytes != 0 || value.buffer_create_ns != 0 || value.texture_create_ns != 0 || value.shader_compile_ns != 0 || value.pipeline_create_ns != 0 || value.upload_ns != 0 || value.staging_allocate_ns != 0 || value.host_copy_ns != 0 || value.submit_queue_wait_ns != 0 || value.fence_wait_ns != 0 || value.transfer_ns != 0 || value.pipeline_create_count != 0;
|
||||
}
|
||||
inline void trace_frame_resources(const Frame_Resource_Metrics& value) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Resource.UploadedBytes", value.uploaded_bytes);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Resource.ReadbackBytes", value.readback_bytes);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Resource.UploadNs", value.upload_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Resource.UploadQueueWaitNs", value.submit_queue_wait_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Resource.UploadFenceWaitNs", value.fence_wait_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Resource.ShaderCompileNs", value.shader_compile_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Resource.PipelineCreateNs", value.pipeline_create_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Resource.PipelineCreateCount", value.pipeline_create_count);
|
||||
if (has_resource_activity(value)) AETHERA_TRACE_MESSAGE_TAGS(value);
|
||||
#else
|
||||
static_cast<void>(value);
|
||||
#endif
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Frame_Resource_Context& value) {
|
||||
sink.field("event", "frame_resource_scope");
|
||||
trace_serialize(sink, value.context);
|
||||
sink.field("frame", value.frame);
|
||||
sink.field("generation", value.generation);
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Frame_Resource_Metrics& value) {
|
||||
sink.field("event", "frame_resources");
|
||||
trace_serialize(sink, value.resource.context);
|
||||
sink.field("frame", value.resource.frame);
|
||||
sink.field("generation", value.resource.generation);
|
||||
sink.field("uploaded_bytes", value.uploaded_bytes);
|
||||
sink.field("readback_bytes", value.readback_bytes);
|
||||
sink.field("buffer_create_ns", value.buffer_create_ns);
|
||||
sink.field("texture_create_ns", value.texture_create_ns);
|
||||
sink.field("shader_compile_ns", value.shader_compile_ns);
|
||||
sink.field("pipeline_create_ns", value.pipeline_create_ns);
|
||||
sink.field("upload_ns", value.upload_ns);
|
||||
sink.field("staging_allocate_ns", value.staging_allocate_ns);
|
||||
sink.field("host_copy_ns", value.host_copy_ns);
|
||||
sink.field("submit_queue_wait_ns", value.submit_queue_wait_ns);
|
||||
sink.field("fence_wait_ns", value.fence_wait_ns);
|
||||
sink.field("transfer_ns", value.transfer_ns);
|
||||
sink.field("pipeline_create_count", value.pipeline_create_count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include "Tracy.hpp"
|
||||
#include <mutex>
|
||||
#ifdef TRACY_ENABLE
|
||||
#define AETHERA_OBSERVATION_MUTEX(varname, description) TracyLockableN(std::mutex, varname, description)
|
||||
#else
|
||||
#define AETHERA_OBSERVATION_MUTEX(varname, description) std::mutex varname
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include "Context.hpp"
|
||||
#include "Frame.hpp"
|
||||
#include "Tracy.hpp"
|
||||
#include <task_flow/export/export.h>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
namespace aethera::observation {
|
||||
struct Taskflow_Task_Context {
|
||||
Context context{};
|
||||
std::uint64_t frame{};
|
||||
std::uint64_t generation{};
|
||||
std::string_view graph{};
|
||||
std::size_t worker{};
|
||||
std::uint64_t task{};
|
||||
};
|
||||
[[nodiscard]] inline Taskflow_Trace_Context make_taskflow_trace_context(const Frame_Context& frame) noexcept {
|
||||
const auto context = make_correlation_context(frame.context);
|
||||
return Taskflow_Trace_Context{frame.frame, frame.generation, context.root.value, context.scope.value, context.parent.value, context.correlation.value};
|
||||
}
|
||||
inline void trace_taskflow_task(Tracy_Task_Zone zone, const Taskflow_Trace_Context& execution, std::string_view graph, std::size_t worker, std::uint64_t task) {
|
||||
#ifdef TRACY_ENABLE
|
||||
attach_tracy_task_tags(zone, Taskflow_Task_Context{{Identity{execution.root}, Identity{execution.scope}, Identity{execution.parent}, Identity{execution.correlation}}, execution.frame, execution.generation, graph, worker, task});
|
||||
#else
|
||||
static_cast<void>(zone);
|
||||
static_cast<void>(execution);
|
||||
static_cast<void>(graph);
|
||||
static_cast<void>(worker);
|
||||
static_cast<void>(task);
|
||||
#endif
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Taskflow_Task_Context& value) {
|
||||
sink.field("event", "taskflow_task");
|
||||
trace_serialize(sink, value.context);
|
||||
sink.field("frame", value.frame);
|
||||
sink.field("generation", value.generation);
|
||||
sink.field("graph", value.graph);
|
||||
sink.field("worker", value.worker);
|
||||
sink.field("task", value.task);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Serialize.hpp"
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <charconv>
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
@@ -20,10 +21,19 @@ class Tracy_Text_Sink {
|
||||
public:
|
||||
template <class T>
|
||||
void field(std::string_view name, const T& value) {
|
||||
if (size_ != 0) append(" ");
|
||||
const auto begin = size_;
|
||||
field_overflow_ = false;
|
||||
if (field_count_ != 0) {
|
||||
++required_size_;
|
||||
if (size_ != 0) append_buffer(" ");
|
||||
}
|
||||
++field_count_;
|
||||
append(name);
|
||||
append("=");
|
||||
append_value(value);
|
||||
if (!field_overflow_) return;
|
||||
size_ = begin;
|
||||
truncated_ = true;
|
||||
}
|
||||
[[nodiscard]] const char* data() const noexcept {
|
||||
return buffer_.data();
|
||||
@@ -31,18 +41,35 @@ public:
|
||||
[[nodiscard]] std::size_t size() const noexcept {
|
||||
return size_;
|
||||
}
|
||||
[[nodiscard]] std::size_t required_size() const noexcept {
|
||||
return required_size_;
|
||||
}
|
||||
[[nodiscard]] constexpr std::size_t capacity() const noexcept {
|
||||
return buffer_.size();
|
||||
}
|
||||
[[nodiscard]] bool empty() const noexcept {
|
||||
return size_ == 0;
|
||||
}
|
||||
[[nodiscard]] bool truncated() const noexcept {
|
||||
return truncated_;
|
||||
}
|
||||
private:
|
||||
template <class>
|
||||
static constexpr bool always_false = false;
|
||||
void append(std::string_view value) {
|
||||
required_size_ += value.size();
|
||||
append_buffer(value);
|
||||
}
|
||||
void append_buffer(std::string_view value) {
|
||||
if (field_overflow_) return;
|
||||
const auto available = buffer_.size() - size_;
|
||||
const auto count = value.size() < available ? value.size() : available;
|
||||
if (count == 0) return;
|
||||
std::memcpy(buffer_.data() + size_, value.data(), count);
|
||||
size_ += count;
|
||||
if (value.size() > available) {
|
||||
field_overflow_ = true;
|
||||
return;
|
||||
}
|
||||
if (value.empty()) return;
|
||||
std::memcpy(buffer_.data() + size_, value.data(), value.size());
|
||||
size_ += value.size();
|
||||
}
|
||||
template <class T>
|
||||
void append_number(T value) {
|
||||
@@ -75,7 +102,25 @@ private:
|
||||
}
|
||||
std::array<char, 1024> buffer_{};
|
||||
std::size_t size_{};
|
||||
std::size_t required_size_{};
|
||||
std::size_t field_count_{};
|
||||
bool field_overflow_{};
|
||||
bool truncated_{};
|
||||
};
|
||||
struct Tracy_Text_Truncation {
|
||||
const char* channel{};
|
||||
std::size_t required_bytes{};
|
||||
std::size_t capacity_bytes{};
|
||||
std::uint64_t count{};
|
||||
};
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Tracy_Text_Truncation& value) {
|
||||
sink.field("event", "observation_text_truncated");
|
||||
sink.field("channel", value.channel);
|
||||
sink.field("required_bytes", value.required_bytes);
|
||||
sink.field("capacity_bytes", value.capacity_bytes);
|
||||
sink.field("count", value.count);
|
||||
}
|
||||
#ifdef TRACY_ENABLE
|
||||
struct Tracy_Task_Zone {
|
||||
TracyCZoneCtx context{};
|
||||
@@ -83,6 +128,15 @@ struct Tracy_Task_Zone {
|
||||
[[nodiscard]] inline bool tracy_connected() noexcept {
|
||||
return TracyCIsConnected != 0;
|
||||
}
|
||||
inline void trace_tracy_text_truncation(const char* channel, const Tracy_Text_Sink& truncated) noexcept {
|
||||
static std::atomic_uint64_t count{};
|
||||
const auto current = count.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
TracyCPlotI("Aethera.Observation.TextTruncations", static_cast<std::int64_t>(current))
|
||||
TracyCPlotI("Aethera.Observation.TruncatedRequiredBytes", static_cast<std::int64_t>(truncated.required_size()))
|
||||
Tracy_Text_Sink sink;
|
||||
serialize(sink, Tracy_Text_Truncation{channel, truncated.required_size(), truncated.capacity(), current});
|
||||
TracyCMessage(sink.data(), sink.size())
|
||||
}
|
||||
inline void set_tracy_thread_name(const char* name) noexcept {
|
||||
TracyCSetThreadName(name)
|
||||
}
|
||||
@@ -103,6 +157,7 @@ void attach_tracy_task_tags(Tracy_Task_Zone zone, const Tags& tags) {
|
||||
Tracy_Text_Sink sink;
|
||||
serialize(sink, tags);
|
||||
if (!sink.empty()) TracyCZoneText(zone.context, sink.data(), sink.size())
|
||||
if (sink.truncated()) trace_tracy_text_truncation("task_zone", sink);
|
||||
}
|
||||
template <class Make_Tags>
|
||||
void attach_tracy_tags(tracy::ScopedZone& zone, Make_Tags&& make_tags) {
|
||||
@@ -110,6 +165,15 @@ void attach_tracy_tags(tracy::ScopedZone& zone, Make_Tags&& make_tags) {
|
||||
Tracy_Text_Sink sink;
|
||||
serialize(sink, make_tags());
|
||||
if (!sink.empty()) zone.Text(sink.data(), sink.size());
|
||||
if (sink.truncated()) trace_tracy_text_truncation("zone", sink);
|
||||
}
|
||||
template <class Tags>
|
||||
void emit_tracy_message(const Tags& tags) {
|
||||
if (!tracy_connected()) return;
|
||||
Tracy_Text_Sink sink;
|
||||
serialize(sink, tags);
|
||||
if (!sink.empty()) TracyCMessage(sink.data(), sink.size())
|
||||
if (sink.truncated()) trace_tracy_text_truncation("message", sink);
|
||||
}
|
||||
#else
|
||||
struct Tracy_Task_Zone {};
|
||||
@@ -121,19 +185,25 @@ inline void set_tracy_thread_name(const char*) noexcept {}
|
||||
return {};
|
||||
}
|
||||
inline void end_tracy_task_zone(Tracy_Task_Zone) noexcept {}
|
||||
template <class Tags>
|
||||
void emit_tracy_message(const Tags&) {}
|
||||
#endif
|
||||
}
|
||||
#ifdef TRACY_ENABLE
|
||||
#define AETHERA_TRACE_ZONE(name) ZoneScopedN(name)
|
||||
#define AETHERA_TRACE_ZONE_TAGS(name, ...) ZoneScopedN(name); ::aethera::observation::attach_tracy_tags(___tracy_scoped_zone, [&]() -> decltype(auto) { return (__VA_ARGS__); })
|
||||
#define AETHERA_TRACE_MESSAGE_TAGS(...) do { ::aethera::observation::emit_tracy_message((__VA_ARGS__)); } while (false)
|
||||
#define AETHERA_TRACE_PLOT_I(name, value) do { if (::aethera::observation::tracy_connected()) TracyCPlotI(name, static_cast<std::int64_t>(value)) } while (false)
|
||||
#define AETHERA_TRACE_PLOT_D(name, value) do { if (::aethera::observation::tracy_connected()) TracyCPlot(name, static_cast<double>(value)) } while (false)
|
||||
#define AETHERA_TRACE_THREAD_NAME(name) do { ::aethera::observation::set_tracy_thread_name(name); } while (false)
|
||||
#define AETHERA_TRACE_FRAME(name) FrameMarkNamed(name)
|
||||
#define AETHERA_TRACE_TASKFLOW_CONTEXT_ARG(frame, generation) , ::aethera::Taskflow_Trace_Context{frame, generation}
|
||||
#else
|
||||
#define AETHERA_TRACE_ZONE(name)
|
||||
#define AETHERA_TRACE_ZONE_TAGS(name, ...)
|
||||
#define AETHERA_TRACE_MESSAGE_TAGS(...)
|
||||
#define AETHERA_TRACE_PLOT_I(name, value)
|
||||
#define AETHERA_TRACE_PLOT_D(name, value)
|
||||
#define AETHERA_TRACE_THREAD_NAME(name)
|
||||
#define AETHERA_TRACE_FRAME(name)
|
||||
#define AETHERA_TRACE_TASKFLOW_CONTEXT_ARG(frame, generation)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include "Tracy.hpp"
|
||||
#ifdef TRACY_ENABLE
|
||||
#include <tracy/TracyVulkan.hpp>
|
||||
#define AETHERA_TRACE_VULKAN_CONTEXT(...) TracyVkContext(__VA_ARGS__)
|
||||
#define AETHERA_TRACE_VULKAN_DESTROY(context) TracyVkDestroy(context)
|
||||
#define AETHERA_TRACE_VULKAN_CONTEXT_NAME(context, name, size) TracyVkContextName(context, name, size)
|
||||
#define AETHERA_TRACE_VULKAN_ZONE(context, command_buffer, name) TracyVkZone(context, command_buffer, name)
|
||||
#define AETHERA_TRACE_VULKAN_ZONE_TAGS(context, command_buffer, name, ...) AETHERA_TRACE_MESSAGE_TAGS(__VA_ARGS__); TracyVkZone(context, command_buffer, name)
|
||||
#define AETHERA_TRACE_VULKAN_COLLECT(context, command_buffer) TracyVkCollect(context, command_buffer)
|
||||
#define AETHERA_TRACE_VULKAN_COLLECT_HOST(context) TracyVkCollectHost(context)
|
||||
#else
|
||||
#define AETHERA_TRACE_VULKAN_CONTEXT(...) nullptr
|
||||
#define AETHERA_TRACE_VULKAN_DESTROY(context)
|
||||
#define AETHERA_TRACE_VULKAN_CONTEXT_NAME(context, name, size)
|
||||
#define AETHERA_TRACE_VULKAN_ZONE(context, command_buffer, name)
|
||||
#define AETHERA_TRACE_VULKAN_ZONE_TAGS(context, command_buffer, name, ...)
|
||||
#define AETHERA_TRACE_VULKAN_COLLECT(context, command_buffer)
|
||||
#define AETHERA_TRACE_VULKAN_COLLECT_HOST(context)
|
||||
#endif
|
||||
@@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
#include "Context.hpp"
|
||||
#include "Frame.hpp"
|
||||
#include "Tracy.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
namespace aethera::observation {
|
||||
struct Vulkan_Submission_Context {
|
||||
Context context{};
|
||||
std::uint64_t frame{};
|
||||
std::uint64_t generation{};
|
||||
std::uint64_t queue{};
|
||||
bool frame_bound{};
|
||||
};
|
||||
struct Vulkan_GPU_Context {
|
||||
Context context{};
|
||||
std::uint64_t frame{};
|
||||
std::uint64_t generation{};
|
||||
std::uint64_t queue{};
|
||||
bool frame_bound{};
|
||||
};
|
||||
struct Vulkan_Completion_Context {
|
||||
Context context{};
|
||||
std::uint64_t frame{};
|
||||
std::uint64_t generation{};
|
||||
std::uint64_t queue{};
|
||||
std::uint64_t latency_ns{};
|
||||
bool frame_bound{};
|
||||
};
|
||||
struct Vulkan_State {
|
||||
std::uint64_t pending_submissions{};
|
||||
std::uint64_t frames_in_flight{};
|
||||
};
|
||||
struct Vulkan_GPU_Timing_Context {
|
||||
Context context{};
|
||||
std::uint64_t frame{};
|
||||
std::uint64_t generation{};
|
||||
std::uint64_t queue{};
|
||||
std::uint64_t render_ns{};
|
||||
std::uint64_t transition_ns{};
|
||||
std::uint64_t copy_ns{};
|
||||
std::uint64_t total_ns{};
|
||||
bool frame_bound{};
|
||||
};
|
||||
namespace detail {
|
||||
inline std::atomic_uint64_t& vulkan_pending_submissions() noexcept {
|
||||
static std::atomic_uint64_t value{};
|
||||
return value;
|
||||
}
|
||||
inline std::atomic_uint64_t& vulkan_frames_in_flight() noexcept {
|
||||
static std::atomic_uint64_t value{};
|
||||
return value;
|
||||
}
|
||||
}
|
||||
[[nodiscard]] inline Vulkan_State vulkan_state() noexcept {
|
||||
return Vulkan_State{detail::vulkan_pending_submissions().load(std::memory_order_relaxed), detail::vulkan_frames_in_flight().load(std::memory_order_relaxed)};
|
||||
}
|
||||
[[nodiscard]] inline Vulkan_Submission_Context make_vulkan_submission_context(const Frame_Context& frame) noexcept {
|
||||
return Vulkan_Submission_Context{make_correlation_context(frame.context), frame.frame, frame.generation, 0, true};
|
||||
}
|
||||
[[nodiscard]] inline Vulkan_Submission_Context make_vulkan_submission_context(std::uint64_t queue) noexcept {
|
||||
auto context = make_root_context();
|
||||
context.correlation = context.scope;
|
||||
return Vulkan_Submission_Context{context, 0, 0, queue, false};
|
||||
}
|
||||
[[nodiscard]] inline Vulkan_GPU_Context make_vulkan_gpu_context(const Vulkan_Submission_Context& submission) noexcept {
|
||||
return Vulkan_GPU_Context{make_correlated_context(submission.context), submission.frame, submission.generation, submission.queue, submission.frame_bound};
|
||||
}
|
||||
[[nodiscard]] inline Vulkan_Completion_Context make_vulkan_completion_context(const Vulkan_Submission_Context& submission, std::uint64_t latency_ns) noexcept {
|
||||
return Vulkan_Completion_Context{make_correlated_context(submission.context), submission.frame, submission.generation, submission.queue, latency_ns, submission.frame_bound};
|
||||
}
|
||||
[[nodiscard]] inline Vulkan_GPU_Timing_Context make_vulkan_gpu_timing_context(const Vulkan_Submission_Context& submission, std::uint64_t render_ns, std::uint64_t transition_ns, std::uint64_t copy_ns, std::uint64_t total_ns) noexcept {
|
||||
return Vulkan_GPU_Timing_Context{make_correlated_context(submission.context), submission.frame, submission.generation, submission.queue, render_ns, transition_ns, copy_ns, total_ns, submission.frame_bound};
|
||||
}
|
||||
template <class Submit>
|
||||
decltype(auto) trace_vulkan_submit(Submit&& submit) {
|
||||
AETHERA_TRACE_ZONE("Vulkan.Submit");
|
||||
return std::invoke(std::forward<Submit>(submit));
|
||||
}
|
||||
inline void trace_vulkan_submitted(const Vulkan_Submission_Context& value) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
const auto pending = detail::vulkan_pending_submissions().fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
AETHERA_TRACE_MESSAGE_TAGS(value);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Vulkan.PendingSubmissions", pending);
|
||||
if (!value.frame_bound) return;
|
||||
const auto frames = detail::vulkan_frames_in_flight().fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Vulkan.FramesInFlight", frames);
|
||||
#else
|
||||
static_cast<void>(value);
|
||||
#endif
|
||||
}
|
||||
inline void trace_vulkan_completed(const Vulkan_Submission_Context& submission, std::uint64_t latency_ns) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
const auto value = make_vulkan_completion_context(submission, latency_ns);
|
||||
const auto pending = detail::vulkan_pending_submissions().fetch_sub(1, std::memory_order_relaxed) - 1;
|
||||
AETHERA_TRACE_MESSAGE_TAGS(value);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Vulkan.PendingSubmissions", pending);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Vulkan.SubmitToCompleteNs", latency_ns);
|
||||
if (!submission.frame_bound) return;
|
||||
const auto frames = detail::vulkan_frames_in_flight().fetch_sub(1, std::memory_order_relaxed) - 1;
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Vulkan.FramesInFlight", frames);
|
||||
#else
|
||||
static_cast<void>(submission);
|
||||
static_cast<void>(latency_ns);
|
||||
#endif
|
||||
}
|
||||
inline void trace_vulkan_gpu_timing(const Vulkan_Submission_Context& submission, std::uint64_t render_ns, std::uint64_t transition_ns, std::uint64_t copy_ns, std::uint64_t total_ns) noexcept {
|
||||
#ifdef TRACY_ENABLE
|
||||
const auto value = make_vulkan_gpu_timing_context(submission, render_ns, transition_ns, copy_ns, total_ns);
|
||||
AETHERA_TRACE_MESSAGE_TAGS(value);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Vulkan.GPU.RenderNs", render_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Vulkan.GPU.TransitionNs", transition_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Vulkan.GPU.CopyNs", copy_ns);
|
||||
AETHERA_TRACE_PLOT_I("Aethera.Vulkan.GPU.TotalNs", total_ns);
|
||||
#else
|
||||
static_cast<void>(submission);
|
||||
static_cast<void>(render_ns);
|
||||
static_cast<void>(transition_ns);
|
||||
static_cast<void>(copy_ns);
|
||||
static_cast<void>(total_ns);
|
||||
#endif
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Vulkan_Submission_Context& value) {
|
||||
sink.field("event", "vulkan_submit");
|
||||
trace_serialize(sink, value.context);
|
||||
if (value.frame_bound) {
|
||||
sink.field("frame", value.frame);
|
||||
sink.field("generation", value.generation);
|
||||
}
|
||||
if (value.queue != 0) sink.field("queue", value.queue);
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Vulkan_GPU_Context& value) {
|
||||
sink.field("event", "vulkan_gpu");
|
||||
trace_serialize(sink, value.context);
|
||||
if (value.frame_bound) {
|
||||
sink.field("frame", value.frame);
|
||||
sink.field("generation", value.generation);
|
||||
}
|
||||
if (value.queue != 0) sink.field("queue", value.queue);
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Vulkan_GPU_Timing_Context& value) {
|
||||
sink.field("event", "vulkan_gpu_timing");
|
||||
trace_serialize(sink, value.context);
|
||||
if (value.frame_bound) {
|
||||
sink.field("frame", value.frame);
|
||||
sink.field("generation", value.generation);
|
||||
}
|
||||
if (value.queue != 0) sink.field("queue", value.queue);
|
||||
sink.field("render_ns", value.render_ns);
|
||||
sink.field("transition_ns", value.transition_ns);
|
||||
sink.field("copy_ns", value.copy_ns);
|
||||
sink.field("total_ns", value.total_ns);
|
||||
}
|
||||
template <class Sink>
|
||||
void trace_serialize(Sink& sink, const Vulkan_Completion_Context& value) {
|
||||
sink.field("event", "vulkan_complete");
|
||||
trace_serialize(sink, value.context);
|
||||
if (value.frame_bound) {
|
||||
sink.field("frame", value.frame);
|
||||
sink.field("generation", value.generation);
|
||||
}
|
||||
if (value.queue != 0) sink.field("queue", value.queue);
|
||||
sink.field("latency_ns", value.latency_ns);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
# Observation 设计原则
|
||||
|
||||
## 目标
|
||||
|
||||
Observation 负责建立统一的业务身份、时间作用域和跨时间线关联,并把事实投影到 Tracy 最合适的一等能力。业务实现只表达自身行为,不负责决定如何观测,也不直接依赖 Tracy。
|
||||
|
||||
Observation 不是另一套 profiler,也不是 Tracy 前面的通用事件总线。CPU/GPU/Lock/Memory/Frame 等 Tracy 已经原生支持的模型直接使用 Tracy;Observation 只补业务身份、关联关系和 Tracy 无法从底层事件自行推导的业务语义。
|
||||
|
||||
## 骨架
|
||||
|
||||
统一骨架由 `Context` 表达:
|
||||
|
||||
- `root`:当前分析根身份。通常可以是 Frame,也可以是资源加载、后台编译等非 Frame 工作。
|
||||
- `scope`:当前逻辑操作身份。
|
||||
- `parent`:当前逻辑操作的直接逻辑父身份。
|
||||
- `correlation`:跨线程、跨 CPU/GPU 时间线关联同一次操作的身份。
|
||||
|
||||
`Identity{0}` 表示未建立身份;`make_identity()` 生成进程内单调唯一的非零身份。身份只用于一次捕获中的关联,不要求等于业务对象的原生 ID。业务原生 ID,例如 frame index、Taskflow native id、queue handle,应作为对应 adapter 的附加语义投影到 Tracy。
|
||||
|
||||
`make_root_context()` 建立分析根,`root` 和 `scope` 指向同一身份。`make_child_context()` 建立同一逻辑时间线中的子作用域并保留 `parent`。`make_correlation_context()` 建立可以被其他时间线引用的逻辑操作。`make_correlated_context()` 用于跨线程或跨 CPU/GPU 时间线继续同一次操作,只保留 `root` 和 `correlation`,不伪造跨时间线 `parent`。
|
||||
|
||||
骨架不保存统一 Span Tree,不尝试把跨线程和跨 CPU/GPU 的工作伪造成 Tracy Zone 父子关系。物理时间关系由 Tracy 原生时间线表示,逻辑关系由 Context 身份关联。
|
||||
|
||||
## Frame
|
||||
|
||||
`Frame_Context` 是 Frame 分析根。每个新 Frame 创建新的 root/scope,并保留业务自己的 frame index 作为附加语义。
|
||||
|
||||
Frame 边界直接投影到 Tracy Frame。Frame identity 不编码到 Frame 名称中,避免把每个 Frame 变成不同统计项;root/scope/frame index 应附着到与该 Frame 相关的 CPU Zone、submission 或其他语义事件上。
|
||||
|
||||
Frame 只是最常见的 root,不是 Observation 的硬编码全局根。后台资源加载、shader 编译等长期工作可以独立建立 root。
|
||||
|
||||
## Vulkan submission、GPU execution 与 completion
|
||||
|
||||
Observation 必须遵循当前后端真实完成机制。现有 Render3D 使用 VkFence 和已有 `Gpu_Completion_Service` 确认完成,因此 Frame submission 的生命周期由 `gpu_submitted` 与 `gpu_completed` 两个既有标记投影;不得为了观测伪造 timeline semaphore。DRP2 即时上传使用自己的 fence,是独立 submission,不伪造为某个 Frame 的子操作。
|
||||
|
||||
一次属于 Frame 的 Vulkan submission 从所属 Frame 建立新的 correlation identity:
|
||||
|
||||
```text
|
||||
Frame root/scope
|
||||
|
|
||||
+-- CPU submission scope/correlation
|
||||
|
|
||||
+---- correlation ---- GPU execution scope
|
||||
|
|
||||
+---- correlation ---- completion scope
|
||||
```
|
||||
|
||||
CPU submission 与 Frame 在同一逻辑链上,因此 submission 保留 `parent=frame.scope`。GPU execution 和 completion 可能发生在其他时间线或线程,因此只继承 `root` 和 `correlation`,`parent` 必须为空。
|
||||
|
||||
业务 queue identity、frame sequence/generation、submission latency 等不是 Observation identity。它们作为 `Vulkan_Submission_Context`、`Vulkan_GPU_Context`、`Vulkan_Completion_Context` 的附加语义投影到 Tracy。
|
||||
|
||||
GPU 执行时间优先使用 Tracy Vulkan GPU Zone,不用 CPU submit/completion 时间戳模拟 GPU 时间。GPU Zone 本身不承担任意业务字段存储;correlation 等语义在 CPU submission/completion 的 Message 或其他 Tracy 原生通道中表达,GPU Zone 保持稳定名称用于聚合统计。
|
||||
|
||||
Tracy Vulkan Zone 只有在 command buffer 每次执行都会重新录制时才能直接对应每次 submission。当前 Datoviz Frame Target 会复用已经录好的 primary command buffer;`TracyVkZone` 的事件和 query identity 在录制时产生,不能把一次录制的 Zone 当成后续所有复用 submission 的新 Zone。禁止为了观测强制每帧重录并破坏 retained command buffer 的性能语义。当前路径使用 Datoviz 已有、每次执行都正确采集的 GPU timestamp 结果投影到 Tracy Plot/Message;以后只有出现不破坏复用语义的可录制 per-submit wrapper 或后端原生入口时,再接 Tracy Vulkan GPU Zone。
|
||||
|
||||
Tracy Vulkan query 必须按 Tracy 原生要求 collect。Observation 不额外建立 GPU 轮询线程,也不复制一套 timestamp query 系统。
|
||||
|
||||
## Tracy 投影
|
||||
|
||||
不同事实直接投影到 Tracy 最合适的一等能力:
|
||||
|
||||
- CPU 执行区间 -> CPU Zone
|
||||
- GPU 执行区间 -> Tracy Vulkan GPU Zone
|
||||
- Frame 边界 -> Tracy Frame
|
||||
- 锁等待和持有 -> Tracy Lock profiling
|
||||
- 内存分配和释放 -> Tracy Memory profiling
|
||||
- 连续状态和计数 -> Tracy Plot
|
||||
- 瞬时完成、提交等语义事件 -> Tracy Message 或对应 CPU Zone
|
||||
- 业务身份和补充语义 -> Zone Text / Value
|
||||
|
||||
禁止为了统一接口,把所有事实先编码成通用 Event 再异步翻译成 Tracy。Tracy 对线程、时间点和 GPU command buffer 上下文有原生语义,观测必须在真实行为边界同步投影。
|
||||
|
||||
`Tracy.hpp` 只封装 Tracy 通用 CPU/Message/Plot/Frame 能力;`Tracy_Vulkan.hpp` 只做 Tracy Vulkan 原生 API 的薄入口。Observation 不重新设计 Tracy 的 Zone 类型体系。
|
||||
|
||||
## 依赖与职责
|
||||
|
||||
业务模块不决定观测逻辑。Observation adapter 使用已有 hook、wrapper、observer 或运行时组合截获事实,并决定投影内容。
|
||||
|
||||
Taskflow 的 task duration、worker utilization 等已有 profiler 能力不重复实现。只在需要业务关联时附加 root/scope/correlation 等身份,或补充 Taskflow 原生 profiler 无法提供的项目语义。
|
||||
|
||||
Vulkan 观测优先使用 Tracy Vulkan profiling。CPU submit、GPU execution、fence completion 通过 correlation 身份关联;不使用 CPU 时间戳模拟 GPU Zone。
|
||||
|
||||
同步和内存观测优先使用 Tracy 原生 Lock/Memory 能力。只有项目特有的业务身份、资源归属和连续状态才额外附加。
|
||||
|
||||
业务代码不得直接调用 `trace_frame()`、`trace_vulkan_submission()`、`trace_vulkan_completion()` 或 Tracy 宏。这些入口只供 Observation adapter、wrapper 或已有底层 hook 使用。关闭或替换观测时,业务接口和业务生命周期保持不变。
|
||||
|
||||
## 过滤与分析
|
||||
|
||||
不假设 Tracy 支持按任意自定义 ID 全局过滤所有数据。Identity 的目标是关联和识别,Tracy 的时间范围、线程、Zone、Plot 等能力负责缩小观察范围。
|
||||
|
||||
不能为了只看一个 Frame 而隐藏同时发生的其他 Frame 或后台工作。并发工作可能正是争用、backpressure 或延迟的来源。root 身份用于识别目标工作,不用于破坏真实并发现场。
|
||||
|
||||
稳定 Zone 名称用于 Tracy 聚合统计,不把 frame、submission 等每次变化的 ID 拼进 Zone 名称。动态业务身份通过 Zone Text、Message、Value 等通道表达。
|
||||
|
||||
## 性能约束
|
||||
|
||||
观测不得改变被观测对象的执行语义:
|
||||
|
||||
- Worker 热路径不为汇总增加共享锁。
|
||||
- 不为观测额外制造业务状态机、轮询线程或完成事件。
|
||||
- 能使用已有 hook 就不增加主动上报点。
|
||||
- 汇总和派生指标尽量在已有完成边界计算。
|
||||
- `TRACY_ENABLE` 关闭时,Tracy instrumentation 应在编译期消失,不在业务热路径保留运行时 enable 分支。
|
||||
|
||||
## 扩展原则
|
||||
|
||||
新增观测项时按以下顺序判断:
|
||||
|
||||
1. Tracy 是否已经有对应的一等能力;有则直接使用。
|
||||
2. 该信息应附着在哪个既有 root/scope/correlation 上。
|
||||
3. 只有确实存在新的执行区间时才创建新的 Zone/GPU Zone。
|
||||
4. 只有跨时间线因果关系无法由时间位置表达时才创建 correlation 身份。
|
||||
5. 不因为存在第二种数据就提前抽象 backend;只有出现第二个真实后端并产生重复实现后再抽象。
|
||||
|
||||
## 当前 Render3D 接入规则
|
||||
|
||||
`Frame_3D` 是 Frame 生命周期事实的拥有者。它根据已有 `mark()` 语义点建立 Frame root 和 Vulkan submission correlation;`Render_Scene_3D` 不直接 include Observation/Tracy,也不新增观测参数。
|
||||
|
||||
`scene_render_entered` 投影 Tracy Frame;`gpu_submitted` 增加真实未完成 Vulkan submission 和 GPU frame-in-flight 计数;`gpu_completed` 在已有 fence completion 回调到达时结束同一 correlation,并用 Frame 已有 marker 计算 submit-to-completion wall latency。计数基于真实 submission 生命周期,不从 semaphore 数值差推导。
|
||||
|
||||
`Datoviz_Render_Context` 是共享 VkQueue 的底层 adapter。DRP2 即时上传的 `vkQueueSubmit2` 在这里投影 CPU submit zone,并按其独立 fence 生命周期增减 pending submission;它不继承 Frame root。
|
||||
|
||||
当前 Frame Target 的 primary command buffer 会跨 Frame 复用,因此这一实现不向 retained command buffer 写 `TracyVkZone`/`TracyVkCollect`。GPU render/transition/copy/total 使用 Datoviz 已有 GPU timestamp 结果,并带同一 submission correlation 投影到 Tracy。这样保留真实 GPU 测量,又不为了 profiler 改变 command recording/reuse 行为。
|
||||
|
||||
`Tracy_Vulkan.hpp` 继续保留 Tracy Vulkan 原生薄入口,供以后出现按 submission 可安全录制的 GPU command path 使用;当前实现不额外建立 query pool、轮询线程或第二套 GPU timestamp 系统。
|
||||
|
||||
## Synchronization 接入规则
|
||||
|
||||
同步观测优先使用 Tracy 原生 Lock profiling,不自己累计 lock wait/hold 统计。只有真正的同步原语才进入 Lock timeline;正常业务区间仍使用 CPU Zone,不能把任意慢函数伪装成锁等待。
|
||||
|
||||
当前 Render3D 的 `Scene_Datoviz_State::target_mutex_` 是 Prepare、Submit、Collect、目标槽录制生命周期之间共享的唯一资源门,并且不参与 condition variable。业务声明只使用同步基础设施的 `AETHERA_MUTEX`,不 include Observation/Tracy。`synchronization/Mutex.hpp` 把普通 mutex 语义交给 Observation 的同步投影;开启 Tracy 时底层使用 Tracy Lockable,关闭时编译回 `std::mutex`。所有既有 `std::lock_guard` 调用保持原样,不增加手工 begin/end 上报。
|
||||
|
||||
不得为了接 Tracy Lock profiling 改变同步语义。Timer Service 的 mutex 与 `std::condition_variable` 绑定,因此不能仅为了 profiler 把它替换成 Tracy Lockable 并进一步改成 `condition_variable_any`。Timer Service 的 idle/deadline wait 是设计上的休眠,不应被大量 CPU Zone 当成性能异常;如果以后需要分析 Timer wakeup latency,应在 Timer 自己的调度语义上设计独立指标,而不是把正常睡眠当 contention。
|
||||
|
||||
`observation/Synchronization.hpp` 只提供 Tracy 原生同步原语的编译期薄投影;`synchronization/Mutex.hpp` 是业务可见的普通同步语义门面,不暴露 Observation 名称。业务层不得主动调用 lock wait/hold 上报函数,也不建立第二套锁统计器。锁的 wait、obtain、hold、release 与线程关系由 Tracy Lock profiling 直接记录;业务 root/scope/correlation 只有在未来确实需要把某次锁竞争关联到特定业务操作时再补,不能为了关联破坏 Lockable 的原生时间线。
|
||||
## Memory / Resource 接入规则
|
||||
|
||||
Memory 与 Resource 必须区分。Tracy Memory profiling 只用于存在真实地址和准确大小的 allocation/free 生命周期;不能把 opaque 资源句柄、Datoviz 包装对象或估算大小伪装成 CPU 内存分配。当前项目没有统一 CPU allocator 边界,因此本阶段不增加假的 `TracyAlloc/TracyFree` 接入。以后只有出现统一 allocator、arena 或明确的分配/释放 hook 时才接 Tracy 原生 Memory profiling。
|
||||
|
||||
Render3D 已经通过 `Datoviz_Frame_Observation` 持有真实的逐帧资源事实,包括有效上传字节、读回字节、Buffer/Texture 创建耗时、Shader 编译、Pipeline 创建、staging Vulkan allocation、host copy、上传队列等待、fence 等待和 transfer 等。Observation 不复制完整 Datoviz profiler,只从已有完成边界抽取适合跨帧分析的资源指标,并建立 `Frame_Resource_Context` 作为 Frame 的逻辑子 scope。
|
||||
|
||||
资源指标是已经完成操作的测量结果,因此投影为 Tracy Plot 和带 Frame root/scope/parent 的 Message;不得在完成边界事后创建假的 CPU Zone 来模拟历史执行区间。稳定 Plot 用于跨帧趋势,Message 保存一次有实际资源活动的详细字段。没有资源活动的 Frame 仍写零值 Plot,便于观察 spike 与基线。
|
||||
|
||||
DRP2 即时上传当前仍由共享 VkQueue adapter 作为独立 Vulkan submission 观测。它发生在资源执行内部,但现有 Datoviz callback 没有安全的业务 Context 透传边界;不能为了 correlation 向 Render Scene API 注入 Observation 参数或依赖 thread-local 猜测跨任务归属。Frame resource metrics 与独立 Vulkan submission 时间线共同保留真实现场,后续只有出现明确的 runtime adapter context hook 时再建立直接 correlation。
|
||||
|
||||
显存占用、budget、heap residency 等指标只有从 Vulkan/allocator 的真实统计 API 读取时才允许进入 Observation。禁止用 uploaded bytes 累加推导显存占用,因为资源可能复用、覆盖、释放或由 allocator 重新布局。
|
||||
|
||||
|
||||
## Taskflow 关联接入规则
|
||||
|
||||
Taskflow Worker timeline 使用 Taskflow 原生 Observer 和 Tracy CPU Zone,不复制 task duration、worker utilization 等原生 profiler 能力。Frame 关联通过 `observation/Taskflow.hpp` 投影:每次属于 Frame 的 graph run 从 Frame root 建立新的 correlation scope,Worker Zone 附带同一 `root/scope/parent/correlation`、业务 frame/generation、graph、worker 和 native task id。
|
||||
|
||||
`Render_Scene_3D` 不构造 `Taskflow_Trace_Context`,也不使用 Tracy 宏或 Observation API。`Frame_3D_Access::run_taskflow()` 是 Render3D 唯一的 Taskflow 执行适配点,它根据 Frame 内已有的诊断请求选择普通 run 或 observed run。只有 `TRACY_ENABLE` 且 Tracy 客户端实际连接时才建立 Frame correlation 并传入 Taskflow runtime,避免未连接 profiler 时为纯展示身份生成无用 correlation;`Taskflow_Observation` 的详细诊断请求不依赖 Tracy 连接状态,仍按原有语义独立工作。Tracy 已连接但未请求 `Taskflow_Observation` 时,Worker Zone 仍可识别所属 Frame;请求详细诊断时继续保留原有 trace 结果语义。
|
||||
|
||||
`Taskflow_Trace_Context` 保留原有 frame/generation 字段并追加 Observation identity 字段,既有聚合初始化 `{frame, generation}` 的语义不变。Taskflow runtime 只传递执行事实,不自己解释 Observation identity;具体 Zone Text 的序列化由 `observation/Taskflow.hpp` 决定。
|
||||
|
||||
## Frame 时序总览规则
|
||||
|
||||
Frame 时序总览只从 `Frame_3D` 已经存在的 marker 派生,不新增业务计时点,也不根据结果反向伪造历史 Zone。`frame_ready` 到达时,Observation 计算并投影稳定的跨帧 Plot 和一次带 Frame identity 的 Message:
|
||||
|
||||
- `RenderLatencyNs`:`scene_render_entered -> frame_ready` 的端到端 render 请求墙钟延迟。
|
||||
- `GraphStartLatencyNs`:`scene_render_requested -> scene_render_started` 的 graph 提交到首个 Frame task 开始的墙钟延迟。
|
||||
- `GraphToSubmitNs`:`scene_render_started -> gpu_submitted` 的 graph 启动到 Vulkan 接受 Frame submission 的墙钟延迟;它不是 CPU busy time。
|
||||
- `BackendPrepareNs`:`backend_prepare_started -> backend_prepare_finished` 的后端 Prepare 墙钟时间。
|
||||
- `SubmitPathNs`:`backend_queue_entered -> gpu_submitted` 的后端提交路径墙钟时间。
|
||||
- `CompletionTailNs`:`gpu_completed -> frame_ready` 的完成观察、collect/readback 和 completion task 尾延迟。
|
||||
|
||||
这些指标只用于快速缩小慢帧范围,不自动宣称 CPU-bound、GPU-bound 或锁竞争根因。GPU 实际执行时间、资源耗时、Lock timeline、Taskflow Worker timeline 仍由各自原生/专用投影提供;归因必须结合真实并发现场分析。
|
||||
|
||||
## GPU Completion Service 接入规则
|
||||
|
||||
`Gpu_Completion_Service` 是进程级 Fence 完成基础设施,不属于单个 Frame 的逻辑子树。它已经拥有准确的准入容量、`in_flight`、Timer poll 活跃集合、Fence watch 时间和终态错误,因此 Observation 直接在这个基础设施边界投影服务级 backpressure,不从 Frame 侧重复推导第二套完成队列状态。
|
||||
|
||||
`Aethera.GPUCompletion.InFlight` 使用 Service 自己的原子计数,只有准入成功和真正回收 reservation 时变化。`Aethera.GPUCompletion.Active` 使用 Timer poll 线程独占的 `active` 集合大小,只在新记录进入 active 或记录被移除时更新;禁止每 1ms poll 都重复写 Plot,也不为 poll 建高频 CPU Zone。正常 `VK_NOT_READY` 是 Fence 轮询协议的一部分,不作为错误事件。
|
||||
|
||||
准入达到固定容量时记录累计 `Aethera.GPUCompletion.CapacityExhausted` 并发送一次服务级 Message。Fence 进入终态时记录 `Aethera.GPUCompletion.WaitNs`;这个时间只在 Tracy 实际连接时从既有 `watched_at` 计算,不改变 `Gpu_Completion_Service::Result::wait_duration_ns` 原有的 `observe` 语义。`fence_abandoned` 和 Vulkan failure 只作为异常 Message 和累计 `Aethera.GPUCompletion.Errors` 投影,正常完成不额外发送逐 Fence Message,避免与已有 Frame/Vulkan completion correlation 重复。
|
||||
|
||||
Service 级指标不建立 Frame correlation。一个 completion reservation 可能来自不同调用方,当前 Service API 没有业务 Context 透传;不得使用 thread-local 或调用栈猜测 Frame 身份。Frame 自己的 `gpu_submitted -> gpu_completed` correlation 继续负责单 Frame 生命周期,GPU Completion Service 只回答全局容量、积压和完成等待是否异常。
|
||||
|
||||
## Frame Attention Snapshot 规则
|
||||
|
||||
Frame Attention 不是自动根因分类器。它只在 `frame_ready` 已经成立后,把当前 Frame 已经存在的观测事实汇成一条结构化 Tracy Message,便于按 `frame/root` 搜索后先看到总览,再进入各自原生时间线分析。
|
||||
|
||||
Snapshot 只消费已有结果,不新增业务计时点,也不事后伪造 Zone。当前汇总 Frame timing、Datoviz GPU total/render timestamp、关键 Resource activity、Vulkan pending/frames-in-flight,以及 GPU Completion Service 的 in-flight/active/capacity/累计异常状态。Taskflow duration/utilization 和 Lock wait/hold 不复制进 Snapshot;它们继续由 Taskflow/Tracy Worker Zone 和 Tracy Lock profiling 原生展示。
|
||||
|
||||
Frame Attention 不使用固定毫秒阈值、滑动均值或经验规则自动标记 CPU-bound、GPU-bound、resource-bound。Frame budget 如果以后成为 Frame runtime 的真实业务事实,可以作为明确输入再增加预算比较;在没有真实 budget 之前,不允许 Observation 自己发明阈值。
|
||||
|
||||
`Vulkan_State` 和 `GPU_Completion_State` 只是 Observation 为同一时刻总览保留的只读快照,不替代 Vulkan submission 生命周期和 `Gpu_Completion_Service` 自己的状态。GPU Completion 的镜像状态只在 Tracy 实际连接时更新,因为它只服务 Tracy 展示;不得被业务逻辑读取或参与调度判断。
|
||||
## Frame Attention Index
|
||||
|
||||
`Frame_Attention` 保留完整的 Frame 事实快照;`Frame_Attention_Index` 只提供可搜索的事实标签,不做性能根因判断。
|
||||
|
||||
当前标签来自同一 Snapshot 的客观状态:
|
||||
|
||||
- `resource_activity`:该 Frame 存在上传、回读、shader/pipeline build 等资源活动。
|
||||
- `upload_wait`:该 Frame 的资源上传发生 queue wait 或 fence wait。
|
||||
- `pipeline_build`:该 Frame 发生 shader compile 或 pipeline create。
|
||||
- `frame_overlap`:Snapshot 时刻存在超过一个 Frame submission in flight。
|
||||
- `non_frame_submission`:Vulkan pending submission 数大于 Frame in-flight 数,说明同时存在独立于 Frame 的 submission。
|
||||
- `completion_queue_gap`:GPU completion service 的 in-flight 数大于 active poll 数。
|
||||
- `completion_saturated`:GPU completion service 的 in-flight 达到当前 capacity。
|
||||
|
||||
这些标签只用于 Tracy 文本筛选和定位。`frame_overlap=true` 不代表 overlap 本身有问题,`upload_wait=true` 也不代表 upload 是当前 Frame 的根因。定位到 Frame 后仍然回到对应的 Taskflow、Lock、GPU、Vulkan、Resource 原生时间线分析。
|
||||
|
||||
完整 Snapshot 与 Index 分开发送,避免继续扩大 `frame_attention` Message 并撞到 Tracy 文本缓冲上限。没有任何标签时不发送 Index Message。
|
||||
|
||||
|
||||
## Frame Policy / CPU admission 接入规则
|
||||
|
||||
Frame Policy 的调度状态是 CPU 进入 Scene/GPU 之前的真实 backpressure 来源,不能从 Vulkan pending、GPU completion 或 Frame timing 反推。当前 `Throttled_Latest_only` 已经维护 `timer_ticks`、`dropped_timer_ticks`、`completed_frames`、render/send/end-to-end 滑动统计和固定三槽 `idle/rendering/ready/sending` 状态,因此 Observation 只投影这些已有事实,不建立第二套 Frame Policy 统计。
|
||||
|
||||
接入点只使用 `Throttled_Latest_only::Private::publish_state()` 这个既有状态出口。它在发布当前 State 前读取同一受 mutex 保护的权威状态和槽位 phase,并交给 `observation/Frame_Policy.hpp` 决定 Tracy Plot;`frame_due()`、Scene、Sink、Timer callback 不新增 Observation 参数或 Tracy 宏。Observation 不参与 slot 选择、tick 丢弃、限速计算和 State 发布。
|
||||
|
||||
`DroppedTimerTicks` 表示策略真实丢弃的 timer tick。当前 latest-only 策略在已有 `rendering` slot 时不会并发启动第二次 Scene render,因此 dropped tick 可以和 `RenderingSlots/ReadySlots/SendingSlots/IdleSlots` 一起判断 CPU admission 是否被策略本身限住。不得把 dropped tick 自动解释成 GPU 慢;同时发生的 Sink send、Frame ready backlog、Taskflow、Lock、Vulkan/GPU 时间线必须共同分析。
|
||||
|
||||
`EffectiveFPS`、`RenderCapacityFPS`、`SendCapacityFPS` 直接使用 Frame Policy 已有派生值;没有 capacity 时投影 0。render/send/end-to-end 的 average/p95/stddev 只在对应 `Statistics_Summary::sample_count != 0` 时投影,Observation 不自行建立滑动窗口。Plot 名称保持稳定,不编码 policy instance 或 frame id;当前数据表达进程中该 Frame Policy 调度域的全局趋势,不与单 Frame root 伪造 parent/correlation。
|
||||
|
||||
## Frame Policy 与 Frame Attention 关联规则
|
||||
|
||||
Frame Policy 的趋势 Plot 是调度域级事实,Frame Attention 是单 Frame 事实;两者不能通过伪造 parent/correlation 直接合成同一执行树。为了从单 Frame 快速看到其完成时刻对应的 CPU admission 状态,Observation 只维护一份最小 Frame Policy 状态镜像,并在 `frame_ready` 的 Attention 投影旁发送 `event=frame_policy_context`。
|
||||
|
||||
状态镜像只保存 `timer_ticks`、`dropped_timer_ticks`、`completed_frames` 和三槽 `idle/rendering/ready/sending` 数量,并使用单调 `policy_revision` 标识最近一次 `publish_state()`。Effective FPS、capacity、average/P95/stddev 继续只使用 Frame Policy 自己的稳定 Plot,不复制进单 Frame Message。
|
||||
|
||||
`frame_policy_context` 带当前 Frame 的 `root/frame/generation`,因此它只是“该 Frame 完成时最近一次已发布 policy 状态”的上下文快照,不宣称该状态由这个 Frame 独占,也不把累计 `dropped_timer_ticks` 自动解释为当前 Frame 丢 tick。跨线程状态通过 Observation 内部原子镜像读取;revision 以奇偶序列保护一致性,Frame 侧遇到正在发布或前后 revision 不一致时直接跳过本次 Context,不自旋等待。镜像只在 Tracy 实际连接时更新,不被业务逻辑读取,不参与 slot 选择、限速或 State 发布。
|
||||
|
||||
## Observation 数据质量与自身开销规则
|
||||
|
||||
Observation 输出本身也必须可验证,不能在 Tracy 文本缓冲达到上限时静默产生半截字段。`Tracy_Text_Sink` 的字段写入以完整 `name=value` 为最小单位;单个字段无法完整放入固定缓冲时整字段省略,已经完整写入的其他字段保持有效,禁止把字段名、数值或业务 identity 截断在中间。
|
||||
|
||||
文本容量仍固定为 1024 bytes,不因为新增指标不断扩大热路径栈对象。Sink 同时记录完整序列化所需字节数;只有实际发生字段省略时才累计 `Aethera.Observation.TextTruncations`、记录 `Aethera.Observation.TruncatedRequiredBytes`,并直接发送一条短 `event=observation_text_truncated` Message,标明 `message`、`zone` 或 `task_zone` channel。该健康事件不得再经过普通 Message 投影路径递归生成自身。
|
||||
|
||||
正常未截断路径不增加额外 Tracy Message/Plot,不建立采样线程、后台队列或周期性 self-profiler。Observation health 只报告真实的数据质量失败,不以“输出条数”“生成 identity 数”等正常行为制造额外指标。发生截断后原事件仍保留所有已经完整写入且能容纳的字段;健康事件只说明有字段被省略,不能把截断自动解释为业务性能异常。
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
#include "observation/Synchronization.hpp"
|
||||
#define AETHERA_MUTEX(varname, description) AETHERA_OBSERVATION_MUTEX(varname, description)
|
||||
@@ -25,24 +25,12 @@ struct Taskflow_Task_Trace {
|
||||
double finished_ms{}; /* 相对本次执行起点的任务体结束时间。 */
|
||||
double completed_ms{}; /* 相对本次执行起点的 Observer on_exit 完成时间。 */
|
||||
};
|
||||
struct Taskflow_Execution_Summary {
|
||||
std::size_t task_count{}; /* 本次实际执行的 Task 实例数。 */
|
||||
std::uint64_t max_execution_native_id{}; /* 任务体耗时最大的 Task 原生身份;无任务时为 0。 */
|
||||
std::uint64_t max_wait_native_id{}; /* 调度等待最大的 Task 原生身份;无任务时为 0。 */
|
||||
double duration_ms{}; /* 从本次观察起点到 Executor completion 的墙钟时长。 */
|
||||
double task_execution_ms{}; /* 所有任务体耗时之和;并行和 Module 嵌套会重复计入墙钟时间。 */
|
||||
double task_wait_ms{}; /* 所有 Task 调度等待之和;并行等待会重复计入墙钟时间。 */
|
||||
double max_execution_ms{}; /* 单次 Task 任务体最大耗时。 */
|
||||
double max_wait_ms{}; /* 单次 Task 最大调度等待。 */
|
||||
double wait_to_duration_ratio{}; /* task_wait_ms / duration_ms,可因并行等待大于 1。 */
|
||||
};
|
||||
struct Taskflow_Execution_Trace {
|
||||
std::string stage{}; /* 调用方提供的本次执行业务阶段。 */
|
||||
std::string taskflow_name{}; /* Taskflow 原生图名称。 */
|
||||
std::uint64_t started_time_unix_ns{}; /* 本次执行开始的 Unix 时间,单位为纳秒。 */
|
||||
double executor_finished_ms{}; /* Taskflow completion 进入时间。 */
|
||||
double observation_finished_ms{}; /* Observer 数据完成并可消费的时间。 */
|
||||
Taskflow_Execution_Summary summary{}; /* 本次执行完成后生成的 Graph 级汇总。 */
|
||||
std::vector<Taskflow_Node_Trace> nodes{}; /* 本次执行的静态 DAG 元信息。 */
|
||||
std::vector<std::vector<Taskflow_Task_Trace>> worker_tasks{}; /* 外层下标即 Worker ID。 */
|
||||
};
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#include "../export/Taskflow_Observation.hpp"
|
||||
#include "detail/Taskflow_Execution.ipp"
|
||||
#include "Error_handling_specification/Failure_Policy.hpp"
|
||||
#ifdef TRACY_ENABLE
|
||||
#include "observation/Tracy.hpp"
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -22,48 +19,6 @@ enum struct Observation_Phase : std::uint8_t {
|
||||
ready,
|
||||
consumed
|
||||
};
|
||||
#ifdef TRACY_ENABLE
|
||||
struct Taskflow_Summary_Trace_Tags {
|
||||
std::string_view stage{};
|
||||
std::string_view graph{};
|
||||
std::size_t task_count{};
|
||||
double duration_ms{};
|
||||
double task_execution_ms{};
|
||||
double task_wait_ms{};
|
||||
std::uint64_t max_execution_task{};
|
||||
double max_execution_ms{};
|
||||
std::uint64_t max_wait_task{};
|
||||
double max_wait_ms{};
|
||||
double wait_to_duration_ratio{};
|
||||
};
|
||||
void publish_taskflow_summary(const Taskflow_Execution_Trace& trace) noexcept {
|
||||
if (!observation::tracy_connected()) return;
|
||||
const auto& summary = trace.summary;
|
||||
TracyCPlot("Aethera.Taskflow.DurationMs", summary.duration_ms)
|
||||
TracyCPlot("Aethera.Taskflow.TaskExecutionMs", summary.task_execution_ms)
|
||||
TracyCPlot("Aethera.Taskflow.TaskWaitMs", summary.task_wait_ms)
|
||||
TracyCPlot("Aethera.Taskflow.MaxExecutionMs", summary.max_execution_ms)
|
||||
TracyCPlot("Aethera.Taskflow.MaxWaitMs", summary.max_wait_ms)
|
||||
TracyCPlot("Aethera.Taskflow.WaitToDurationRatio", summary.wait_to_duration_ratio)
|
||||
TracyCPlotI("Aethera.Taskflow.TaskCount", static_cast<std::int64_t>(summary.task_count))
|
||||
TracyCZoneN(context, "Taskflow.Summary", 1)
|
||||
observation::Tracy_Text_Sink sink;
|
||||
observation::serialize(sink, Taskflow_Summary_Trace_Tags{
|
||||
trace.stage,
|
||||
trace.taskflow_name,
|
||||
summary.task_count,
|
||||
summary.duration_ms,
|
||||
summary.task_execution_ms,
|
||||
summary.task_wait_ms,
|
||||
summary.max_execution_native_id,
|
||||
summary.max_execution_ms,
|
||||
summary.max_wait_native_id,
|
||||
summary.max_wait_ms,
|
||||
summary.wait_to_duration_ratio});
|
||||
if (!sink.empty()) TracyCZoneText(context, sink.data(), sink.size())
|
||||
TracyCZoneEnd(context)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
struct Taskflow_Observation::Private {
|
||||
struct Start_Record {
|
||||
@@ -149,29 +104,6 @@ struct Taskflow_Observation::Private {
|
||||
latest_entered[task.native_id] = Timeline_Point{task.entered_ms, event.task};
|
||||
}
|
||||
}
|
||||
void resolve_summary() noexcept {
|
||||
auto& summary = trace.summary;
|
||||
summary = {};
|
||||
summary.duration_ms = trace.executor_finished_ms;
|
||||
for (const auto& tasks : trace.worker_tasks) {
|
||||
for (const auto& task : tasks) {
|
||||
const auto first_task = summary.task_count == 0;
|
||||
const auto execution_ms = task.finished_ms - task.started_ms;
|
||||
++summary.task_count;
|
||||
summary.task_execution_ms += execution_ms;
|
||||
summary.task_wait_ms += task.wait_ms;
|
||||
if (first_task || execution_ms > summary.max_execution_ms) {
|
||||
summary.max_execution_ms = execution_ms;
|
||||
summary.max_execution_native_id = task.native_id;
|
||||
}
|
||||
if (first_task || task.wait_ms > summary.max_wait_ms) {
|
||||
summary.max_wait_ms = task.wait_ms;
|
||||
summary.max_wait_native_id = task.native_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (summary.duration_ms != 0.0) summary.wait_to_duration_ratio = summary.task_wait_ms / summary.duration_ms;
|
||||
}
|
||||
void record_failure(std::exception_ptr value) noexcept {
|
||||
if (failure.load(std::memory_order_acquire)) return;
|
||||
auto next = std::make_shared<std::exception_ptr>(std::move(value));
|
||||
@@ -373,17 +305,13 @@ void detail::Taskflow_Observation_Execution::finish(
|
||||
Clock::time_point executor_finished) noexcept {
|
||||
if (!observation) return;
|
||||
auto& data = *observation;
|
||||
data.trace.executor_finished_ms = data.elapsed_ms(executor_finished);
|
||||
try {
|
||||
data.resolve_task_waits();
|
||||
data.resolve_summary();
|
||||
#ifdef TRACY_ENABLE
|
||||
publish_taskflow_summary(data.trace);
|
||||
#endif
|
||||
}
|
||||
catch (...) {
|
||||
data.record_failure(std::current_exception());
|
||||
}
|
||||
data.trace.executor_finished_ms = data.elapsed_ms(executor_finished);
|
||||
data.trace.observation_finished_ms = data.elapsed_ms(Clock::now());
|
||||
data.phase.store(Observation_Phase::ready, std::memory_order_release);
|
||||
}
|
||||
|
||||
@@ -80,37 +80,6 @@ TEST_F(Taskflow_Observation_Test, Explicit_Request_Records_And_Consumes_One_Trac
|
||||
EXPECT_DOUBLE_EQ(task.ready_ms, 0.0);
|
||||
EXPECT_DOUBLE_EQ(task.wait_ms, task.entered_ms);
|
||||
EXPECT_GE(task.completed_ms, task.finished_ms);
|
||||
EXPECT_EQ(trace->summary.task_count, 1);
|
||||
EXPECT_EQ(trace->summary.max_execution_native_id, native_id);
|
||||
EXPECT_EQ(trace->summary.max_wait_native_id, native_id);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.duration_ms, trace->executor_finished_ms);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.task_execution_ms, task.finished_ms - task.started_ms);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.task_wait_ms, task.wait_ms);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.max_execution_ms, task.finished_ms - task.started_ms);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.max_wait_ms, task.wait_ms);
|
||||
ASSERT_GT(trace->summary.duration_ms, 0.0);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.wait_to_duration_ratio, task.wait_ms / trace->summary.duration_ms);
|
||||
}
|
||||
TEST_F(Taskflow_Observation_Test, Empty_Execution_Produces_Zero_Task_Summary) {
|
||||
nodes.clear();
|
||||
auto execution = aethera::detail::Taskflow_Observation_Execution::try_start(
|
||||
observation,
|
||||
1,
|
||||
"test.graph",
|
||||
nodes);
|
||||
ASSERT_TRUE(execution.has_value());
|
||||
execution->finish(std::chrono::steady_clock::now());
|
||||
auto trace = observation.take();
|
||||
ASSERT_TRUE(trace.has_value());
|
||||
EXPECT_EQ(trace->summary.task_count, 0);
|
||||
EXPECT_EQ(trace->summary.max_execution_native_id, 0);
|
||||
EXPECT_EQ(trace->summary.max_wait_native_id, 0);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.duration_ms, trace->executor_finished_ms);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.task_execution_ms, 0.0);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.task_wait_ms, 0.0);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.max_execution_ms, 0.0);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.max_wait_ms, 0.0);
|
||||
EXPECT_DOUBLE_EQ(trace->summary.wait_to_duration_ratio, 0.0);
|
||||
}
|
||||
TEST_F(Taskflow_Observation_Test, Wait_Starts_When_The_Last_Predecessor_Completes) {
|
||||
constexpr std::uint64_t successor_native_id{23};
|
||||
|
||||
Reference in New Issue
Block a user