#pragma once #include #include #include #include #include #include #include #include #include #include #include namespace renderive::web { namespace gallery_capture_detail { using Json = nlohmann::json; inline const char* node_kind(Render_Node_Kind kind) noexcept { switch (kind) { case Render_Node_Kind::prepare: return "prepare"; case Render_Node_Kind::paint: return "paint"; case Render_Node_Kind::composite: return "composite"; case Render_Node_Kind::render: return "render"; } return "prepare"; } inline const char* execution_status(Node_Execution_Status status) noexcept { switch (status) { case Node_Execution_Status::pending: return "pending"; case Node_Execution_Status::ready: return "ready"; case Node_Execution_Status::running: return "running"; case Node_Execution_Status::waiting_external: return "waiting_external"; case Node_Execution_Status::complete: return "complete"; case Node_Execution_Status::failed: return "failed"; case Node_Execution_Status::cancelled: return "cancelled"; } return "pending"; } inline Json metrics_json(const Node_Execution_Metrics& metrics) { Json result = Json::object(); constexpr std::array names{ "input_count", "chunk_size", "prepared_cells", "pixel_count", "primitive_count", "queue_wait_ns", "apply_duration_ns", "plan_emit_duration_ns", "backend_execute_duration_ns", "submit_duration_ns", "gpu_fence_wait_ns", "gpu_render_duration_ns", "gpu_transition_duration_ns", "gpu_copy_duration_ns", "gpu_total_duration_ns", "readback_duration_ns", "backend_resource_version", "backend_frame_index", "backend_artifact_status", "backend_validation_code", "backend_validation_command", "backend_artifact_json_bytes"}; static_assert(names.size() == static_cast(Node_Metric_Kind::count)); for (std::size_t index = 0; index < names.size(); ++index) { const auto kind = static_cast(index); if (metrics.contains(kind)) result[names[index]] = metrics.get(kind); } return result; } inline std::unordered_map owner_names( const Scene_Base& scene) { std::unordered_map result; const auto topology = scene.topology_snapshot(); result.reserve(topology.renderables.size()); for (const auto& base : topology.renderables) { std::string name = base->object_name(); if (name.empty()) name = "Renderable " + std::to_string(base->renderable_id()); result.emplace(base->renderable_id(), std::move(name)); } return result; } inline Json plan_json(const Scene_Base& scene, const Render_Plan& plan) { const auto names = owner_names(scene); Json nodes = Json::array(); Json edges = Json::array(); std::map> active_stages; std::set owners; for (const auto& node : plan.graph.nodes) { const std::string owner = node.owner_id == 0 ? "Scene" : names.contains(node.owner_id) ? names.at(node.owner_id) : "Renderable " + std::to_string(node.owner_id); nodes.push_back({ {"id", std::to_string(node.node_id)}, {"node_id", node.node_id}, {"owner_id", node.owner_id}, {"owner", owner}, {"label", owner + " / " + node.name}, {"name", node.name}, {"kind", node_kind(node.kind)}, {"execution_index", node.execution_index} }); if (node.owner_id != 0) { owners.insert(node.owner_id); auto& stages = active_stages[node.owner_id]; stages.first = stages.first || node.kind == Render_Node_Kind::prepare; stages.second = stages.second || node.kind == Render_Node_Kind::paint; } } for (const auto& edge : plan.graph.edges) { edges.push_back({{"from", std::to_string(edge.from)}, {"to", std::to_string(edge.to)}, {"kind", "dependency"}}); } Json renderables = Json::array(); for (const std::uint64_t owner_id : owners) { const auto stages = active_stages.find(owner_id); const bool has_prepare = stages != active_stages.end() && stages->second.first; const bool has_paint = stages != active_stages.end() && stages->second.second; renderables.push_back({ {"owner_id", owner_id}, {"name", names.contains(owner_id) ? names.at(owner_id) : "Renderable " + std::to_string(owner_id)}, {"prepare_cache", has_prepare ? "MISS" : "HIT"}, {"paint_cache", has_paint ? "MISS" : "HIT"}, {"prepare_in_plan", has_prepare}, {"paint_in_plan", has_paint} }); } return {{"version", plan.version}, {"topology_id", "plan-" + std::to_string(plan.version)}, {"nodes", std::move(nodes)}, {"edges", std::move(edges)}, {"renderables", std::move(renderables)}}; } inline Json execution_json(const Node_Execution& execution, const Frame_Snapshot& frame) { Json attachments = Json::array(); for (const auto& attachment : execution.attachments) { attachments.push_back({{"type", attachment.type}, {"content", attachment.content}}); } return { {"node_id", execution.node_id}, {"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 ? execution.cpu_end_time_ns - frame.render_start_ns : 0}, {"external_start_offset_ns", execution.external_start_time_ns >= frame.render_start_ns ? execution.external_start_time_ns - frame.render_start_ns : 0}, {"external_end_offset_ns", execution.external_end_time_ns >= frame.render_start_ns ? execution.external_end_time_ns - frame.render_start_ns : 0}, {"end_offset_ns", execution.end_time_ns >= frame.render_start_ns ? execution.end_time_ns - frame.render_start_ns : 0}, {"duration_ns", execution.duration_ns()}, {"cpu_duration_ns", execution.cpu_duration_ns()}, {"external_duration_ns", execution.external_duration_ns()}, {"worker_id", execution.worker_id}, {"status", execution_status(execution.status)}, {"metrics", metrics_json(execution.metrics)}, {"attachments", std::move(attachments)} }; } inline Json node_analysis_json(const Node_Frame_Analysis& node) { return { {"node_id", node.node_id}, {"duration_ns", node.duration_ns}, {"cpu_duration_ns", node.cpu_duration_ns}, {"render_domain_cpu_duration_ns", node.render_domain_cpu_duration_ns}, {"total_cpu_work_duration_ns", node.total_cpu_work_duration_ns}, {"external_duration_ns", node.external_duration_ns}, {"render_domain_queue_wait_ns", node.render_domain_queue_wait_ns}, {"gpu_completion_wait_ns", node.gpu_completion_wait_ns}, {"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_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}, {"on_critical_path", node.on_critical_path} }; } inline Json frame_json(const Captured_Frame& frame) { Json executions = Json::array(); for (const auto& execution : frame.snapshot->node_executions) executions.push_back(execution_json(execution, *frame.snapshot)); Json nodes = Json::array(); for (const auto& node : frame.analysis.nodes) nodes.push_back(node_analysis_json(node)); Json workers = Json::array(); for (const auto& worker : frame.analysis.workers) { workers.push_back({{"worker_id", worker.worker_id}, {"work_duration_ns", worker.work_duration_ns}, {"utilization", worker.utilization}}); } return { {"frame_id", frame.snapshot->frame_id}, {"render_plan_version", frame.snapshot->render_plan_version}, {"render_duration_ns", frame.snapshot->render_duration_ns()}, {"node_executions", std::move(executions)}, {"analysis", { {"total_render_duration_ns", frame.analysis.total_render_duration_ns}, {"critical_path_duration_ns", frame.analysis.critical_path_duration_ns}, {"total_work_duration_ns", frame.analysis.total_work_duration_ns}, {"scheduler_work_duration_ns", frame.analysis.scheduler_work_duration_ns}, {"render_domain_work_duration_ns", frame.analysis.render_domain_work_duration_ns}, {"total_external_duration_ns", frame.analysis.total_external_duration_ns}, {"total_render_domain_queue_wait_ns", frame.analysis.total_render_domain_queue_wait_ns}, {"total_gpu_completion_wait_ns", frame.analysis.total_gpu_completion_wait_ns}, {"total_gpu_execution_duration_ns", frame.analysis.total_gpu_execution_duration_ns}, {"scheduler_parallel_overlap_ns", frame.analysis.scheduler_parallel_overlap_ns}, {"scheduler_peak_parallelism", frame.analysis.scheduler_peak_parallelism}, {"average_cpu_concurrency", frame.analysis.average_cpu_concurrency}, {"scheduler_average_parallelism", frame.analysis.scheduler_average_parallelism}, {"render_domain_utilization", frame.analysis.render_domain_utilization}, {"critical_path", frame.analysis.critical_path}, {"bottleneck_nodes", frame.analysis.bottleneck_nodes}, {"nodes", std::move(nodes)}, {"workers", std::move(workers)} }} }; } inline Json node_statistics_json(const Node_Statistics& statistics) { return { {"node_id", statistics.node_id}, {"execution_count", statistics.execution_count}, {"average_ns", statistics.average_ns}, {"moving_average_ns", statistics.moving_average_ns}, {"p50_ns", statistics.p50_ns}, {"p95_ns", statistics.p95_ns}, {"p99_ns", statistics.p99_ns}, {"minimum_ns", statistics.minimum_ns}, {"maximum_ns", statistics.maximum_ns}, {"average_cpu_ns", statistics.average_cpu_ns}, {"average_render_domain_cpu_ns", statistics.average_render_domain_cpu_ns}, {"average_total_cpu_work_ns", statistics.average_total_cpu_work_ns}, {"average_external_ns", statistics.average_external_ns}, {"average_render_domain_queue_wait_ns", statistics.average_render_domain_queue_wait_ns}, {"average_gpu_completion_wait_ns", statistics.average_gpu_completion_wait_ns}, {"average_gpu_execution_ns", statistics.average_gpu_execution_ns}, {"p95_cpu_ns", statistics.p95_cpu_ns}, {"p95_render_domain_cpu_ns", statistics.p95_render_domain_cpu_ns}, {"p95_external_ns", statistics.p95_external_ns}, {"p95_gpu_execution_ns", statistics.p95_gpu_execution_ns}, {"critical_path_frequency", statistics.critical_path_frequency}, {"average_scheduler_wait_ns", statistics.average_scheduler_wait_ns} }; } inline Json plan_statistics_json(const Plan_Version_Statistics& statistics) { Json nodes = Json::array(); for (const auto& node : statistics.nodes) nodes.push_back(node_statistics_json(node)); return { {"render_plan_version", statistics.render_plan_version}, {"frame_count", statistics.frame_count}, {"render_average_ns", statistics.render_average_ns}, {"render_p50_ns", statistics.render_p50_ns}, {"render_p95_ns", statistics.render_p95_ns}, {"render_maximum_ns", statistics.render_maximum_ns}, {"average_cpu_concurrency", statistics.average_cpu_concurrency}, {"scheduler_average_parallelism", statistics.scheduler_average_parallelism}, {"render_domain_utilization", statistics.render_domain_utilization}, {"scheduler_peak_parallelism", statistics.scheduler_peak_parallelism}, {"scheduler_wait_average_ns", statistics.scheduler_wait_average_ns}, {"scheduler_wait_p95_ns", statistics.scheduler_wait_p95_ns}, {"nodes", std::move(nodes)} }; } inline double percentile(std::vector values, double quantile) { if (values.empty()) return 0.0; std::sort(values.begin(), values.end()); const double position = quantile * static_cast(values.size() - 1); const auto lower = static_cast(std::floor(position)); const auto upper = static_cast(std::ceil(position)); const double fraction = position - static_cast(lower); return static_cast(values[lower]) * (1.0 - fraction) + static_cast(values[upper]) * fraction; } inline Json session_summary_json(const Capture_Session& session) { std::vector durations; std::vector waits; std::map critical_frequency; double cpu_concurrency{}; double scheduler_parallelism{}; double render_domain_utilization{}; std::size_t scheduler_peak{}; for (const auto& frame : session.frames) { durations.push_back(frame.analysis.total_render_duration_ns); cpu_concurrency += frame.analysis.average_cpu_concurrency; scheduler_parallelism += frame.analysis.scheduler_average_parallelism; render_domain_utilization += frame.analysis.render_domain_utilization; scheduler_peak = std::max( scheduler_peak, frame.analysis.scheduler_peak_parallelism); for (const auto& node : frame.analysis.nodes) waits.push_back(node.scheduler_wait_ns); for (Render_Node_Id node_id : frame.analysis.critical_path) ++critical_frequency[node_id]; } const double duration_sum = std::accumulate( durations.begin(), durations.end(), 0.0); const double wait_sum = std::accumulate(waits.begin(), waits.end(), 0.0); Json critical = Json::array(); for (const auto& [node_id, frequency] : critical_frequency) critical.push_back({{"node_id", node_id}, {"frequency", frequency}}); return { {"render_average_ns", durations.empty() ? 0.0 : duration_sum / durations.size()}, {"render_p50_ns", percentile(durations, 0.50)}, {"render_p95_ns", percentile(durations, 0.95)}, {"render_maximum_ns", durations.empty() ? 0 : *std::max_element(durations.begin(), durations.end())}, {"average_cpu_concurrency", session.frames.empty() ? 0.0 : cpu_concurrency / session.frames.size()}, {"scheduler_average_parallelism", session.frames.empty() ? 0.0 : scheduler_parallelism / session.frames.size()}, {"render_domain_utilization", session.frames.empty() ? 0.0 : render_domain_utilization / session.frames.size()}, {"scheduler_peak_parallelism", scheduler_peak}, {"scheduler_wait_average_ns", waits.empty() ? 0.0 : wait_sum / waits.size()}, {"scheduler_wait_p95_ns", percentile(waits, 0.95)}, {"critical_path_frequency", std::move(critical)} }; } inline Json plan_difference_json(const Render_Plan_Difference& difference) { Json added_edges = Json::array(); Json removed_edges = Json::array(); for (const auto& edge : difference.added_edges) added_edges.push_back({{"from", edge.from}, {"to", edge.to}}); for (const auto& edge : difference.removed_edges) removed_edges.push_back({{"from", edge.from}, {"to", edge.to}}); return {{"added_nodes", difference.added_nodes}, {"removed_nodes", difference.removed_nodes}, {"added_edges", std::move(added_edges)}, {"removed_edges", std::move(removed_edges)}}; } } // namespace gallery_capture_detail inline nlohmann::json gallery_render_plan_json(const Scene_Base& scene) { const auto plan = scene.render_plan_snapshot(); return plan ? gallery_capture_detail::plan_json(scene, *plan) : nlohmann::json{{"version", 0}, {"nodes", nlohmann::json::array()}, {"edges", nlohmann::json::array()}, {"renderables", nlohmann::json::array()}}; } inline nlohmann::json gallery_performance_capture_json(const Scene_Base& scene) { using namespace gallery_capture_detail; const auto controller = scene.capture_state(); Json sessions = Json::array(); std::map> referenced_plans; for (Capture_Session_Id id : scene.capture_sessions()) { const auto session = scene.capture_session(id); if (!session) continue; Json frames = Json::array(); for (const auto& frame : session->frames) { frames.push_back(frame_json(frame)); referenced_plans[frame.snapshot->render_plan_version] = frame.render_plan; } Json node_statistics = Json::array(); for (const auto& node : scene.node_statistics(id)) node_statistics.push_back(node_statistics_json(node)); Json plan_statistics = Json::array(); for (const auto& plan : scene.plan_version_statistics(id)) plan_statistics.push_back(plan_statistics_json(plan)); sessions.push_back({ {"session_id", session->session_id}, {"requested_count", session->requested_count}, {"captured_count", session->captured_count()}, {"active", session->active()}, {"frames", std::move(frames)}, {"summary", session_summary_json(*session)}, {"node_statistics", std::move(node_statistics)}, {"plan_statistics", std::move(plan_statistics)} }); } if (const auto current = scene.render_plan_snapshot()) referenced_plans[current->version] = current; Json plans = Json::array(); std::shared_ptr previous; for (const auto& [version, captured_plan] : referenced_plans) { auto plan = captured_plan ? captured_plan : scene.find_render_plan(version); if (!plan) continue; Json value = plan_json(scene, *plan); value["difference"] = previous ? plan_difference_json(compare_render_plans(*previous, *plan)) : Json{{"added_nodes", Json::array()}, {"removed_nodes", Json::array()}, {"added_edges", Json::array()}, {"removed_edges", Json::array()}}; plans.push_back(std::move(value)); previous = plan; } return { {"controller", { {"enabled", controller.enabled()}, {"session_id", controller.session_id}, {"remaining_frame_count", controller.remaining_frame_count} }}, {"sessions", std::move(sessions)}, {"plans", std::move(plans)} }; } } // namespace renderive::web