441 lines
19 KiB
C++
441 lines
19 KiB
C++
#pragma once
|
|
|
|
#include <renderive/scene/base/Scene_Base.hpp>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <map>
|
|
#include <numeric>
|
|
#include <set>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
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<std::size_t>(Node_Metric_Kind::count));
|
|
for (std::size_t index = 0; index < names.size(); ++index) {
|
|
const auto kind = static_cast<Node_Metric_Kind>(index);
|
|
if (metrics.contains(kind))
|
|
result[names[index]] = metrics.get(kind);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
inline std::unordered_map<std::uint64_t, std::string> owner_names(
|
|
const Scene_Base& scene) {
|
|
std::unordered_map<std::uint64_t, std::string> 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<std::uint64_t, std::pair<bool, bool>> active_stages;
|
|
std::set<std::uint64_t> 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<std::uint64_t> values, double quantile) {
|
|
if (values.empty())
|
|
return 0.0;
|
|
std::sort(values.begin(), values.end());
|
|
const double position = quantile * static_cast<double>(values.size() - 1);
|
|
const auto lower = static_cast<std::size_t>(std::floor(position));
|
|
const auto upper = static_cast<std::size_t>(std::ceil(position));
|
|
const double fraction = position - static_cast<double>(lower);
|
|
return static_cast<double>(values[lower]) * (1.0 - fraction) +
|
|
static_cast<double>(values[upper]) * fraction;
|
|
}
|
|
|
|
inline Json session_summary_json(const Capture_Session& session) {
|
|
std::vector<std::uint64_t> durations;
|
|
std::vector<std::uint64_t> waits;
|
|
std::map<Render_Node_Id, std::size_t> 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<Render_Plan_Version, std::shared_ptr<const Render_Plan>> 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<const Render_Plan> 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
|