From c7b1171846d4dcbbd6dc5461fbd9f83a40d0791b Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Fri, 26 Jun 2026 03:48:38 -0700 Subject: [PATCH] Improvements to studio profiler PiperOrigin-RevId: 938503994 Change-Id: I3a12647feea4c761dd8f8ba239f92355ceccfba1 --- src/experimental/platform/sim/sim_profiler.cc | 146 ++++++++++++++---- src/experimental/platform/sim/sim_profiler.h | 2 +- src/experimental/platform/ux/gui.cc | 94 ++++++----- src/experimental/platform/ux/gui.h | 2 +- src/experimental/platform/ux/imgui_widgets.h | 24 +++ src/experimental/studio/app.cc | 23 ++- src/experimental/studio/app.h | 1 + 7 files changed, 218 insertions(+), 74 deletions(-) diff --git a/src/experimental/platform/sim/sim_profiler.cc b/src/experimental/platform/sim/sim_profiler.cc index e3b794b8..2fe150a6 100644 --- a/src/experimental/platform/sim/sim_profiler.cc +++ b/src/experimental/platform/sim/sim_profiler.cc @@ -14,6 +14,12 @@ #include "experimental/platform/sim/sim_profiler.h" +#include +#include +#include +#include +#include + #include #include #include @@ -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& 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(dim_dof_.size()); + struct GetterData { + const std::vector* vec; + int count; + }; + auto getter = +[](int idx, void* user_data) -> ImPlotPoint { + const auto* gd = static_cast(user_data); + float val = (*gd->vec)[idx]; + double x = -gd->count + idx; + double y = (val <= 0.0f) ? std::nan("") : static_cast(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(); } diff --git a/src/experimental/platform/sim/sim_profiler.h b/src/experimental/platform/sim/sim_profiler.h index c1f1ef8e..d9afdcd6 100644 --- a/src/experimental/platform/sim/sim_profiler.h +++ b/src/experimental/platform/sim/sim_profiler.h @@ -46,7 +46,7 @@ class SimProfiler { std::vector dim_dof_; std::vector dim_body_; std::vector dim_constraint_; - std::vector dim_sqrt_nnz_; + std::vector dim_nnz_; std::vector dim_contact_; std::vector dim_iteration_; }; diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index 5e765af5..3682cdfa 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -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(user_data); const float x = static_cast(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(user_data); const float x = static_cast(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(user_data); const float x = static_cast(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); + } } } diff --git a/src/experimental/platform/ux/gui.h b/src/experimental/platform/ux/gui.h index a5b1f18b..cbb16a6a 100644 --- a/src/experimental/platform/ux/gui.h +++ b/src/experimental/platform/ux/gui.h @@ -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. diff --git a/src/experimental/platform/ux/imgui_widgets.h b/src/experimental/platform/ux/imgui_widgets.h index e7e42d35..f699acc6 100644 --- a/src/experimental/platform/ux/imgui_widgets.h +++ b/src/experimental/platform/ux/imgui_widgets.h @@ -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(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. diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index bd328b41..06d470bf 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -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(); diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index d0165268..9529679f 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -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;