From 963fae202108acd0498349e872e4811fa6c6aba0 Mon Sep 17 00:00:00 2001 From: Nic Gaffney Date: Wed, 12 Jun 2024 21:15:52 -0500 Subject: Added imgui for configuration --- .../libs/imgui_test_engine/imgui_te_perftool.cpp | 1949 ++++++++++++++++++++ 1 file changed, 1949 insertions(+) create mode 100644 vendor/zgui/libs/imgui_test_engine/imgui_te_perftool.cpp (limited to 'vendor/zgui/libs/imgui_test_engine/imgui_te_perftool.cpp') diff --git a/vendor/zgui/libs/imgui_test_engine/imgui_te_perftool.cpp b/vendor/zgui/libs/imgui_test_engine/imgui_te_perftool.cpp new file mode 100644 index 0000000..65b853a --- /dev/null +++ b/vendor/zgui/libs/imgui_test_engine/imgui_te_perftool.cpp @@ -0,0 +1,1949 @@ +// 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); + }; +} + +//------------------------------------------------------------------------- -- cgit v1.2.3