// dear imgui test engine // (performance tool) // Browse and visualize samples recorded by ctx->PerfCapture() calls. // User access via 'Test Engine UI -> Tools -> Perf Tool' /* Index of this file: // [SECTION] Header mess // [SECTION] ImGuiPerflogEntry // [SECTION] Types & everything else // [SECTION] USER INTERFACE // [SECTION] SETTINGS // [SECTION] TESTS */ // Terminology: // * Entry: information about execution of a single perf test. This corresponds to one line in CSV file. // * Batch: a group of entries that were created together during a single execution. A new batch is created each time // one or more perf tests are executed. All entries in a single batch will have a matching ImGuiPerflogEntry::Timestamp. // * Build: A group of batches that have matching BuildType, OS, Cpu, Compiler, GitBranchName. // * Baseline: A batch that we are comparing against. Baselines are identified by batch timestamp and build id. //------------------------------------------------------------------------- // [SECTION] Header mess //------------------------------------------------------------------------- #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) #define _CRT_SECURE_NO_WARNINGS #endif #define IMGUI_DEFINE_MATH_OPERATORS #include "imgui_te_perftool.h" #include "imgui.h" #include "imgui_internal.h" #include "imgui_te_utils.h" #include "thirdparty/Str/Str.h" #include // time(), localtime() #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT #include "implot.h" #include "implot_internal.h" #endif // For tests #include "imgui_te_engine.h" #include "imgui_te_context.h" #include "imgui_te_internal.h" // ImGuiTestEngine_GetPerfTool() #include "imgui_capture_tool.h" //------------------------------------------------------------------------- // [SECTION] ImGuiPerflogEntry //------------------------------------------------------------------------- void ImGuiPerfToolEntry::Set(const ImGuiPerfToolEntry& other) { Timestamp = other.Timestamp; Category = other.Category; TestName = other.TestName; DtDeltaMs = other.DtDeltaMs; DtDeltaMsMin = other.DtDeltaMsMin; DtDeltaMsMax = other.DtDeltaMsMax; NumSamples = other.NumSamples; PerfStressAmount = other.PerfStressAmount; GitBranchName = other.GitBranchName; BuildType = other.BuildType; Cpu = other.Cpu; OS = other.OS; Compiler = other.Compiler; Date = other.Date; //DateMax = ... VsBaseline = other.VsBaseline; LabelIndex = other.LabelIndex; } //------------------------------------------------------------------------- // [SECTION] Types & everything else //------------------------------------------------------------------------- typedef ImGuiID(*HashEntryFn)(ImGuiPerfToolEntry* entry); typedef void(*FormatEntryLabelFn)(ImGuiPerfTool* perftool, Str* result, ImGuiPerfToolEntry* entry); struct ImGuiPerfToolColumnInfo { const char* Title; int Offset; ImGuiDataType Type; bool ShowAlways; ImGuiTableFlags Flags; template T GetValue(const ImGuiPerfToolEntry* entry) const { return *(T*)((const char*)entry + Offset); } }; // Update _ShowEntriesTable() and SaveHtmlReport() when adding new entries. static const ImGuiPerfToolColumnInfo PerfToolColumnInfo[] = { { /* 00 */ "Date", offsetof(ImGuiPerfToolEntry, Timestamp), ImGuiDataType_U64, true, ImGuiTableColumnFlags_DefaultHide }, { /* 01 */ "Test Name", offsetof(ImGuiPerfToolEntry, TestName), ImGuiDataType_COUNT, true, 0 }, { /* 02 */ "Branch", offsetof(ImGuiPerfToolEntry, GitBranchName), ImGuiDataType_COUNT, true, 0 }, { /* 03 */ "Compiler", offsetof(ImGuiPerfToolEntry, Compiler), ImGuiDataType_COUNT, true, 0 }, { /* 04 */ "OS", offsetof(ImGuiPerfToolEntry, OS), ImGuiDataType_COUNT, true, 0 }, { /* 05 */ "CPU", offsetof(ImGuiPerfToolEntry, Cpu), ImGuiDataType_COUNT, true, 0 }, { /* 06 */ "Build", offsetof(ImGuiPerfToolEntry, BuildType), ImGuiDataType_COUNT, true, 0 }, { /* 07 */ "Stress", offsetof(ImGuiPerfToolEntry, PerfStressAmount), ImGuiDataType_S32, true, 0 }, { /* 08 */ "Avg ms", offsetof(ImGuiPerfToolEntry, DtDeltaMs), ImGuiDataType_Double, true, 0 }, { /* 09 */ "Min ms", offsetof(ImGuiPerfToolEntry, DtDeltaMsMin), ImGuiDataType_Double, false, 0 }, { /* 00 */ "Max ms", offsetof(ImGuiPerfToolEntry, DtDeltaMsMax), ImGuiDataType_Double, false, 0 }, { /* 11 */ "Samples", offsetof(ImGuiPerfToolEntry, NumSamples), ImGuiDataType_S32, false, 0 }, { /* 12 */ "VS Baseline", offsetof(ImGuiPerfToolEntry, VsBaseline), ImGuiDataType_Float, true, 0 }, }; static const char* PerfToolReportDefaultOutputPath = "./output/capture_perf_report.html"; // This is declared as a standalone function in order to run without a PerfTool instance void ImGuiTestEngine_PerfToolAppendToCSV(ImGuiPerfTool* perf_log, ImGuiPerfToolEntry* entry, const char* filename) { if (filename == NULL) filename = IMGUI_PERFLOG_DEFAULT_FILENAME; if (!ImFileCreateDirectoryChain(filename, ImPathFindFilename(filename))) { fprintf(stderr, "Unable to create missing directory '%*s', perftool entry was not saved.\n", (int)(ImPathFindFilename(filename) - filename), filename); return; } // Appends to .csv FILE* f = fopen(filename, "a+b"); if (f == NULL) { fprintf(stderr, "Unable to open '%s', perftool entry was not saved.\n", filename); return; } fprintf(f, "%llu,%s,%s,%.3f,x%d,%s,%s,%s,%s,%s,%s\n", entry->Timestamp, entry->Category, entry->TestName, entry->DtDeltaMs, entry->PerfStressAmount, entry->GitBranchName, entry->BuildType, entry->Cpu, entry->OS, entry->Compiler, entry->Date); fflush(f); fclose(f); // Register to runtime perf tool if any if (perf_log != NULL) perf_log->AddEntry(entry); } // Tri-state button. Copied and modified ButtonEx(). static bool Button3(const char* label, int* value) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const ImGuiID id = window->GetID(label); const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); float dot_radius2 = g.FontSize; ImVec2 btn_size(dot_radius2 * 2, dot_radius2); ImVec2 pos = window->DC.CursorPos; ImVec2 size = ImGui::CalcItemSize(ImVec2(), btn_size.x + label_size.x + style.FramePadding.x * 2.0f + style.ItemInnerSpacing.x, label_size.y + style.FramePadding.y * 2.0f); const ImRect bb(pos, pos + size); ImGui::ItemSize(size, style.FramePadding.y); if (!ImGui::ItemAdd(bb, id)) return false; bool hovered, held; bool pressed = ImGui::ButtonBehavior(ImRect(pos, pos + style.FramePadding + btn_size), id, &hovered, &held, 0); // Render const ImU32 col = ImGui::GetColorU32(ImGuiCol_FrameBg); ImGui::RenderNavHighlight(bb, id); ImGui::RenderFrame(bb.Min + style.FramePadding, bb.Min + style.FramePadding + btn_size, col, true, /*style.FrameRounding*/ 5.0f); ImColor btn_col; if (held) btn_col = style.Colors[ImGuiCol_SliderGrabActive]; else if (hovered) btn_col = style.Colors[ImGuiCol_ButtonHovered]; else btn_col = style.Colors[ImGuiCol_SliderGrab]; ImVec2 center = bb.Min + ImVec2(dot_radius2 + (dot_radius2 * (float)*value), dot_radius2) * 0.5f + style.FramePadding; window->DrawList->AddCircleFilled(center, dot_radius2 * 0.5f, btn_col); ImRect text_bb; text_bb.Min = bb.Min + style.FramePadding + ImVec2(btn_size.x + style.ItemInnerSpacing.x, 0); text_bb.Max = text_bb.Min + label_size; ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, label, NULL, &label_size, style.ButtonTextAlign, &bb); *value = (*value + pressed) % 3; return pressed; } static ImGuiID GetBuildID(const ImGuiPerfToolEntry* entry) { IM_ASSERT(entry != NULL); ImGuiID build_id = ImHashStr(entry->BuildType); build_id = ImHashStr(entry->OS, 0, build_id); build_id = ImHashStr(entry->Cpu, 0, build_id); build_id = ImHashStr(entry->Compiler, 0, build_id); build_id = ImHashStr(entry->GitBranchName, 0, build_id); return build_id; } static ImGuiID GetBuildID(const ImGuiPerfToolBatch* batch) { IM_ASSERT(batch != NULL); IM_ASSERT(!batch->Entries.empty()); return GetBuildID(&batch->Entries.Data[0]); } // Batch ID depends on display type. It is either a build ID (when combinding by build type) or batch timestamp otherwise. static ImGuiID GetBatchID(const ImGuiPerfTool* perftool, const ImGuiPerfToolEntry* entry) { IM_ASSERT(perftool != NULL); IM_ASSERT(entry != NULL); if (perftool->_DisplayType == ImGuiPerfToolDisplayType_CombineByBuildInfo) return GetBuildID(entry); else return (ImU32)entry->Timestamp; } static int PerfToolComparerStr(const void* a, const void* b) { return strcmp(*(const char**)b, *(const char**)a); } static int IMGUI_CDECL PerfToolComparerByEntryInfo(const void* lhs, const void* rhs) { const ImGuiPerfToolEntry* a = (const ImGuiPerfToolEntry*)lhs; const ImGuiPerfToolEntry* b = (const ImGuiPerfToolEntry*)rhs; // While build ID does include git branch it wont ensure branches are grouped together, therefore we do branch // sorting manually. int result = strcmp(a->GitBranchName, b->GitBranchName); // Now that we have groups of branches - sort individual builds within those groups. if (result == 0) result = ImClamp((int)((ImS64)GetBuildID(a) - (ImS64)GetBuildID(b)), -1, +1); // Group individual runs together within build groups. if (result == 0) result = (int)ImClamp((ImS64)b->Timestamp - (ImS64)a->Timestamp, -1, +1); // And finally sort individual runs by perf name so we can have a predictable order (used to optimize in _Rebuild()). if (result == 0) result = (int)strcmp(a->TestName, b->TestName); return result; } static ImGuiPerfTool* PerfToolInstance = NULL; static int IMGUI_CDECL CompareWithSortSpecs(const void* lhs, const void* rhs) { IM_ASSERT(PerfToolInstance != NULL); ImGuiPerfTool* tool = PerfToolInstance; const ImGuiTableSortSpecs* sort_specs = PerfToolInstance->_InfoTableSortSpecs; int batch_index_a, entry_index_a, mono_index_a, batch_index_b, entry_index_b, mono_index_b; tool->_UnpackSortedKey(*(ImU64*)lhs, &batch_index_a, &entry_index_a, &mono_index_a); tool->_UnpackSortedKey(*(ImU64*)rhs, &batch_index_b, &entry_index_b, &mono_index_b); for (int i = 0; i < sort_specs->SpecsCount; i++) { const ImGuiTableColumnSortSpecs* specs = &sort_specs->Specs[i]; const ImGuiPerfToolColumnInfo& col_info = PerfToolColumnInfo[specs->ColumnIndex]; const ImGuiPerfToolBatch* batch_a = &tool->_Batches[batch_index_a]; const ImGuiPerfToolBatch* batch_b = &tool->_Batches[batch_index_b]; ImGuiPerfToolEntry* a = &batch_a->Entries.Data[entry_index_a]; ImGuiPerfToolEntry* b = &batch_b->Entries.Data[entry_index_b]; if (specs->SortDirection == ImGuiSortDirection_Ascending) ImSwap(a, b); int result = 0; switch (col_info.Type) { case ImGuiDataType_S32: result = col_info.GetValue(a) - col_info.GetValue(b); break; case ImGuiDataType_U64: result = (int)(col_info.GetValue(a) - col_info.GetValue(b)); break; case ImGuiDataType_Float: result = (int)((col_info.GetValue(a) - col_info.GetValue(b)) * 1000.0f); break; case ImGuiDataType_Double: result = (int)((col_info.GetValue(a) - col_info.GetValue(b)) * 1000.0); break; case ImGuiDataType_COUNT: result = strcmp(col_info.GetValue(a), col_info.GetValue(b)); break; default: IM_ASSERT(false); } if (result != 0) return result; } return mono_index_a - mono_index_b; } // Dates are in format "YYYY-MM-DD" static bool IsDateValid(const char* date) { if (date[4] != '-' || date[7] != '-') return false; for (int i = 0; i < 10; i++) { if (i == 4 || i == 7) continue; if (date[i] < '0' || date[i] > '9') return false; } return true; } static float FormatVsBaseline(ImGuiPerfToolEntry* entry, ImGuiPerfToolEntry* baseline_entry, Str& out_label) { if (baseline_entry == NULL) { out_label.appendf("--"); return FLT_MAX; } if (entry == baseline_entry) { out_label.append("baseline"); return FLT_MAX; } double percent_vs_first = 100.0 / baseline_entry->DtDeltaMs * entry->DtDeltaMs; double dt_change = -(100.0 - percent_vs_first); if (dt_change == INFINITY) out_label.appendf("--"); else if (ImAbs(dt_change) > 0.001f) out_label.appendf("%+.2lf%% (%s)", dt_change, dt_change < 0.0f ? "faster" : "slower"); else out_label.appendf("=="); return (float)dt_change; } #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT static void PerfToolFormatBuildInfo(ImGuiPerfTool* perftool, Str* result, ImGuiPerfToolBatch* batch) { IM_ASSERT(perftool != NULL); IM_ASSERT(result != NULL); IM_ASSERT(batch != NULL); IM_ASSERT(batch->Entries.Size > 0); ImGuiPerfToolEntry* entry = &batch->Entries.Data[0]; Str64f legend_format("x%%-%dd %%-%ds %%-%ds %%-%ds %%-%ds %%-%ds %%s%%s%%s%%s(%%-%dd sample%%s)%%s", perftool->_AlignStress, perftool->_AlignType, perftool->_AlignCpu, perftool->_AlignOs, perftool->_AlignCompiler, perftool->_AlignBranch, perftool->_AlignSamples); result->appendf(legend_format.c_str(), entry->PerfStressAmount, entry->BuildType, entry->Cpu, entry->OS, entry->Compiler, entry->GitBranchName, entry->Date, #if 0 // Show min-max dates. perftool->_CombineByBuildInfo ? " - " : "", entry->DateMax ? entry->DateMax : "", #else "", "", #endif *entry->Date ? " " : "", batch->NumSamples, batch->NumSamples > 1 ? "s" : "", // Singular/plural form of "sample(s)" batch->NumSamples > 1 || perftool->_AlignSamples == 1 ? "" : " " // Space after legend entry to separate * marking baseline ); } #endif static int PerfToolCountBuilds(ImGuiPerfTool* perftool, bool only_visible) { int num_builds = 0; ImU64 build_id = 0; for (ImGuiPerfToolEntry& entry : perftool->_SrcData) { if (build_id != GetBuildID(&entry)) { if (!only_visible || perftool->_IsVisibleBuild(&entry)) num_builds++; build_id = GetBuildID(&entry); } } return num_builds; } static bool InputDate(const char* label, char* date, int date_len, bool valid) { ImGui::SetNextItemWidth(ImGui::CalcTextSize("YYYY-MM-DD").x + ImGui::GetStyle().FramePadding.x * 2.0f); const bool date_valid = date[0] == 0 || (IsDateValid(date) && valid); if (!date_valid) { ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32(255, 0, 0, 255)); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1); } bool date_changed = ImGui::InputTextWithHint(label, "YYYY-MM-DD", date, date_len); if (!date_valid) { ImGui::PopStyleVar(); ImGui::PopStyleColor(); } return date_changed; } static void FormatDate(ImU64 microseconds, char* buf, size_t buf_size) { time_t timestamp = (time_t)(microseconds / 1000000); tm* time = localtime(×tamp); ImFormatString(buf, buf_size, "%04d-%02d-%02d", time->tm_year + 1900, time->tm_mon + 1, time->tm_mday); } static void FormatDateAndTime(ImU64 microseconds, char* buf, size_t buf_size) { time_t timestamp = (time_t)(microseconds / 1000000); tm* time = localtime(×tamp); ImFormatString(buf, buf_size, "%04d-%02d-%02d %02d:%02d:%02d", time->tm_year + 1900, time->tm_mon + 1, time->tm_mday, time->tm_hour, time->tm_min, time->tm_sec); } static void RenderFilterInput(ImGuiPerfTool* perf, const char* hint, float width = -FLT_MIN) { if (ImGui::IsWindowAppearing()) strcpy(perf->_Filter, ""); ImGui::SetNextItemWidth(width); ImGui::InputTextWithHint("##filter", hint, perf->_Filter, IM_ARRAYSIZE(perf->_Filter)); if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere(); } static bool RenderMultiSelectFilter(ImGuiPerfTool* perf, const char* filter_hint, ImVector* labels) { ImGuiContext& g = *ImGui::GetCurrentContext(); ImGuiIO& io = ImGui::GetIO(); ImGuiStorage& visibility = perf->_Visibility; bool modified = false; RenderFilterInput(perf, filter_hint, -(ImGui::CalcTextSize("(?)").x + g.Style.ItemSpacing.x)); ImGui::SameLine(); ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Hold CTRL to invert other items.\nHold SHIFT to close popup instantly."); // Keep popup open for multiple actions if SHIFT is pressed. if (!io.KeyShift) ImGui::PushItemFlag(ImGuiItemFlags_SelectableDontClosePopup, true); if (ImGui::MenuItem("Show All")) { for (const char* label : *labels) if (strstr(label, perf->_Filter) != NULL) visibility.SetBool(ImHashStr(label), true); modified = true; } if (ImGui::MenuItem("Hide All")) { for (const char* label : *labels) if (strstr(label, perf->_Filter) != NULL) visibility.SetBool(ImHashStr(label), false); modified = true; } // Render perf labels in reversed order. Labels are sorted, but stored in reversed order to render them on the plot // from top down (ImPlot renders stuff from bottom up). int filtered_entries = 0; for (int i = labels->Size - 1; i >= 0; i--) { const char* label = (*labels)[i]; if (strstr(label, perf->_Filter) == NULL) // Filter out entries not matching a filter query continue; if (filtered_entries == 0) ImGui::Separator(); ImGuiID build_id = ImHashStr(label); bool visible = visibility.GetBool(build_id, true); if (ImGui::MenuItem(label, NULL, &visible)) { modified = true; if (io.KeyCtrl) { for (const char* label2 : *labels) { ImGuiID build_id2 = ImHashStr(label2); visibility.SetBool(build_id2, !visibility.GetBool(build_id2, true)); } } else { visibility.SetBool(build_id, !visibility.GetBool(build_id, true)); } } filtered_entries++; } if (!io.KeyShift) ImGui::PopItemFlag(); return modified; } // Based on ImPlot::SetupFinish(). #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT static ImRect ImPlotGetYTickRect(int t, int y = 0) { ImPlotContext& gp = *GImPlot; ImPlotPlot& plot = *gp.CurrentPlot; ImPlotAxis& ax = plot.YAxis(y); const ImPlotTicker& tkc = ax.Ticker; const bool opp = ax.IsOpposite(); ImRect result(1.0f, 1.0f, -1.0f, -1.0f); if (ax.HasTickLabels()) { const ImPlotTick& tk = tkc.Ticks[t]; const float datum = ax.Datum1 + (opp ? gp.Style.LabelPadding.x : (-gp.Style.LabelPadding.x - tk.LabelSize.x)); if (tk.ShowLabel && tk.PixelPos >= plot.PlotRect.Min.y - 1 && tk.PixelPos <= plot.PlotRect.Max.y + 1) { ImVec2 start(datum, tk.PixelPos - 0.5f * tk.LabelSize.y); result.Min = start; result.Max = start + tk.LabelSize; } } return result; } #endif // #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT ImGuiPerfTool::ImGuiPerfTool() { _CsvParser = IM_NEW(ImGuiCsvParser)(); Clear(); } ImGuiPerfTool::~ImGuiPerfTool() { _SrcData.clear_destruct(); _Batches.clear_destruct(); IM_DELETE(_CsvParser); } void ImGuiPerfTool::AddEntry(ImGuiPerfToolEntry* entry) { if (strcmp(_FilterDateFrom, entry->Date) > 0) ImStrncpy(_FilterDateFrom, entry->Date, IM_ARRAYSIZE(_FilterDateFrom)); if (strcmp(_FilterDateTo, entry->Date) < 0) ImStrncpy(_FilterDateTo, entry->Date, IM_ARRAYSIZE(_FilterDateTo)); _SrcData.push_back(*entry); _Batches.clear_destruct(); } void ImGuiPerfTool::_Rebuild() { if (_SrcData.empty()) return; ImGuiStorage& temp_set = _TempSet; _Labels.resize(0); _LabelsVisible.resize(0); _InfoTableSort.resize(0); _Batches.clear_destruct(); _InfoTableSortDirty = true; // Gather all visible labels. Legend batches will store data in this order. temp_set.Data.resize(0); // name_id:IsLabelSeen for (ImGuiPerfToolEntry& entry : _SrcData) { ImGuiID name_id = ImHashStr(entry.TestName); if (!temp_set.GetBool(name_id)) { temp_set.SetBool(name_id, true); _Labels.push_back(entry.TestName); if (_IsVisibleTest(entry.TestName)) _LabelsVisible.push_front(entry.TestName); } } int num_visible_labels = _LabelsVisible.Size; // Labels are sorted in reverse order so they appear to be oredered from top down. ImQsort(_Labels.Data, _Labels.Size, sizeof(const char*), &PerfToolComparerStr); ImQsort(_LabelsVisible.Data, num_visible_labels, sizeof(const char*), &PerfToolComparerStr); // _SrcData vector stores sorted raw entries of imgui_perflog.csv. Sorting is very important, // algorithm depends on data being correctly sorted. Sorting _SrcData is OK, because it is only // ever appended to and never written out to disk. Entries are sorted by multiple criteria, // in specified order: // 1. By branch name // 2. By build ID // 3. By run timestamp // 4. By test name // This results in a neatly partitioned dataset where similar data is grouped together and where perf test order // is consistent in all batches. Sorting by build ID _before_ timestamp is also important as we will be aggregating // entries by build ID instead of timestamp, when appropriate display mode is enabled. ImQsort(_SrcData.Data, _SrcData.Size, sizeof(ImGuiPerfToolEntry), &PerfToolComparerByEntryInfo); // Sort groups of entries into batches. const bool combine_by_build_info = _DisplayType == ImGuiPerfToolDisplayType_CombineByBuildInfo; _LabelBarCounts.Data.resize(0); // Process all batches. `entry` is always a first batch element (guaranteed by _SrcData being sorted by timestamp). // At the end of this loop we fast-forward until next batch (first entry having different batch id (which is a // timestamp or build info)). for (ImGuiPerfToolEntry* entry = _SrcData.begin(); entry < _SrcData.end();) { // Filtered out entries can be safely ignored. Note that entry++ does not follow logic of fast-forwarding to the // next batch, as found at the end of this loop. This is OK, because all entries belonging to a same batch will // also have same date. if ((_FilterDateFrom[0] && strcmp(entry->Date, _FilterDateFrom) < 0) || (_FilterDateTo[0] && strcmp(entry->Date, _FilterDateTo) > 0)) { entry++; continue; } _Batches.push_back(ImGuiPerfToolBatch()); ImGuiPerfToolBatch& batch = _Batches.back(); batch.BatchID = GetBatchID(this, entry); batch.Entries.resize(num_visible_labels); // Fill in defaults. Done once before data aggregation loop, because same entry may be touched multiple times in // the following loop when entries are being combined by build info. for (int i = 0; i < num_visible_labels; i++) { ImGuiPerfToolEntry* e = &batch.Entries.Data[i]; *e = *entry; e->DtDeltaMs = 0; e->NumSamples = 0; e->LabelIndex = i; e->TestName = _LabelsVisible.Data[i]; } // Find perf test runs for this particular batch and accumulate them. for (int i = 0; i < num_visible_labels; i++) { // This inner loop walks all entries that belong to current batch. Due to sorting we are sure that batch // always starts with `entry`, and all entries that belong to a batch (whether we combine by build info or not) // will be grouped in _SrcData. ImGuiPerfToolEntry* aggregate = &batch.Entries.Data[i]; for (ImGuiPerfToolEntry* e = entry; e < _SrcData.end() && GetBatchID(this, e) == batch.BatchID; e++) { if (strcmp(e->TestName, aggregate->TestName) != 0) continue; aggregate->DtDeltaMs += e->DtDeltaMs; aggregate->NumSamples++; aggregate->DtDeltaMsMin = ImMin(aggregate->DtDeltaMsMin, e->DtDeltaMs); aggregate->DtDeltaMsMax = ImMax(aggregate->DtDeltaMsMax, e->DtDeltaMs); } } // In case data is combined by build info, DtDeltaMs will be a sum of all combined entries. Average it out. if (combine_by_build_info) for (int i = 0; i < num_visible_labels; i++) { ImGuiPerfToolEntry* aggregate = &batch.Entries.Data[i]; if (aggregate->NumSamples > 0) aggregate->DtDeltaMs /= aggregate->NumSamples; } // Advance to the next batch. batch.NumSamples = 1; if (combine_by_build_info) { ImU64 last_timestamp = entry->Timestamp; for (ImGuiID build_id = GetBuildID(entry); entry < _SrcData.end() && build_id == GetBuildID(entry);) { // Also count how many unique batches participate in this aggregated batch. if (entry->Timestamp != last_timestamp) { batch.NumSamples++; last_timestamp = entry->Timestamp; } entry++; } } else { for (ImU64 timestamp = entry->Timestamp; entry < _SrcData.end() && timestamp == entry->Timestamp;) entry++; } } // Create man entries for every batch. // Pushed after sorting so they are always at the start of the chart. const char* mean_labels[] = { "harmonic mean", "arithmetic mean", "geometric mean" }; int num_visible_mean_labels = 0; for (const char* label : mean_labels) { _Labels.push_back(label); if (_IsVisibleTest(label)) { _LabelsVisible.push_back(label); num_visible_mean_labels++; } } for (ImGuiPerfToolBatch& batch : _Batches) { double delta_sum = 0.0; double delta_prd = 1.0; double delta_rec = 0.0; for (int i = 0; i < batch.Entries.Size; i++) { ImGuiPerfToolEntry* entry = &batch.Entries.Data[i]; delta_sum += entry->DtDeltaMs; delta_prd *= entry->DtDeltaMs; delta_rec += 1 / entry->DtDeltaMs; } int visible_label_i = 0; for (int i = 0; i < IM_ARRAYSIZE(mean_labels); i++) { if (!_IsVisibleTest(mean_labels[i])) continue; batch.Entries.push_back(ImGuiPerfToolEntry()); ImGuiPerfToolEntry* mean_entry = &batch.Entries.back(); *mean_entry = batch.Entries.Data[0]; mean_entry->LabelIndex = _LabelsVisible.Size - num_visible_mean_labels + visible_label_i; mean_entry->TestName = _LabelsVisible.Data[mean_entry->LabelIndex]; mean_entry->GitBranchName = ""; mean_entry->BuildType = ""; mean_entry->Compiler = ""; mean_entry->OS = ""; mean_entry->Cpu = ""; mean_entry->Date = ""; visible_label_i++; if (i == 0) mean_entry->DtDeltaMs = num_visible_labels / delta_rec; else if (i == 1) mean_entry->DtDeltaMs = delta_sum / num_visible_labels; else if (i == 2) mean_entry->DtDeltaMs = pow(delta_prd, 1.0 / num_visible_labels); else IM_ASSERT(0); } IM_ASSERT(batch.Entries.Size == _LabelsVisible.Size); } // Find number of bars (batches) each label will render. for (ImGuiPerfToolBatch& batch : _Batches) { if (!_IsVisibleBuild(&batch)) continue; for (ImGuiPerfToolEntry& entry : batch.Entries) { ImGuiID label_id = ImHashStr(entry.TestName); int num_bars = _LabelBarCounts.GetInt(label_id) + 1; _LabelBarCounts.SetInt(label_id, num_bars); } } // Index branches, used for per-branch colors. temp_set.Data.resize(0); // ImHashStr(branch_name):linear_index int branch_index_last = 0; _BaselineBatchIndex = -1; for (ImGuiPerfToolBatch& batch : _Batches) { if (batch.Entries.empty()) continue; ImGuiPerfToolEntry* entry = &batch.Entries.Data[0]; ImGuiID branch_hash = ImHashStr(entry->GitBranchName); batch.BranchIndex = temp_set.GetInt(branch_hash, -1); if (batch.BranchIndex < 0) { batch.BranchIndex = branch_index_last++; temp_set.SetInt(branch_hash, batch.BranchIndex); } if (_BaselineBatchIndex < 0) if ((combine_by_build_info && GetBuildID(entry) == _BaselineBuildId) || _BaselineTimestamp == entry->Timestamp) _BaselineBatchIndex = _Batches.index_from_ptr(&batch); } // When per-branch colors are enabled we aggregate sample counts and set them to all batches with identical build info. temp_set.Data.resize(0); // build_id:TotalSamples if (_DisplayType == ImGuiPerfToolDisplayType_PerBranchColors) { // Aggregate totals to temp_set. for (ImGuiPerfToolBatch& batch : _Batches) { ImGuiID build_id = GetBuildID(&batch); temp_set.SetInt(build_id, temp_set.GetInt(build_id, 0) + batch.NumSamples); } // Fill in batch sample counts. for (ImGuiPerfToolBatch& batch : _Batches) { ImGuiID build_id = GetBuildID(&batch); batch.NumSamples = temp_set.GetInt(build_id, 1); } } _NumVisibleBuilds = PerfToolCountBuilds(this, true); _NumUniqueBuilds = PerfToolCountBuilds(this, false); _CalculateLegendAlignment(); temp_set.Data.resize(0); } void ImGuiPerfTool::Clear() { _Labels.clear(); _LabelsVisible.clear(); _Batches.clear_destruct(); _Visibility.Clear(); _SrcData.clear_destruct(); _CsvParser->Clear(); ImStrncpy(_FilterDateFrom, "9999-99-99", IM_ARRAYSIZE(_FilterDateFrom)); ImStrncpy(_FilterDateTo, "0000-00-00", IM_ARRAYSIZE(_FilterDateFrom)); } bool ImGuiPerfTool::LoadCSV(const char* filename) { if (filename == NULL) filename = IMGUI_PERFLOG_DEFAULT_FILENAME; Clear(); ImGuiCsvParser* parser = _CsvParser; parser->Columns = 11; if (!parser->Load(filename)) return false; // Read perf test entries from CSV for (int row = 0; row < parser->Rows; row++) { ImGuiPerfToolEntry entry; int col = 0; sscanf(parser->GetCell(row, col++), "%llu", &entry.Timestamp); entry.Category = parser->GetCell(row, col++); entry.TestName = parser->GetCell(row, col++); sscanf(parser->GetCell(row, col++), "%lf", &entry.DtDeltaMs); sscanf(parser->GetCell(row, col++), "x%d", &entry.PerfStressAmount); entry.GitBranchName = parser->GetCell(row, col++); entry.BuildType = parser->GetCell(row, col++); entry.Cpu = parser->GetCell(row, col++); entry.OS = parser->GetCell(row, col++); entry.Compiler = parser->GetCell(row, col++); entry.Date = parser->GetCell(row, col++); AddEntry(&entry); } return true; } void ImGuiPerfTool::ViewOnly(const char** perf_names) { // Data would not be built if we tried to view perftool of a particular test without first opening perftool via button. We need data to be built to hide perf tests. if (_Batches.empty()) _Rebuild(); // Hide other perf tests. for (const char* label : _Labels) { bool visible = false; for (const char** p_name = perf_names; !visible && *p_name; p_name++) visible |= strcmp(label, *p_name) == 0; _Visibility.SetBool(ImHashStr(label), visible); } } void ImGuiPerfTool::ViewOnly(const char* perf_name) { const char* names[] = { perf_name, NULL }; ViewOnly(names); } ImGuiPerfToolEntry* ImGuiPerfTool::GetEntryByBatchIdx(int idx, const char* perf_name) { if (idx < 0) return NULL; IM_ASSERT(idx < _Batches.Size); ImGuiPerfToolBatch& batch = _Batches.Data[idx]; for (int i = 0; i < batch.Entries.Size; i++) if (ImGuiPerfToolEntry* entry = &batch.Entries.Data[i]) if (strcmp(entry->TestName, perf_name) == 0) return entry; return NULL; } bool ImGuiPerfTool::_IsVisibleBuild(ImGuiPerfToolBatch* batch) { IM_ASSERT(batch != NULL); if (batch->Entries.empty()) return false; // All entries are hidden. return _IsVisibleBuild(&batch->Entries.Data[0]); } bool ImGuiPerfTool::_IsVisibleBuild(ImGuiPerfToolEntry* entry) { return _Visibility.GetBool(ImHashStr(entry->GitBranchName), true) && _Visibility.GetBool(ImHashStr(entry->Compiler), true) && _Visibility.GetBool(ImHashStr(entry->Cpu), true) && _Visibility.GetBool(ImHashStr(entry->OS), true) && _Visibility.GetBool(ImHashStr(entry->BuildType), true); } bool ImGuiPerfTool::_IsVisibleTest(const char* test_name) { return _Visibility.GetBool(ImHashStr(test_name), true); } void ImGuiPerfTool::_CalculateLegendAlignment() { // Estimate paddings for legend format so it looks nice and aligned // FIXME: Rely on font being monospace. May need to recalculate every frame on a per-need basis based on font? _AlignStress = _AlignType = _AlignCpu = _AlignOs = _AlignCompiler = _AlignBranch = _AlignSamples = 0; for (ImGuiPerfToolBatch& batch : _Batches) { if (batch.Entries.empty()) continue; ImGuiPerfToolEntry* entry = &batch.Entries.Data[0]; if (!_IsVisibleBuild(entry)) continue; _AlignStress = ImMax(_AlignStress, (int)ceil(log10(entry->PerfStressAmount))); _AlignType = ImMax(_AlignType, (int)strlen(entry->BuildType)); _AlignCpu = ImMax(_AlignCpu, (int)strlen(entry->Cpu)); _AlignOs = ImMax(_AlignOs, (int)strlen(entry->OS)); _AlignCompiler = ImMax(_AlignCompiler, (int)strlen(entry->Compiler)); _AlignBranch = ImMax(_AlignBranch, (int)strlen(entry->GitBranchName)); _AlignSamples = ImMax(_AlignSamples, (int)Str16f("%d", entry->NumSamples).length()); } } bool ImGuiPerfTool::SaveHtmlReport(const char* file_name, const char* image_file) { if (!ImFileCreateDirectoryChain(file_name, ImPathFindFilename(file_name))) return false; FILE* fp = fopen(file_name, "w+"); if (fp == NULL) return false; fprintf(fp, "\n" "\n" "\n" " \n" " Dear ImGui perf report\n" "\n" "\n" "
\n");

    // Embed performance chart.
    fprintf(fp, "## Dear ImGui perf report\n\n");

    if (image_file != NULL)
    {
        FILE* fp_img = fopen(image_file, "rb");
        if (fp_img != NULL)
        {
            ImVector image_buffer;
            ImVector base64_buffer;
            fseek(fp_img, 0, SEEK_END);
            image_buffer.resize((int)ftell(fp_img));
            base64_buffer.resize(((image_buffer.Size / 3) + 1) * 4 + 1);
            rewind(fp_img);
            fread(image_buffer.Data, 1, image_buffer.Size, fp_img);
            fclose(fp_img);
            int len = ImStrBase64Encode((unsigned char*)image_buffer.Data, base64_buffer.Data, image_buffer.Size);
            base64_buffer.Data[len] = 0;
            fprintf(fp, "![](data:image/png;base64,%s)\n\n", base64_buffer.Data);
        }
    }

    // Print info table.
    const bool combine_by_build_info = _DisplayType == ImGuiPerfToolDisplayType_CombineByBuildInfo;
    for (const auto& column_info : PerfToolColumnInfo)
        if (column_info.ShowAlways || combine_by_build_info)
            fprintf(fp, "| %s ", column_info.Title);
    fprintf(fp, "|\n");
    for (const auto& column_info : PerfToolColumnInfo)
        if (column_info.ShowAlways || combine_by_build_info)
            fprintf(fp, "| -- ");
    fprintf(fp, "|\n");

    for (int row_index = _InfoTableSort.Size - 1; row_index >= 0; row_index--)
    {
        int batch_index_sorted, entry_index_sorted;
        _UnpackSortedKey(_InfoTableSort[row_index], &batch_index_sorted, &entry_index_sorted);
        ImGuiPerfToolBatch* batch = &_Batches[batch_index_sorted];
        ImGuiPerfToolEntry* entry = &batch->Entries[entry_index_sorted];
        const char* test_name = entry->TestName;
        if (!_IsVisibleBuild(entry) || entry->NumSamples == 0)
            continue;

        ImGuiPerfToolEntry* baseline_entry = GetEntryByBatchIdx(_BaselineBatchIndex, test_name);
        for (int i = 0; i < IM_ARRAYSIZE(PerfToolColumnInfo); i++)
        {
            Str30f label("");
            const ImGuiPerfToolColumnInfo& column_info = PerfToolColumnInfo[i];
            if (column_info.ShowAlways || combine_by_build_info)
            {
                switch (i)
                {
                case 0:
                {
                    char date[64];
                    FormatDateAndTime(entry->Timestamp, date, IM_ARRAYSIZE(date));
                    fprintf(fp, "| %s ", date);
                    break;
                }
                case 1:  fprintf(fp, "| %s ", entry->TestName);             break;
                case 2:  fprintf(fp, "| %s ", entry->GitBranchName);        break;
                case 3:  fprintf(fp, "| %s ", entry->Compiler);             break;
                case 4:  fprintf(fp, "| %s ", entry->OS);                   break;
                case 5:  fprintf(fp, "| %s ", entry->Cpu);                  break;
                case 6:  fprintf(fp, "| %s ", entry->BuildType);            break;
                case 7:  fprintf(fp, "| x%d ", entry->PerfStressAmount);    break;
                case 8:  fprintf(fp, "| %.2f ", entry->DtDeltaMs);          break;
                case 9:  fprintf(fp, "| %.2f ", entry->DtDeltaMsMin);       break;
                case 10: fprintf(fp, "| %.2f ", entry->DtDeltaMsMax);       break;
                case 11: fprintf(fp, "| %d ", entry->NumSamples);           break;
                case 12: FormatVsBaseline(entry, baseline_entry, label); fprintf(fp, "| %s ", label.c_str()); break;
                default: IM_ASSERT(0); break;
                }
            }
        }
        fprintf(fp, "|\n");
    }

    fprintf(fp, "
\n" " \n" " \n" "\n" "\n"); fclose(fp); return true; } void ImGuiPerfTool::_SetBaseline(int batch_index) { IM_ASSERT(batch_index < _Batches.Size); _BaselineBatchIndex = batch_index; if (batch_index >= 0) { _BaselineTimestamp = _Batches.Data[batch_index].Entries.Data[0].Timestamp; _BaselineBuildId = GetBuildID(&_Batches.Data[batch_index]); } } //------------------------------------------------------------------------- // [SECTION] USER INTERFACE //------------------------------------------------------------------------- void ImGuiPerfTool::ShowPerfToolWindow(ImGuiTestEngine* engine, bool* p_open) { if (!ImGui::Begin("Dear ImGui Perf Tool", p_open)) { ImGui::End(); return; } if (ImGui::IsWindowAppearing() && Empty()) LoadCSV(); ImGuiStyle& style = ImGui::GetStyle(); // ----------------------------------------------------------------------------------------------------------------- // Render utility buttons // ----------------------------------------------------------------------------------------------------------------- // Date filter ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Date Range:"); ImGui::SameLine(); bool dirty = _Batches.empty(); bool date_changed = InputDate("##date-from", _FilterDateFrom, IM_ARRAYSIZE(_FilterDateFrom), (strcmp(_FilterDateFrom, _FilterDateTo) <= 0 || !*_FilterDateTo)); if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("InputDate From Menu"); ImGui::SameLine(0, 0.0f); ImGui::TextUnformatted(".."); ImGui::SameLine(0, 0.0f); date_changed |= InputDate("##date-to", _FilterDateTo, IM_ARRAYSIZE(_FilterDateTo), (strcmp(_FilterDateFrom, _FilterDateTo) <= 0 || !*_FilterDateFrom)); if (date_changed) { dirty = (!_FilterDateFrom[0] || IsDateValid(_FilterDateFrom)) && (!_FilterDateTo[0] || IsDateValid(_FilterDateTo)); if (_FilterDateFrom[0] && _FilterDateTo[0]) dirty &= strcmp(_FilterDateFrom, _FilterDateTo) <= 0; } if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("InputDate To Menu"); ImGui::SameLine(); for (int i = 0; i < 2; i++) { if (ImGui::BeginPopup(i == 0 ? "InputDate From Menu" : "InputDate To Menu")) { char* date = i == 0 ? _FilterDateFrom : _FilterDateTo; int date_size = i == 0 ? IM_ARRAYSIZE(_FilterDateFrom) : IM_ARRAYSIZE(_FilterDateTo); if (i == 0 && ImGui::MenuItem("Set Min")) { for (ImGuiPerfToolEntry& entry : _SrcData) if (strcmp(date, entry.Date) > 0) { ImStrncpy(date, entry.Date, date_size); dirty = true; } } if (ImGui::MenuItem("Set Max")) { for (ImGuiPerfToolEntry& entry : _SrcData) if (strcmp(date, entry.Date) < 0) { ImStrncpy(date, entry.Date, date_size); dirty = true; } } if (ImGui::MenuItem("Set Today")) { time_t now = time(NULL); FormatDate((ImU64)now * 1000000, date, date_size); dirty = true; } ImGui::EndPopup(); } } if (ImGui::Button(Str64f("Filter builds (%d/%d)###Filter builds", _NumVisibleBuilds, _NumUniqueBuilds).c_str())) ImGui::OpenPopup("Filter builds"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Hide or show individual builds."); ImGui::SameLine(); if (ImGui::Button(Str64f("Filter tests (%d/%d)###Filter tests", _LabelsVisible.Size, _Labels.Size).c_str())) ImGui::OpenPopup("Filter perfs"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Hide or show individual tests."); ImGui::SameLine(); dirty |= Button3("Combine", (int*)&_DisplayType); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::RadioButton("Display each run separately", _DisplayType == ImGuiPerfToolDisplayType_Simple); ImGui::RadioButton("Use one color per branch. Disables baseline comparisons!", _DisplayType == ImGuiPerfToolDisplayType_PerBranchColors); ImGui::RadioButton("Combine multiple runs with same build info into one averaged build entry.", _DisplayType == ImGuiPerfToolDisplayType_CombineByBuildInfo); ImGui::EndTooltip(); } ImGui::SameLine(); if (_ReportGenerating && ImGuiTestEngine_IsTestQueueEmpty(engine)) { _ReportGenerating = false; ImOsOpenInShell(PerfToolReportDefaultOutputPath); } if (_Batches.empty()) ImGui::BeginDisabled(); if (ImGui::Button("Html Export")) { // In order to capture a screenshot Report is saved by executing a "capture_perf_report" test. _ReportGenerating = true; ImGuiTestEngine_QueueTests(engine, ImGuiTestGroup_Tests, "capture_perf_report"); } if (_Batches.empty()) ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Generate a report and open it in the browser."); // Align help button to the right. float help_pos = ImGui::GetWindowContentRegionMax().x - style.FramePadding.x * 2 - ImGui::CalcTextSize("(?)").x; if (help_pos > ImGui::GetCursorPosX()) ImGui::SetCursorPosX(help_pos); ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::BulletText("To change baseline build, double-click desired build in the legend."); ImGui::BulletText("Extra information is displayed when hovering bars of a particular perf test and holding SHIFT."); ImGui::BulletText("Double-click plot to fit plot into available area."); ImGui::EndTooltip(); } if (ImGui::BeginPopup("Filter builds")) { ImGuiStorage& temp_set = _TempSet; temp_set.Data.resize(0); // ImHashStr(BuildProperty):seen static const char* columns[] = { "Branch", "Build", "CPU", "OS", "Compiler" }; bool show_all = ImGui::Button("Show All"); ImGui::SameLine(); bool hide_all = ImGui::Button("Hide All"); if (ImGui::BeginTable("Builds", IM_ARRAYSIZE(columns), ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { for (int i = 0; i < IM_ARRAYSIZE(columns); i++) ImGui::TableSetupColumn(columns[i]); ImGui::TableHeadersRow(); // Find columns with nothing checked. bool checked_any[] = { false, false, false, false, false }; for (ImGuiPerfToolEntry& entry : _SrcData) { const char* properties[] = { entry.GitBranchName, entry.BuildType, entry.Cpu, entry.OS, entry.Compiler }; for (int i = 0; i < IM_ARRAYSIZE(properties); i++) { ImGuiID hash = ImHashStr(properties[i]); checked_any[i] |= _Visibility.GetBool(hash, true); } } int property_offsets[] = { offsetof(ImGuiPerfToolEntry, GitBranchName), offsetof(ImGuiPerfToolEntry, BuildType), offsetof(ImGuiPerfToolEntry, Cpu), offsetof(ImGuiPerfToolEntry, OS), offsetof(ImGuiPerfToolEntry, Compiler), }; ImGui::TableNextRow(); for (int i = 0; i < IM_ARRAYSIZE(property_offsets); i++) { ImGui::TableSetColumnIndex(i); for (ImGuiPerfToolEntry& entry : _SrcData) { const char* property = *(const char**)((const char*)&entry + property_offsets[i]); ImGuiID hash = ImHashStr(property); if (temp_set.GetBool(hash)) continue; temp_set.SetBool(hash, true); bool visible = _Visibility.GetBool(hash, true) || show_all; if (hide_all) visible = false; bool modified = ImGui::Checkbox(property, &visible) || show_all || hide_all; _Visibility.SetBool(hash, visible); if (modified) { _CalculateLegendAlignment(); _NumVisibleBuilds = PerfToolCountBuilds(this, true); dirty = true; } if (!checked_any[i]) { ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ImColor(1.0f, 0.0f, 0.0f, 0.2f)); if (ImGui::TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) ImGui::SetTooltip("Check at least one item in each column to see any data."); } } } ImGui::EndTable(); } ImGui::EndPopup(); } if (ImGui::BeginPopup("Filter perfs")) { dirty |= RenderMultiSelectFilter(this, "Filter by perf test", &_Labels); if (ImGui::IsKeyPressed(ImGuiKey_Escape)) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); } if (dirty) _Rebuild(); // Rendering a plot of empty dataset is not possible. if (_Batches.empty() || _LabelsVisible.Size == 0 || _NumVisibleBuilds == 0) { ImGui::TextUnformatted("No data is available. Run some perf tests or adjust filter settings."); } else { #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT // Splitter between two following child windows is rendered first. float plot_height = 0.0f; float& table_height = _InfoTableHeight; ImGui::Splitter("splitter", &plot_height, &table_height, ImGuiAxis_Y, +1); // Double-click to move splitter to bottom if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { table_height = 0; plot_height = ImGui::GetContentRegionAvail().y - style.ItemSpacing.y; ImGui::ClearActiveID(); } // Render entries plot if (ImGui::BeginChild(ImGui::GetID("plot"), ImVec2(0, plot_height))) _ShowEntriesPlot(); ImGui::EndChild(); // Render entries tables if (table_height > 0.0f) { if (ImGui::BeginChild(ImGui::GetID("info-table"), ImVec2(0, table_height))) _ShowEntriesTable(); ImGui::EndChild(); } #else _ShowEntriesTable(); #endif } ImGui::End(); } #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT static double GetLabelVerticalOffset(double occupy_h, int max_visible_builds, int now_visible_builds) { const double h = occupy_h / (float)max_visible_builds; double offset = -h * ((max_visible_builds - 1) * 0.5); return (double)now_visible_builds * h + offset; } #endif void ImGuiPerfTool::_ShowEntriesPlot() { #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT ImGuiIO& io = ImGui::GetIO(); ImGuiStyle& style = ImGui::GetStyle(); Str256 label; Str256 display_label; ImPlot::PushStyleColor(ImPlotCol_AxisBgHovered, IM_COL32(0, 0, 0, 0)); ImPlot::PushStyleColor(ImPlotCol_AxisBgActive, IM_COL32(0, 0, 0, 0)); if (!ImPlot::BeginPlot("PerfTool", ImVec2(-1, -1), ImPlotFlags_NoTitle)) return; ImPlot::SetupAxis(ImAxis_X1, NULL, ImPlotAxisFlags_NoTickLabels); if (_LabelsVisible.Size > 1) { ImPlot::SetupAxisTicks(ImAxis_Y1, 0, _LabelsVisible.Size, _LabelsVisible.Size, _LabelsVisible.Data); } else if (_LabelsVisible.Size == 1) { const char* labels[] = { _LabelsVisible[0], "" }; ImPlot::SetupAxisTicks(ImAxis_Y1, 0, _LabelsVisible.Size, 2, labels); } ImPlot::SetupLegend(ImPlotLocation_NorthEast); // Amount of vertical space bars of one label will occupy. 1.0 would leave no space between bars of adjacent labels. const float occupy_h = 0.8f; // Plot bars bool legend_hovered = false; ImGuiStorage& temp_set = _TempSet; temp_set.Data.resize(0); // ImHashStr(TestName):now_visible_builds_i int current_baseline_batch_index = _BaselineBatchIndex; // Cache this value before loop, so toggling it does not create flicker. for (int batch_index = 0; batch_index < _Batches.Size; batch_index++) { ImGuiPerfToolBatch& batch = _Batches[batch_index]; if (!_IsVisibleBuild(&batch.Entries.Data[0])) continue; // Plot bars. label.clear(); display_label.clear(); PerfToolFormatBuildInfo(this, &label, &batch); display_label.append(label.c_str()); ImGuiID batch_label_id; bool baseline_match = false; if (_DisplayType == ImGuiPerfToolDisplayType_PerBranchColors) { // No "vs baseline" comparison for per-branch colors, because runs are combined in the legend, but not in the info table. batch_label_id = GetBuildID(&batch); } else { batch_label_id = ImHashData(&batch.BatchID, sizeof(batch.BatchID)); baseline_match = current_baseline_batch_index == batch_index; } display_label.appendf("%s###%08X", baseline_match ? " *" : "", batch_label_id); // Plot all bars one by one, so batches with varying number of bars would not contain empty holes. for (ImGuiPerfToolEntry& entry : batch.Entries) { if (entry.NumSamples == 0) continue; // Dummy entry, perf did not run for this test in this batch. ImGuiID label_id = ImHashStr(entry.TestName); const int max_visible_builds = _LabelBarCounts.GetInt(label_id); const int now_visible_builds = temp_set.GetInt(label_id); temp_set.SetInt(label_id, now_visible_builds + 1); double y_pos = (double)entry.LabelIndex + GetLabelVerticalOffset(occupy_h, max_visible_builds, now_visible_builds); ImPlot::SetNextFillStyle(ImPlot::GetColormapColor(_DisplayType == ImGuiPerfToolDisplayType_PerBranchColors ? batch.BranchIndex : batch_index)); ImPlot::PlotBars(display_label.c_str(), &entry.DtDeltaMs, &y_pos, 1, occupy_h / (double)max_visible_builds, ImPlotBarsFlags_Horizontal); } legend_hovered |= ImPlot::IsLegendEntryHovered(display_label.c_str()); // Set baseline. if (ImPlot::IsLegendEntryHovered(display_label.c_str())) { if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) _SetBaseline(batch_index); } } // Plot highlights. ImPlotContext& gp = *GImPlot; ImPlotPlot& plot = *gp.CurrentPlot; _PlotHoverTest = -1; _PlotHoverBatch = -1; _PlotHoverTestLabel = false; bool can_highlight = !legend_hovered && (ImPlot::IsPlotHovered() || ImPlot::IsAxisHovered(ImAxis_Y1)); ImDrawList* plot_draw_list = ImPlot::GetPlotDrawList(); // Highlight bars when hovering a label. int hovered_label_index = -1; for (int i = 0; i < _LabelsVisible.Size && can_highlight; i++) { ImRect label_rect_loose = ImPlotGetYTickRect(i); // Rect around test label ImRect label_rect_tight; // Rect around test label, covering bar height and label area width label_rect_tight.Min.y = ImPlot::PlotToPixels(0, (float)i + 0.5f).y; label_rect_tight.Max.y = ImPlot::PlotToPixels(0, (float)i - 0.5f).y; label_rect_tight.Min.x = plot.CanvasRect.Min.x; label_rect_tight.Max.x = plot.PlotRect.Min.x; ImRect rect_bars; // Rect around bars only rect_bars.Min.x = plot.PlotRect.Min.x; rect_bars.Max.x = plot.PlotRect.Max.x; rect_bars.Min.y = ImPlot::PlotToPixels(0, (float)i + 0.5f).y; rect_bars.Max.y = ImPlot::PlotToPixels(0, (float)i - 0.5f).y; // Render underline signaling it is clickable. Clicks are handled when rendering info table. if (label_rect_loose.Contains(io.MousePos)) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); plot_draw_list->AddLine(ImFloor(label_rect_loose.GetBL()), ImFloor(label_rect_loose.GetBR()), ImColor(style.Colors[ImGuiCol_Text])); } // Highlight bars belonging to hovered label. if (label_rect_tight.Contains(io.MousePos)) { plot_draw_list->AddRectFilled(rect_bars.Min, rect_bars.Max, ImColor(style.Colors[ImGuiCol_TextSelectedBg])); _PlotHoverTestLabel = true; _PlotHoverTest = i; } if (rect_bars.Contains(io.MousePos)) hovered_label_index = i; } // Highlight individual bars when hovering them on the plot or info table. temp_set.Data.resize(0); // ImHashStr(hovered_label):now_visible_builds_i if (hovered_label_index < 0) hovered_label_index = _TableHoveredTest; if (hovered_label_index >= 0) { const char* hovered_label = _LabelsVisible.Data[hovered_label_index]; ImGuiID label_id = ImHashStr(hovered_label); for (ImGuiPerfToolBatch& batch : _Batches) { int batch_index = _Batches.index_from_ptr(&batch); if (!_IsVisibleBuild(&batch)) continue; ImGuiPerfToolEntry* entry = &batch.Entries.Data[hovered_label_index]; if (entry->NumSamples == 0) continue; // Dummy entry, perf did not run for this test in this batch. int max_visible_builds = _LabelBarCounts.GetInt(label_id); const int now_visible_builds = temp_set.GetInt(label_id); temp_set.SetInt(label_id, now_visible_builds + 1); float h = occupy_h / (float)max_visible_builds; float y_pos = (float)entry->LabelIndex; y_pos += (float)GetLabelVerticalOffset(occupy_h, max_visible_builds, now_visible_builds); ImRect rect_bar; // Rect around hovered bar only rect_bar.Min.x = plot.PlotRect.Min.x; rect_bar.Max.x = plot.PlotRect.Max.x; rect_bar.Min.y = ImPlot::PlotToPixels(0, y_pos - h * 0.5f + h).y; // ImPlot y_pos is for bar center, therefore we adjust positions by half-height to get a bounding box. rect_bar.Max.y = ImPlot::PlotToPixels(0, y_pos - h * 0.5f).y; // Mouse is hovering label or bars of a perf test - highlight them in info table. if (_PlotHoverTest < 0 && rect_bar.Min.y <= io.MousePos.y && io.MousePos.y < rect_bar.Max.y && io.MousePos.x > plot.PlotRect.Min.x) { // _LabelsVisible is inverted to make perf test order match info table order. Revert it back. _PlotHoverTest = hovered_label_index; _PlotHoverBatch = batch_index; plot_draw_list->AddRectFilled(rect_bar.Min, rect_bar.Max, ImColor(style.Colors[ImGuiCol_TextSelectedBg])); } // Mouse is hovering a row in info table - highlight relevant bars on the plot. if (_TableHoveredBatch == batch_index && _TableHoveredTest == hovered_label_index) plot_draw_list->AddRectFilled(rect_bar.Min, rect_bar.Max, ImColor(style.Colors[ImGuiCol_TextSelectedBg])); } } if (io.KeyShift && _PlotHoverTest >= 0) { // Info tooltip with delta times of each batch for a hovered test. const char* test_name = _LabelsVisible.Data[_PlotHoverTest]; ImGui::BeginTooltip(); float w = ImGui::CalcTextSize(test_name).x; float total_w = ImGui::GetContentRegionAvail().x; if (total_w > w) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (total_w - w) * 0.5f); ImGui::TextUnformatted(test_name); for (int i = 0; i < _Batches.Size; i++) { if (ImGuiPerfToolEntry* hovered_entry = GetEntryByBatchIdx(i, test_name)) ImGui::Text("%s %.3fms", label.c_str(), hovered_entry->DtDeltaMs); else ImGui::Text("%s --", label.c_str()); } ImGui::EndTooltip(); } ImPlot::EndPlot(); ImPlot::PopStyleColor(2); #else ImGui::TextUnformatted("Not enabled because ImPlot is not available (IMGUI_TEST_ENGINE_ENABLE_IMPLOT=0)."); #endif } void ImGuiPerfTool::_ShowEntriesTable() { ImGuiTableFlags table_flags = ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders | ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | ImGuiTableFlags_SortTristate | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY; if (!ImGui::BeginTable("PerfInfo", IM_ARRAYSIZE(PerfToolColumnInfo), table_flags)) return; ImGuiStyle& style = ImGui::GetStyle(); int num_visible_labels = _LabelsVisible.Size; // Test name column is not sorted because we do sorting only within perf runs of a particular tests, // so as far as sorting function is concerned all items in first column are identical. for (int i = 0; i < IM_ARRAYSIZE(PerfToolColumnInfo); i++) { const ImGuiPerfToolColumnInfo& info = PerfToolColumnInfo[i]; ImGuiTableColumnFlags column_flags = info.Flags; if (i == 0 && _DisplayType != ImGuiPerfToolDisplayType_Simple) column_flags |= ImGuiTableColumnFlags_Disabled; // Date only visible in non-combining mode. if (!info.ShowAlways && _DisplayType != ImGuiPerfToolDisplayType_CombineByBuildInfo) column_flags |= ImGuiTableColumnFlags_Disabled; ImGui::TableSetupColumn(info.Title, column_flags); } ImGui::TableSetupScrollFreeze(0, 1); if (ImGuiTableSortSpecs* sorts_specs = ImGui::TableGetSortSpecs()) if (sorts_specs->SpecsDirty || _InfoTableSortDirty) { // Fill sort table with unsorted indices. sorts_specs->SpecsDirty = _InfoTableSortDirty = false; // Reinitialize sorting table to unsorted state. _InfoTableSort.resize(num_visible_labels * _Batches.Size); for (int entry_index = 0, i = 0; entry_index < num_visible_labels; entry_index++) for (int batch_index = 0; batch_index < _Batches.Size; batch_index++, i++) _InfoTableSort.Data[i] = (((ImU64)batch_index * num_visible_labels + entry_index) << 24) | i; // Sort batches of each label. if (sorts_specs->SpecsCount > 0) { _InfoTableSortSpecs = sorts_specs; PerfToolInstance = this; ImQsort(_InfoTableSort.Data, (size_t)_InfoTableSort.Size, sizeof(_InfoTableSort.Data[0]), CompareWithSortSpecs); _InfoTableSortSpecs = NULL; PerfToolInstance = NULL; } } ImGui::TableHeadersRow(); // ImPlot renders bars from bottom to the top. We want bars to render from top to the bottom, therefore we loop // labels and batches in reverse order. _TableHoveredTest = -1; _TableHoveredBatch = -1; const bool scroll_into_view = _PlotHoverTestLabel && ImGui::IsMouseClicked(ImGuiMouseButton_Left); const float header_row_height = ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).GetHeight(); ImRect scroll_into_view_rect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); for (int row_index = _InfoTableSort.Size - 1; row_index >= 0; row_index--) { int batch_index_sorted, entry_index_sorted; _UnpackSortedKey(_InfoTableSort[row_index], &batch_index_sorted, &entry_index_sorted); ImGuiPerfToolBatch* batch = &_Batches[batch_index_sorted]; ImGuiPerfToolEntry* entry = &batch->Entries[entry_index_sorted]; const char* test_name = entry->TestName; if (!_IsVisibleBuild(entry) || !_IsVisibleTest(entry->TestName) || entry->NumSamples == 0) continue; ImGui::PushID(entry); ImGui::TableNextRow(); if (row_index & 1) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_TableRowBgAlt, 0.5f)); else ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_TableRowBg, 0.5f)); if (_PlotHoverTest == entry_index_sorted) { // Highlight a row that corresponds to hovered bar, or all rows that correspond to hovered perf test label. if (_PlotHoverBatch == batch_index_sorted || _PlotHoverTestLabel) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImColor(style.Colors[ImGuiCol_TextSelectedBg])); } ImGuiPerfToolEntry* baseline_entry = GetEntryByBatchIdx(_BaselineBatchIndex, test_name); // Date if (ImGui::TableNextColumn()) { char date[64]; FormatDateAndTime(entry->Timestamp, date, IM_ARRAYSIZE(date)); ImGui::TextUnformatted(date); } // Build info if (ImGui::TableNextColumn()) { // ImGuiSelectableFlags_Disabled + changing ImGuiCol_TextDisabled color prevents selectable from overriding table highlight behavior. ImGui::PushStyleColor(ImGuiCol_Header, style.Colors[ImGuiCol_Text]); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, style.Colors[ImGuiCol_TextSelectedBg]); ImGui::PushStyleColor(ImGuiCol_HeaderActive, style.Colors[ImGuiCol_TextSelectedBg]); ImGui::Selectable(entry->TestName, false, ImGuiSelectableFlags_SpanAllColumns); ImGui::PopStyleColor(3); if (ImGui::IsItemHovered()) { _TableHoveredTest = entry_index_sorted; _TableHoveredBatch = batch_index_sorted; } if (ImGui::BeginPopupContextItem()) { if (entry == baseline_entry) ImGui::BeginDisabled(); if (ImGui::MenuItem("Set as baseline")) _SetBaseline(batch_index_sorted); if (entry == baseline_entry) ImGui::EndDisabled(); ImGui::EndPopup(); } } if (ImGui::TableNextColumn()) ImGui::TextUnformatted(entry->GitBranchName); if (ImGui::TableNextColumn()) ImGui::TextUnformatted(entry->Compiler); if (ImGui::TableNextColumn()) ImGui::TextUnformatted(entry->OS); if (ImGui::TableNextColumn()) ImGui::TextUnformatted(entry->Cpu); if (ImGui::TableNextColumn()) ImGui::TextUnformatted(entry->BuildType); if (ImGui::TableNextColumn()) ImGui::Text("x%d", entry->PerfStressAmount); // Avg ms if (ImGui::TableNextColumn()) ImGui::Text("%.3lf", entry->DtDeltaMs); // Min ms if (ImGui::TableNextColumn()) ImGui::Text("%.3lf", entry->DtDeltaMsMin); // Max ms if (ImGui::TableNextColumn()) ImGui::Text("%.3lf", entry->DtDeltaMsMax); // Num samples if (ImGui::TableNextColumn()) ImGui::Text("%d", entry->NumSamples); // VS Baseline if (ImGui::TableNextColumn()) { float dt_change = (float)entry->VsBaseline; if (_DisplayType == ImGuiPerfToolDisplayType_PerBranchColors) { ImGui::TextUnformatted("--"); } else { Str30 label; dt_change = FormatVsBaseline(entry, baseline_entry, label); ImGui::TextUnformatted(label.c_str()); if (dt_change != entry->VsBaseline) { entry->VsBaseline = dt_change; _InfoTableSortDirty = true; // Force re-sorting. } } } if (_PlotHoverTest == entry_index_sorted && scroll_into_view) { ImGuiTable* table = ImGui::GetCurrentTable(); scroll_into_view_rect.Add(ImGui::TableGetCellBgRect(table, 0)); } ImGui::PopID(); } if (scroll_into_view) { scroll_into_view_rect.Min.y -= header_row_height; // FIXME-TABLE: Compensate for frozen header row covering a first content row scrolled into view. ImGui::ScrollToRect(ImGui::GetCurrentWindow(), scroll_into_view_rect, ImGuiScrollFlags_NoScrollParent); } ImGui::EndTable(); } //------------------------------------------------------------------------- // [SECTION] SETTINGS //------------------------------------------------------------------------- static void PerflogSettingsHandler_ClearAll(ImGuiContext*, ImGuiSettingsHandler* ini_handler) { ImGuiPerfTool* perftool = (ImGuiPerfTool*)ini_handler->UserData; perftool->_Visibility.Clear(); } static void* PerflogSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char*) { return (void*)1; } static void PerflogSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler* ini_handler, void*, const char* line) { ImGuiPerfTool* perftool = (ImGuiPerfTool*)ini_handler->UserData; char buf[128]; int visible = -1, display_type = -1; /**/ if (sscanf(line, "DateFrom=%10s", perftool->_FilterDateFrom)) {} else if (sscanf(line, "DateTo=%10s", perftool->_FilterDateTo)) {} else if (sscanf(line, "DisplayType=%d", &display_type)) { perftool->_DisplayType = (ImGuiPerfToolDisplayType)display_type; } else if (sscanf(line, "BaselineBuildId=%llu", &perftool->_BaselineBuildId)) {} else if (sscanf(line, "BaselineTimestamp=%llu", &perftool->_BaselineTimestamp)) {} else if (sscanf(line, "TestVisibility=%[^,],%d", buf, &visible) == 2) { perftool->_Visibility.SetBool(ImHashStr(buf), !!visible); } else if (sscanf(line, "BuildVisibility=%[^,],%d", buf, &visible) == 2) { perftool->_Visibility.SetBool(ImHashStr(buf), !!visible); } } static void PerflogSettingsHandler_ApplyAll(ImGuiContext*, ImGuiSettingsHandler* ini_handler) { ImGuiPerfTool* perftool = (ImGuiPerfTool*)ini_handler->UserData; perftool->_Batches.clear_destruct(); perftool->_SetBaseline(-1); } static void PerflogSettingsHandler_WriteAll(ImGuiContext*, ImGuiSettingsHandler* ini_handler, ImGuiTextBuffer* buf) { ImGuiPerfTool* perftool = (ImGuiPerfTool*)ini_handler->UserData; if (perftool->_Batches.empty()) return; buf->appendf("[%s][Data]\n", ini_handler->TypeName); buf->appendf("DateFrom=%s\n", perftool->_FilterDateFrom); buf->appendf("DateTo=%s\n", perftool->_FilterDateTo); buf->appendf("DisplayType=%d\n", perftool->_DisplayType); buf->appendf("BaselineBuildId=%llu\n", perftool->_BaselineBuildId); buf->appendf("BaselineTimestamp=%llu\n", perftool->_BaselineTimestamp); for (const char* label : perftool->_Labels) buf->appendf("TestVisibility=%s,%d\n", label, perftool->_Visibility.GetBool(ImHashStr(label), true)); ImGuiStorage& temp_set = perftool->_TempSet; temp_set.Data.clear(); for (ImGuiPerfToolEntry& entry : perftool->_SrcData) { const char* properties[] = { entry.GitBranchName, entry.BuildType, entry.Cpu, entry.OS, entry.Compiler }; for (int i = 0; i < IM_ARRAYSIZE(properties); i++) { ImGuiID hash = ImHashStr(properties[i]); if (!temp_set.GetBool(hash)) { temp_set.SetBool(hash, true); buf->appendf("BuildVisibility=%s,%d\n", properties[i], perftool->_Visibility.GetBool(hash, true)); } } } buf->append("\n"); } void ImGuiPerfTool::_AddSettingsHandler() { ImGuiSettingsHandler ini_handler; ini_handler.TypeName = "TestEnginePerfTool"; ini_handler.TypeHash = ImHashStr("TestEnginePerfTool"); ini_handler.ClearAllFn = PerflogSettingsHandler_ClearAll; ini_handler.ReadOpenFn = PerflogSettingsHandler_ReadOpen; ini_handler.ReadLineFn = PerflogSettingsHandler_ReadLine; ini_handler.ApplyAllFn = PerflogSettingsHandler_ApplyAll; ini_handler.WriteAllFn = PerflogSettingsHandler_WriteAll; ini_handler.UserData = this; ImGui::AddSettingsHandler(&ini_handler); } void ImGuiPerfTool::_UnpackSortedKey(ImU64 key, int* batch_index, int* entry_index, int* monotonic_index) { IM_ASSERT(batch_index != NULL); IM_ASSERT(entry_index != NULL); const int num_visible_labels = _LabelsVisible.Size; *batch_index = (int)((key >> 24) / num_visible_labels); *entry_index = (int)((key >> 24) % num_visible_labels); if (monotonic_index) *monotonic_index = (int)(key & 0xFFFFFF); } //------------------------------------------------------------------------- // [SECTION] TESTS //------------------------------------------------------------------------- static bool SetPerfToolWindowOpen(ImGuiTestContext* ctx, bool is_open) { ctx->MenuClick("//Dear ImGui Test Engine/Tools"); bool was_open = ctx->ItemIsChecked("//##Menu_00/Perf Tool"); ctx->MenuAction(is_open ? ImGuiTestAction_Check : ImGuiTestAction_Uncheck, "//Dear ImGui Test Engine/Tools/Perf Tool"); return was_open; } void RegisterTests_TestEnginePerfTool(ImGuiTestEngine* e) { ImGuiTest* t = NULL; // ## Flex perf tool code. t = IM_REGISTER_TEST(e, "testengine", "testengine_cov_perftool"); t->GuiFunc = [](ImGuiTestContext* ctx) { IM_UNUSED(ctx); ImGui::Begin("Test Func", NULL, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize); int loop_count = 1000; bool v1 = false, v2 = true; for (int n = 0; n < loop_count / 2; n++) { ImGui::PushID(n); ImGui::Checkbox("Hello, world", &v1); ImGui::Checkbox("Hello, world", &v2); ImGui::PopID(); } ImGui::End(); }; t->TestFunc = [](ImGuiTestContext* ctx) { ImGuiPerfTool* perftool = ImGuiTestEngine_GetPerfTool(ctx->Engine); const char* temp_perf_csv = "output/misc_cov_perf_tool.csv"; Str16f min_date_bkp = perftool->_FilterDateFrom; Str16f max_date_bkp = perftool->_FilterDateTo; // Execute few perf tests, serialize them to temporary csv file. ctx->PerfIterations = 50; // Make faster ctx->PerfCapture("perf", "misc_cov_perf_tool_1", temp_perf_csv); ctx->PerfCapture("perf", "misc_cov_perf_tool_2", temp_perf_csv); // Load perf data from csv file and open perf tool. perftool->Clear(); perftool->LoadCSV(temp_perf_csv); bool perf_was_open = SetPerfToolWindowOpen(ctx, true); ctx->Yield(); ImGuiWindow* window = ctx->GetWindowByRef("Dear ImGui Perf Tool"); IM_CHECK(window != NULL); ImVec2 pos_bkp = window->Pos; ImVec2 size_bkp = window->Size; ctx->SetRef(window); ctx->WindowMove("", ImVec2(50, 50)); ctx->WindowResize("", ImVec2(1400, 900)); #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT ImGuiWindow* plot_child = ctx->WindowInfo("plot")->Window; // "plot/PerfTool" prior to implot 2023/08/21 IM_CHECK(plot_child != NULL); // Move legend to right side. ctx->MouseMoveToPos(plot_child->Rect().GetCenter()); ctx->MouseDoubleClick(ImGuiMouseButton_Left); // Auto-size plots while at it ctx->MouseClick(ImGuiMouseButton_Right); ctx->MenuClick("//$FOCUSED/Legend/NE"); // Click some stuff for more coverage. ctx->MouseMoveToPos(plot_child->Rect().GetCenter()); ctx->KeyPress(ImGuiMod_Shift); #endif ctx->ItemClick("##date-from", ImGuiMouseButton_Right); ctx->ItemClick(ctx->GetID("//$FOCUSED/Set Min")); ctx->ItemClick("##date-to", ImGuiMouseButton_Right); ctx->ItemClick(ctx->GetID("//$FOCUSED/Set Max")); ctx->ItemClick("###Filter builds"); ctx->ItemClick("###Filter tests"); ctx->ItemClick("Combine", 0, ImGuiTestOpFlags_MoveToEdgeL); // Toggle thrice to leave state unchanged ctx->ItemClick("Combine", 0, ImGuiTestOpFlags_MoveToEdgeL); ctx->ItemClick("Combine", 0, ImGuiTestOpFlags_MoveToEdgeL); // Restore original state. perftool->Clear(); // Clear test data and load original data ImFileDelete(temp_perf_csv); perftool->LoadCSV(); ctx->Yield(); #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT ctx->MouseMoveToPos(plot_child->Rect().GetCenter()); ctx->MouseDoubleClick(ImGuiMouseButton_Left); // Fit plot to original data #endif ImStrncpy(perftool->_FilterDateFrom, min_date_bkp.c_str(), IM_ARRAYSIZE(perftool->_FilterDateFrom)); ImStrncpy(perftool->_FilterDateTo, max_date_bkp.c_str(), IM_ARRAYSIZE(perftool->_FilterDateTo)); ImGui::SetWindowPos(window, pos_bkp); ImGui::SetWindowSize(window, size_bkp); SetPerfToolWindowOpen(ctx, perf_was_open); // Restore window visibility }; // ## Capture perf tool graph. t = IM_REGISTER_TEST(e, "capture", "capture_perf_report"); t->TestFunc = [](ImGuiTestContext* ctx) { ImGuiPerfTool* perftool = ImGuiTestEngine_GetPerfTool(ctx->Engine); const char* perf_report_image = NULL; if (!ImFileExist(IMGUI_PERFLOG_DEFAULT_FILENAME)) { ctx->LogWarning("Perf tool has no data. Perf report generation was aborted."); return; } char min_date_bkp[sizeof(perftool->_FilterDateFrom)], max_date_bkp[sizeof(perftool->_FilterDateTo)]; ImStrncpy(min_date_bkp, perftool->_FilterDateFrom, IM_ARRAYSIZE(min_date_bkp)); ImStrncpy(max_date_bkp, perftool->_FilterDateTo, IM_ARRAYSIZE(max_date_bkp)); bool perf_was_open = SetPerfToolWindowOpen(ctx, true); ctx->Yield(); ImGuiWindow* window = ctx->GetWindowByRef("Dear ImGui Perf Tool"); IM_CHECK_SILENT(window != NULL); ImVec2 pos_bkp = window->Pos; ImVec2 size_bkp = window->Size; ctx->SetRef(window); ctx->WindowMove("", ImVec2(50, 50)); ctx->WindowResize("", ImVec2(1400, 900)); #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT ctx->ItemDoubleClick("splitter"); // Hide info table ImGuiWindow* plot_child = ctx->WindowInfo("plot")->Window; // "plot/PerfTool" prior to implot 2023/08/21 IM_CHECK(plot_child != NULL); // Move legend to right side. ctx->MouseMoveToPos(plot_child->Rect().GetCenter()); ctx->MouseDoubleClick(ImGuiMouseButton_Left); // Auto-size plots while at it ctx->MouseClick(ImGuiMouseButton_Right); ctx->MenuClick("//$FOCUSED/Legend/NE"); #endif // Click some stuff for more coverage. ctx->ItemClick("##date-from", ImGuiMouseButton_Right); ctx->ItemClick(ctx->GetID("//$FOCUSED/Set Min")); ctx->ItemClick("##date-to", ImGuiMouseButton_Right); ctx->ItemClick(ctx->GetID("//$FOCUSED/Set Max")); #if IMGUI_TEST_ENGINE_ENABLE_IMPLOT // Take a screenshot. ImGuiCaptureArgs* args = ctx->CaptureArgs; args->InCaptureRect = plot_child->Rect(); ctx->CaptureAddWindow(window->Name); ctx->CaptureScreenshot(ImGuiCaptureFlags_HideMouseCursor); ctx->ItemDragWithDelta("splitter", ImVec2(0, -180)); // Show info table perf_report_image = args->InOutputFile; #endif ImStrncpy(perftool->_FilterDateFrom, min_date_bkp, IM_ARRAYSIZE(min_date_bkp)); ImStrncpy(perftool->_FilterDateTo, max_date_bkp, IM_ARRAYSIZE(max_date_bkp)); ImGui::SetWindowPos(window, pos_bkp); ImGui::SetWindowSize(window, size_bkp); SetPerfToolWindowOpen(ctx, perf_was_open); // Restore window visibility const char* perf_report_output = getenv("CAPTURE_PERF_REPORT_OUTPUT"); if (perf_report_output == NULL) perf_report_output = PerfToolReportDefaultOutputPath; perftool->SaveHtmlReport(perf_report_output, perf_report_image); }; } //-------------------------------------------------------------------------