Improvements to studio profiler

PiperOrigin-RevId: 938503994
Change-Id: I3a12647feea4c761dd8f8ba239f92355ceccfba1
This commit is contained in:
Yuval Tassa
2026-06-26 03:48:38 -07:00
committed by Copybara-Service
parent 56a182e67c
commit c7b1171846
7 changed files with 218 additions and 74 deletions
+113 -33
View File
@@ -14,6 +14,12 @@
#include "experimental/platform/sim/sim_profiler.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include <imgui.h>
#include <implot.h>
#include <mujoco/mujoco.h>
@@ -33,7 +39,7 @@ void SimProfiler::Clear() {
dim_dof_.clear();
dim_body_.clear();
dim_constraint_.clear();
dim_sqrt_nnz_.clear();
dim_nnz_.clear();
dim_contact_.clear();
dim_iteration_.clear();
@@ -45,7 +51,7 @@ void SimProfiler::Clear() {
dim_dof_.resize(kProfilerMaxFrames, 0);
dim_body_.resize(kProfilerMaxFrames, 0);
dim_constraint_.resize(kProfilerMaxFrames, 0);
dim_sqrt_nnz_.resize(kProfilerMaxFrames, 0);
dim_nnz_.resize(kProfilerMaxFrames, 0);
dim_contact_.resize(kProfilerMaxFrames, 0);
dim_iteration_.resize(kProfilerMaxFrames, 0);
}
@@ -63,20 +69,20 @@ void SimProfiler::Update(const mjModel* model, const mjData* data) {
return;
}
mjtNum avg_total = total / number;
mjtNum avg_total = 1000.0 * total / number;
cpu_total_.erase(cpu_total_.begin());
cpu_total_.push_back(avg_total);
mjtNum collision = data->timer[mjTIMER_POS_COLLISION].duration / number;
mjtNum collision = 1000.0 * data->timer[mjTIMER_POS_COLLISION].duration / number;
cpu_collision_.erase(cpu_collision_.begin());
cpu_collision_.push_back(collision);
mjtNum prepare = (data->timer[mjTIMER_POS_MAKE].duration / number) +
(data->timer[mjTIMER_POS_PROJECT].duration / number);
mjtNum prepare = 1000.0 * ((data->timer[mjTIMER_POS_MAKE].duration / number) +
(data->timer[mjTIMER_POS_PROJECT].duration / number));
cpu_prepare_.erase(cpu_prepare_.begin());
cpu_prepare_.push_back(prepare);
mjtNum solve = data->timer[mjTIMER_CONSTRAINT].duration / number;
mjtNum solve = 1000.0 * data->timer[mjTIMER_CONSTRAINT].duration / number;
cpu_solve_.erase(cpu_solve_.begin());
cpu_solve_.push_back(solve);
@@ -85,15 +91,14 @@ void SimProfiler::Update(const mjModel* model, const mjData* data) {
cpu_other_.push_back(other);
// Solver diagnostics.
mjtNum sqrt_nnz = 0;
mjtNum nnz = 0;
int solver_niter = 0;
const int nisland =
data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0;
for (int island = 0; island < nisland; island++) {
sqrt_nnz += data->solver_nnz[island];
nnz += data->solver_nnz[island];
solver_niter += data->solver_niter[island];
}
sqrt_nnz = mju_sqrt(sqrt_nnz);
dim_dof_.erase(dim_dof_.begin());
int nv = (model->opt.enableflags & mjENBL_SLEEP) ? data->nv_awake : model->nv;
@@ -107,8 +112,8 @@ void SimProfiler::Update(const mjModel* model, const mjData* data) {
dim_constraint_.erase(dim_constraint_.begin());
dim_constraint_.push_back(data->nefc);
dim_sqrt_nnz_.erase(dim_sqrt_nnz_.begin());
dim_sqrt_nnz_.push_back(sqrt_nnz);
dim_nnz_.erase(dim_nnz_.begin());
dim_nnz_.push_back(nnz);
dim_contact_.erase(dim_contact_.begin());
dim_contact_.push_back(data->ncon);
@@ -118,25 +123,65 @@ void SimProfiler::Update(const mjModel* model, const mjData* data) {
mjMAX(1, nisland));
}
// Returns a dynamically padded legend label for ImPlot.
static std::string GetLegendLabel(const char* name, const std::vector<float>& values) {
float sum = 0.f;
int count = 0;
for (float val : values) {
if (val != 0.f) {
sum += val;
count++;
}
}
float avg = count > 0 ? (sum / count) : 0.f;
char buf[128];
std::snprintf(buf, sizeof(buf), "%-9s (%4.0f)###%s", name, avg, name);
return std::string(buf);
}
// Returns a dynamically padded legend label for the Dimensions plot.
static std::string GetDimensionLabel(const char* name, float value) {
char buf[128];
std::snprintf(buf, sizeof(buf), "%-10s: %5.0f###%s", name, value, name);
return std::string(buf);
}
void SimProfiler::CpuTimeGraph(ImVec2 plot_size) {
ScopedStyle style;
style.Font(ScopedFont::kMono);
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
if (ImPlot::BeginPlot("CPU msec vs frame", plot_size, flags)) {
if (ImPlot::BeginPlot("CPU microseconds (avg) vs frame", plot_size, flags)) {
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f);
ImPlot_SetupTimeAxis(plot_size, "");
ImPlot_SetupValueAxis(plot_size, "", "%.2f");
ImPlot_SetupValueAxis(plot_size, "", "%.0f");
float max_val = 0.f;
for (float val : cpu_total_) {
if (val > max_val) {
max_val = val;
}
}
float rounded_max = std::max(100.0f, std::ceil(max_val / 100.0f) * 100.0f);
ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, rounded_max, ImPlotCond_Always);
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish();
ImPlot::PlotLine("total", cpu_total_.data(), cpu_total_.size(), 1,
ImPlot::PlotLine(GetLegendLabel("total", cpu_total_).c_str(),
cpu_total_.data(), cpu_total_.size(), 1,
-(int)cpu_total_.size());
ImPlot::PlotLine("prepare", cpu_prepare_.data(), cpu_prepare_.size(), 1,
ImPlot::PlotLine(GetLegendLabel("prepare", cpu_prepare_).c_str(),
cpu_prepare_.data(), cpu_prepare_.size(), 1,
-(int)cpu_prepare_.size());
ImPlot::PlotLine("solve", cpu_solve_.data(), cpu_solve_.size(), 1,
ImPlot::PlotLine(GetLegendLabel("solve", cpu_solve_).c_str(),
cpu_solve_.data(), cpu_solve_.size(), 1,
-(int)cpu_solve_.size());
ImPlot::PlotLine("collision", cpu_collision_.data(), cpu_collision_.size(),
1, -(int)cpu_collision_.size());
ImPlot::PlotLine("other", cpu_other_.data(), cpu_other_.size(), 1,
ImPlot::PlotLine(GetLegendLabel("collision", cpu_collision_).c_str(),
cpu_collision_.data(), cpu_collision_.size(), 1,
-(int)cpu_collision_.size());
ImPlot::PlotLine(GetLegendLabel("other", cpu_other_).c_str(),
cpu_other_.data(), cpu_other_.size(), 1,
-(int)cpu_other_.size());
ImPlot::PopStyleVar();
ImPlot::EndPlot();
@@ -144,27 +189,62 @@ void SimProfiler::CpuTimeGraph(ImVec2 plot_size) {
}
void SimProfiler::DimensionsGraph(ImVec2 plot_size) {
ScopedStyle style;
style.Font(ScopedFont::kMono);
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
if (ImPlot::BeginPlot("Dimensions vs frame", plot_size, flags)) {
if (ImPlot::BeginPlot("Dimensions: current vs frame", plot_size, flags)) {
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f);
ImPlot_SetupTimeAxis(plot_size, "");
ImPlot_SetupValueAxis(plot_size, "", "%.0f");
ImPlot::SetupAxisScale(ImAxis_Y1, ImPlotScale_Log10);
float max_val = 10.f;
for (float val : dim_dof_) { max_val = std::max(max_val, val); }
for (float val : dim_body_) { max_val = std::max(max_val, val); }
for (float val : dim_constraint_) { max_val = std::max(max_val, val); }
for (float val : dim_nnz_) { max_val = std::max(max_val, val); }
for (float val : dim_contact_) { max_val = std::max(max_val, val); }
for (float val : dim_iteration_) { max_val = std::max(max_val, val); }
float log_max = std::log10(max_val);
float y_max = std::pow(10.f, log_max + 0.1f);
ImPlot::SetupAxisLimits(ImAxis_Y1, 1.0, y_max, ImPlotCond_Always);
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish();
ImPlot::PlotLine("dof", dim_dof_.data(), dim_dof_.size(), 1,
-(int)dim_dof_.size());
ImPlot::PlotLine("body", dim_body_.data(), dim_body_.size(), 1,
-(int)dim_body_.size());
ImPlot::PlotLine("constraint", dim_constraint_.data(),
dim_constraint_.size(), 1, -(int)dim_constraint_.size());
ImPlot::PlotLine("sqrt(nnz)", dim_sqrt_nnz_.data(), dim_sqrt_nnz_.size(), 1,
-(int)dim_sqrt_nnz_.size());
ImPlot::PlotLine("contact", dim_contact_.data(), dim_contact_.size(), 1,
-(int)dim_contact_.size());
ImPlot::PlotLine("iteration", dim_iteration_.data(), dim_iteration_.size(),
1, -(int)dim_iteration_.size());
const int count = static_cast<int>(dim_dof_.size());
struct GetterData {
const std::vector<float>* vec;
int count;
};
auto getter = +[](int idx, void* user_data) -> ImPlotPoint {
const auto* gd = static_cast<const GetterData*>(user_data);
float val = (*gd->vec)[idx];
double x = -gd->count + idx;
double y = (val <= 0.0f) ? std::nan("") : static_cast<double>(val);
return ImPlotPoint{x, y};
};
GetterData nnz_data{&dim_nnz_, count};
ImPlot::PlotLineG(GetDimensionLabel("nnz", dim_nnz_.back()).c_str(),
getter, &nnz_data, count);
GetterData constraint_data{&dim_constraint_, count};
ImPlot::PlotLineG(
GetDimensionLabel("constraint", dim_constraint_.back()).c_str(),
getter, &constraint_data, count);
GetterData dof_data{&dim_dof_, count};
ImPlot::PlotLineG(GetDimensionLabel("dof", dim_dof_.back()).c_str(),
getter, &dof_data, count);
GetterData contact_data{&dim_contact_, count};
ImPlot::PlotLineG(GetDimensionLabel("contact", dim_contact_.back()).c_str(),
getter, &contact_data, count);
GetterData body_data{&dim_body_, count};
ImPlot::PlotLineG(GetDimensionLabel("body", dim_body_.back()).c_str(),
getter, &body_data, count);
GetterData iteration_data{&dim_iteration_, count};
ImPlot::PlotLineG(
GetDimensionLabel("iteration", dim_iteration_.back()).c_str(),
getter, &iteration_data, count);
ImPlot::PopStyleVar();
ImPlot::EndPlot();
}
+1 -1
View File
@@ -46,7 +46,7 @@ class SimProfiler {
std::vector<float> dim_dof_;
std::vector<float> dim_body_;
std::vector<float> dim_constraint_;
std::vector<float> dim_sqrt_nnz_;
std::vector<float> dim_nnz_;
std::vector<float> dim_contact_;
std::vector<float> dim_iteration_;
};
+59 -35
View File
@@ -1313,19 +1313,25 @@ static int GetPlotXLimit(const mjData* data) {
for (int k = 0; k < nisland0; k++) {
max_niter = mjMAX(max_niter, data->solver_niter[k]);
}
return mjMAX(10, ((max_niter + 9) / 10) * 10);
return max_niter <= 10 ? 10 : ((max_niter + 59) / 60) * 60;
}
void ConvergenceGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
ScopedStyle style;
style.Font(ScopedFont::kMono);
int xlim = GetPlotXLimit(data);
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
if (ImPlot::BeginPlot("Convergence (log 10) vs iter", plot_size, flags)) {
if (ImPlot::BeginPlot("Convergence vs iter", plot_size, flags)) {
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f);
ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, xlim, ImPlotCond_Always);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.1f");
ImPlot::SetupAxisLimits(ImAxis_Y1, -20, 5, ImPlotCond_Always);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0e");
ImPlot::SetupAxisLimits(ImAxis_Y1, 1e-15, 1e0, ImPlotCond_Always);
ImPlot::SetupAxisScale(ImAxis_Y1, ImPlotScale_Log10);
const double ticks[] = {1e-15, 1e-12, 1e-9, 1e-6, 1e-3, 1e0};
ImPlot::SetupAxisTicks(ImAxis_Y1, ticks, 6);
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish();
@@ -1342,7 +1348,7 @@ void ConvergenceGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = mju_log10(mju_max(mjMINVAL, stats[i].improvement));
const float y = mju_max(mjMINVAL, stats[i].improvement);
return ImPlotPoint{x, y};
},
stats, npoints);
@@ -1357,7 +1363,7 @@ void ConvergenceGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = mju_log10(mju_max(mjMINVAL, stats[i].gradient));
const float y = mju_max(mjMINVAL, stats[i].gradient);
return ImPlotPoint{x, y};
},
stats, npoints);
@@ -1368,7 +1374,7 @@ void ConvergenceGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
const mjSolverStat* stats =
static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = mju_log10(mju_max(mjMINVAL, stats[i].lineslope));
const float y = mju_max(mjMINVAL, stats[i].lineslope);
return ImPlotPoint{x, y};
},
stats, npoints);
@@ -1380,6 +1386,9 @@ void ConvergenceGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
}
void CountsGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
ScopedStyle style;
style.Font(ScopedFont::kMono);
int xlim = GetPlotXLimit(data);
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
@@ -1538,27 +1547,13 @@ void InfoGui(const mjModel* model, const mjData* data, bool paused,
ImGui::Columns();
}
void ProfilerGui(const mjModel* model, mjData* data, SimProfiler* profiler) {
ImGui::SetWindowFontScale(0.8f);
void ProfilerGui(const mjModel* model, mjData* data, SimProfiler* profiler, bool show_iter) {
ImVec2 avail = ImGui::GetContentRegionAvail();
const float pad = ImGui::GetStyle().ItemSpacing.x;
const float aspect = avail.y > 0 ? avail.x / avail.y : 1.0f;
ImVec2 plot_size;
int cols;
if (aspect < 0.8f) {
plot_size.x = avail.x;
plot_size.y = (avail.y - pad * 3.0f) * 0.25f;
cols = 1;
} else if (aspect < 1.8f) {
plot_size.x = (avail.x - pad) * 0.5f;
plot_size.y = (avail.y - pad) * 0.5f;
cols = 2;
} else {
plot_size.x = (avail.x - pad * 3.0f) * 0.25f;
plot_size.y = avail.y;
cols = 4;
}
int current_col = 0;
auto advance = [&]() {
@@ -1570,23 +1565,52 @@ void ProfilerGui(const mjModel* model, mjData* data, SimProfiler* profiler) {
}
};
if (cols == 2) {
// In 2x2 layout, vertically stack charts with the same x-axis.
CountsGui(model, data, plot_size);
advance();
if (!show_iter) {
if (aspect < 0.8f) {
plot_size.x = avail.x;
plot_size.y = (avail.y - pad) * 0.5f;
cols = 1;
} else {
plot_size.x = (avail.x - pad) * 0.5f;
plot_size.y = avail.y;
cols = 2;
}
profiler->DimensionsGraph(plot_size);
advance();
ConvergenceGui(model, data, plot_size);
advance();
profiler->CpuTimeGraph(plot_size);
} else {
CountsGui(model, data, plot_size);
advance();
ConvergenceGui(model, data, plot_size);
advance();
profiler->DimensionsGraph(plot_size);
advance();
profiler->CpuTimeGraph(plot_size);
if (aspect < 0.8f) {
plot_size.x = avail.x;
plot_size.y = (avail.y - pad * 3.0f) * 0.25f;
cols = 1;
} else if (aspect < 1.8f) {
plot_size.x = (avail.x - pad) * 0.5f;
plot_size.y = (avail.y - pad) * 0.5f;
cols = 2;
} else {
plot_size.x = (avail.x - pad * 3.0f) * 0.25f;
plot_size.y = avail.y;
cols = 4;
}
if (cols == 2) {
// In 2x2 layout, vertically stack charts with the same x-axis.
CountsGui(model, data, plot_size);
advance();
profiler->DimensionsGraph(plot_size);
advance();
ConvergenceGui(model, data, plot_size);
advance();
profiler->CpuTimeGraph(plot_size);
} else {
CountsGui(model, data, plot_size);
advance();
ConvergenceGui(model, data, plot_size);
advance();
profiler->DimensionsGraph(plot_size);
advance();
profiler->CpuTimeGraph(plot_size);
}
}
}
+1 -1
View File
@@ -160,7 +160,7 @@ void CountsGui(const mjModel* model, mjData* data,
ImVec2 plot_size = ImVec2(-1, 0));
// UX for Profiler panel combining Solver and Performance metrics.
void ProfilerGui(const mjModel* model, mjData* data, SimProfiler* profiler);
void ProfilerGui(const mjModel* model, mjData* data, SimProfiler* profiler, bool show_iter);
// UX for displaying basic simulation information. Note that the pause state and
// FPS needs to be tracked by the caller and passed here to be displayed.
@@ -126,6 +126,11 @@ T ReadIniValue(const KeyValues& key_values, const std::string& key, T def) {
}
}
enum class ScopedFont {
kDefault = 0,
kMono = 1,
};
// Helper class for setting ImGui style options; automatically resets the
// styles when going out of scope.
struct ScopedStyle {
@@ -143,6 +148,20 @@ struct ScopedStyle {
void Swap(ScopedStyle& other) {
std::swap(num_colors, other.num_colors);
std::swap(num_vars, other.num_vars);
std::swap(num_fonts, other.num_fonts);
}
ScopedStyle& Font(int index) {
ImFontAtlas* atlas = ImGui::GetIO().Fonts;
if (atlas && index >= 0 && index < atlas->Fonts.Size) {
ImGui::PushFont(atlas->Fonts[index]);
++num_fonts;
}
return *this;
}
ScopedStyle& Font(ScopedFont font) {
return Font(static_cast<int>(font));
}
ScopedStyle& Color(ImGuiCol col, ImColor color) {
@@ -172,14 +191,19 @@ struct ScopedStyle {
ImVec4 CurrentColor(ImGuiCol col) { return ImGui::GetStyle().Colors[col]; }
void Reset() {
for (int i = 0; i < num_fonts; ++i) {
ImGui::PopFont();
}
ImGui::PopStyleVar(num_vars);
ImGui::PopStyleColor(num_colors);
num_colors = 0;
num_vars = 0;
num_fonts = 0;
}
int num_colors = 0;
int num_vars = 0;
int num_fonts = 0;
};
// Helper for displaying rows of key/value pairs in an ImGui table.
+19 -4
View File
@@ -314,7 +314,7 @@ void App::UpdatePhysics() {
platform::StepControl::Status status =
step_control_.Advance(model(), data());
if (status == platform::StepControl::Status::kPaused) {
// do nothing
profiler_.Update(model(), data());
} else if (status == platform::StepControl::Status::kOk) {
stepped = true;
// If we are adding to the history we didn't have a divergence error
@@ -694,7 +694,15 @@ void App::HandleKeyboardEvents() {
} else if (ImGui_IsChordJustPressed(ImGuiKey_F2)) {
ToggleWindow(tmp_.info);
} else if (ImGui_IsChordJustPressed(ImGuiKey_F3)) {
ToggleWindow(tmp_.profiler);
// Cycle through profiler views.
if (!tmp_.profiler) {
tmp_.profiler = true;
tmp_.profiler_show_iter = false;
} else if (!tmp_.profiler_show_iter) {
tmp_.profiler_show_iter = true;
} else {
tmp_.profiler = false;
}
} else if (ImGui_IsChordJustPressed(ImGuiKey_F6)) {
vis_options_.frame = (vis_options_.frame + 1) % mjNFRAME;
} else if (ImGui_IsChordJustPressed(ImGuiKey_F7)) {
@@ -949,7 +957,7 @@ void App::BuildGui() {
if (tmp_.profiler) {
if (ImGui::Begin("Profiler", &tmp_.profiler,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
platform::ProfilerGui(model(), data(), &profiler_);
platform::ProfilerGui(model(), data(), &profiler_, tmp_.profiler_show_iter);
}
ImGui::End();
}
@@ -2003,7 +2011,14 @@ void App::MainMenuGui() {
ToggleWindow(tmp_.info);
}
if (ImGui::MenuItem("Profiler", "F3", tmp_.profiler)) {
ToggleWindow(tmp_.profiler);
if (!tmp_.profiler) {
tmp_.profiler = true;
tmp_.profiler_show_iter = false;
} else if (!tmp_.profiler_show_iter) {
tmp_.profiler_show_iter = true;
} else {
tmp_.profiler = false;
}
}
ImGui::Separator();
+1
View File
@@ -132,6 +132,7 @@ class App {
bool help = false;
bool info = false;
bool profiler = false;
bool profiler_show_iter = false;
bool picture_in_picture = false;
bool options_panel = true;
bool toolbar = false;