diff --git a/bench.hpp b/bench.hpp new file mode 100644 index 000000000000..5504177b306b --- /dev/null +++ b/bench.hpp @@ -0,0 +1,346 @@ +#pragma once +// +// bench.hpp: Dead-simple benchmark utility (header only) +// +// Pascal Thomet / 2024 +// Originally developed by +// for FunctionalPlus (https://github.com/Dobiasd/FunctionalPlus) +// +// +// Example usage: +// -------------- +// +//static BenchmarkSession gBenchmark; +//#include +// +// +//int main() +//{ +// // Create a random huge vector +// auto CreateRandomVector = [] +// { +// std::vector randomValues; +// for (int i = 0; i < 1000 * 1000; ++i) +// randomValues.push_back(rand() % 1000); +// return randomValues; +// }; +// +// // Run the benchmark several times +// for (int i = 0; i < 100; ++i) +// { +// // Create a new random vector each time +// auto randomValues = CreateRandomVector(); +// +// // Benchmark the sort function +// BENCHMARK_VOID_EXPRESSION(gBenchmark, "Sort", +// { +// std::sort(randomValues.begin(), randomValues.end()); +// }); +// } +// +// printf("Benchmark \n%s", gBenchmark.Report().c_str()); +//} + + + +// +// See public API at the end +// + +#include +#include +#include +#include +#include +#include + + +namespace Bench +{ + using ExecutionTime = double; + using FunctionName = std::string; + + + inline double Sum(const std::vector& xs) + { + double result = 0.0; + for (const double x : xs) + { + result += x; + } + return result; + } + + struct MeanAndStdDev + { + double Mean; + double StdDev; + }; + + inline MeanAndStdDev ComputeMeanStdDev(const std::vector& xs) + { + const double mean = Sum(xs) / static_cast(xs.size()); + double variance = 0.0; + for (const double x : xs) + { + variance += (x - mean) * (x - mean); + } + variance /= static_cast(xs.size()); + return {mean, std::sqrt(variance)}; + } + + + template + inline std::vector> MapToPairs(const std::map& m) + { + std::vector> result; + for (const auto& kv : m) + { + result.push_back(kv); + } + return result; + } + + template + inline std::vector SortBy(const CmpFunction& is_less, const std::vector& xs) + { + auto ys = xs; + std::sort(ys.begin(), ys.end(), is_less); + return ys; + } + + + class HighResTimer + { + public: + HighResTimer() : _StartTime(clock::now()) {} + void Reset() { _StartTime = clock::now(); } + + // time since creation or last reset in seconds + double Elapsed() const + { + return std::chrono::duration_cast(clock::now() - _StartTime).count(); + } + + private: + typedef std::chrono::high_resolution_clock clock; + typedef std::chrono::duration> second; + std::chrono::time_point _StartTime; + }; + + + struct BenchmarkFunctionReport + { + std::size_t NbCalls; + ExecutionTime TotalTime; + ExecutionTime AverageTime; + ExecutionTime Deviation; + }; + + namespace Internal + { + std::string ShowBenchmarkFunctionReport( + const std::map& reports); + } + + // BenchmarkSession stores timings during a benchmark session + // and is able to emit a report at the end + class BenchmarkSession + { + public: + BenchmarkSession() + : mFunctionTimesMutex() + , mFunctionTimes() {}; + + inline std::string Report() const + { + const auto reports = ReportList(); + return Bench::Internal::ShowBenchmarkFunctionReport(reports); + } + + std::map ReportList() const + { + std::lock_guard lock(mFunctionTimesMutex); + std::map report; + for (const auto& one_function_time : mFunctionTimes) { + report[one_function_time.first] = _MakeBenchReport(one_function_time.second); + } + return report; + } + + inline void StoreOneTime(const FunctionName& function_name, ExecutionTime time) + { + std::lock_guard lock(mFunctionTimesMutex); + mFunctionTimes[function_name].push_back(time); + } + + private: + BenchmarkFunctionReport _MakeBenchReport( + const std::vector& times) const + { + BenchmarkFunctionReport result; + result.NbCalls = times.size(); + auto mean_and_dev = Bench::ComputeMeanStdDev(times); + result.AverageTime = mean_and_dev.Mean; + result.Deviation = mean_and_dev.StdDev; + result.TotalTime = Bench::Sum(times); + return result; + } + + mutable std::mutex mFunctionTimesMutex; + std::map> mFunctionTimes; + }; + + namespace Internal + { + template + class BenchFunctionImpl + { + public: + explicit BenchFunctionImpl( + BenchmarkSession& benchmark_sess, + FunctionName function_name, + Fn fn) + : benchmark_session_(benchmark_sess) + , function_name_(function_name) + , fn_(fn) {}; + + template + auto operator()(Args&&... args) + { + return _BenchResult(std::forward(args)...); + } + + private: + template + auto _BenchResult(Args&&... args) + { + Bench::HighResTimer timer; + auto r = fn_(std::forward(args)...); + benchmark_session_.StoreOneTime(function_name_, timer.Elapsed()); + return r; + } + + BenchmarkSession& benchmark_session_; + FunctionName function_name_; + Fn fn_; + }; + + template + class BenchVoidFunctionImpl + { + public: + explicit BenchVoidFunctionImpl( + BenchmarkSession& benchmark_sess, + FunctionName function_name, + Fn fn) + : benchmark_session_(benchmark_sess) + , function_name_(function_name) + , fn_(fn) {}; + + template + auto operator()(Args&&... args) + { + _BenchResult(std::forward(args)...); + } + + private: + template + auto _BenchResult(Args&&... args) + { + Bench::HighResTimer timer; + fn_(std::forward(args)...); + benchmark_session_.StoreOneTime(function_name_, timer.Elapsed()); + } + + BenchmarkSession& benchmark_session_; + FunctionName function_name_; + Fn fn_; + }; + + } // namespace Internal + + + template + auto MakeBenchmarkFunction(BenchmarkSession& session, const FunctionName& name, Fn f) + { + // transforms f into a function with the same + // signature, that will store timings into the benchmark session + return Internal::BenchFunctionImpl(session, name, f); + } + + template + auto MakeBenchmarkVoidFunction(BenchmarkSession& session, const FunctionName& name, Fn f) + { + // transforms a void returning function into a function with the same + // signature, that will store timings into the benchmark session + return Internal::BenchVoidFunctionImpl(session, name, f); + } + + + namespace Internal + { + inline std::vector> make_ordered_reports( + const std::map& report_map) + { + auto report_pairs = Bench::MapToPairs(report_map); + auto fnCompareTotalTime = [](const auto& a, const auto& b) { + return a.second.TotalTime > b.second.TotalTime; + }; + auto report_pairs_sorted = Bench::SortBy(fnCompareTotalTime, report_pairs); + return report_pairs_sorted; + } + + inline std::string ShowBenchmarkFunctionReport(const std::map& reports) + { + auto ordered_reports = make_ordered_reports(reports); + + auto my_show_time_ms = [](double time) -> std::string + { + std::stringstream ss; + ss << std::fixed << std::setprecision(3); + ss << (time * 1000.); + return ss.str() + "ms"; + }; + + auto my_show_time_us = [](double time) -> std::string + { + std::stringstream ss; + ss << std::fixed << std::setprecision(3); + ss << (time * 1000000.); + return ss.str() + "us"; + }; + + std::stringstream ss; + for (const auto& kv: ordered_reports) + { + const auto& report = kv.second; + const auto& function_name = kv.first; + ss << function_name << " : " << report.NbCalls << " calls, " + << my_show_time_ms(report.TotalTime) << " total, " + << my_show_time_us(report.AverageTime) << " average, " + << my_show_time_us(report.Deviation) << " Deviation" << std::endl; + } + return ss.str(); + } + } // namespace Internal + +} // namespace Bench + + +// +// Public API +// +using Bench::BenchmarkSession; + +#define BENCHMARK_EXPRESSION(bench_session, name, expression) \ + MakeBenchmarkFunction( \ + bench_session, \ + name, \ + [&]() { return expression; })(); + +#define BENCHMARK_VOID_EXPRESSION(bench_session, name, expression) \ + MakeBenchmarkVoidFunction( \ + bench_session, \ + name, \ + [&]() { expression; })(); diff --git a/imgui.cpp b/imgui.cpp index 0293ea698500..144899df3d4b 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1238,6 +1238,14 @@ static ImGuiMemAllocFunc GImAllocatorAllocFunc = MallocWrapper; static ImGuiMemFreeFunc GImAllocatorFreeFunc = FreeWrapper; static void* GImAllocatorUserData = NULL; +// Optional benchmark for GetID_AssertUnique +// (See https://github.com/ocornut/imgui/issues/7669) +// #define BENCHMARK_GETID // uncomment to use this benchmark +#ifdef BENCHMARK_GETID +#include "bench.hpp" +BenchmarkSession gBenchmark; +#endif + //----------------------------------------------------------------------------- // [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO) //----------------------------------------------------------------------------- @@ -3752,6 +3760,10 @@ void ImGui::DestroyContext(ImGuiContext* ctx) Shutdown(); SetCurrentContext((prev_ctx != ctx) ? prev_ctx : NULL); IM_DELETE(ctx); + +#ifdef BENCHMARK_GETID + printf("%s", gBenchmark.Report().c_str()); +#endif } // IMPORTANT: ###xxx suffixes must be same in ALL languages @@ -8823,40 +8835,115 @@ bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) // it should ideally be flattened at some point but it's been used a lots by widgets. ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) { - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); -#ifndef IMGUI_DISABLE_DEBUG_TOOLS - ImGuiContext& g = *Ctx; - if (g.DebugHookIdInfo == id) - ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); + ImGuiID id; +#ifdef BENCHMARK_GETID + BENCHMARK_VOID_EXPRESSION(gBenchmark, "GetId(const char*)", + { +#endif + ImGuiID seed = IDStack.back(); + id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); + #ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext &g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); + #endif + +#ifdef BENCHMARK_GETID + }); #endif return id; } ImGuiID ImGuiWindow::GetID(const void* ptr) { - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); -#ifndef IMGUI_DISABLE_DEBUG_TOOLS - ImGuiContext& g = *Ctx; - if (g.DebugHookIdInfo == id) - ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL); + ImGuiID id; +#ifdef BENCHMARK_GETID + BENCHMARK_VOID_EXPRESSION(gBenchmark, "GetId(void*)", + { +#endif + ImGuiID seed = IDStack.back(); + id = ImHashData(&ptr, sizeof(void*), seed); + #ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL); + #endif +#ifdef BENCHMARK_GETID + }); #endif return id; } ImGuiID ImGuiWindow::GetID(int n) { - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHashData(&n, sizeof(n), seed); -#ifndef IMGUI_DISABLE_DEBUG_TOOLS - ImGuiContext& g = *Ctx; - if (g.DebugHookIdInfo == id) - ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); + ImGuiID id; +#ifdef BENCHMARK_GETID + BENCHMARK_VOID_EXPRESSION(gBenchmark, "GetId(int)", + { +#endif + ImGuiID seed = IDStack.back(); + id = ImHashData(&n, sizeof(n), seed); + #ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); + #endif +#ifdef BENCHMARK_GETID + }); +#endif + return id; +} + +// Addition to ImGui Bundle: a version of GetID that warns if the ID was already used +IMGUI_API ImGuiID ImGuiWindow::GetID_AssertUnique(const char* str_id) +{ + // We do not need to benchmark this part + // (as it is already benchmarked inside ImGuiWindow::GetID(const char*)) + ImGuiID id = GetID(str_id); + +#ifdef BENCHMARK_GETID + // Only benchmark the part that checks for reused id + BENCHMARK_VOID_EXPRESSION(gBenchmark, "GetID_AssertUniquePart", + { +#endif + // sIdsThisFrame: a cache of the previously encountered ID + // allocated once for all, max size = 100 + constexpr int sMaxCacheSize = 100; + static ImVector sIdsThisFrame; + + // Allocate memory once at startup + if (sIdsThisFrame.capacity() < sMaxCacheSize) + sIdsThisFrame.reserve(sMaxCacheSize); + + static int sLastFrame = -1; + int currentFrame = ImGui::GetFrameCount(); + + if (currentFrame != sLastFrame) + { + // This log was used to log the number of items in sIdsThisFrame + // => maximum reached inside ShowDemoWindow/Widgets/Basic + // with 52 items + //printf("Clearing %i items\n", sIdsThisFrame.size()); + + // We do not call sIdsThisFrame.clear(): we want to avoid any memory allocation during runtime + // instead we reset the size, and keep the allocated capacity. This way, our cache remains hot in the memory. + sIdsThisFrame.Size = 0; + } + + if (sIdsThisFrame.contains(id)) + IM_ASSERT(false && "Either your widgets names/ids must be distinct, or you shall call ImGui::PushID before reusing an id"); + + if (sIdsThisFrame.size() < sMaxCacheSize - 1) + sIdsThisFrame.push_back(id); + sLastFrame = currentFrame; +#ifdef BENCHMARK_GETID + }); #endif return id; } + + // This is only used in rare/specific situations to manufacture an ID out of nowhere. ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) { diff --git a/imgui_internal.h b/imgui_internal.h index 38be6d13b36e..7d79009255b1 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2883,6 +2883,10 @@ struct IMGUI_API ImGuiWindow ImGuiID GetID(const char* str, const char* str_end = NULL); ImGuiID GetID(const void* ptr); ImGuiID GetID(int n); + + // Addition to ImGui Bundle: a version of GetID that warns if the ID was already used + IMGUI_API ImGuiID GetID_AssertUnique(const char* str_id); // (Specific to ImGui Bundle) Calculate unique ID (hash of whole ID stack + given parameter). Will warn if the ID was already used, and advise to call ImGui::PushID() before + ImGuiID GetIDFromRectangle(const ImRect& r_abs); // We don't use g.FontSize because the window may be != g.CurrentWindow. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index bcf16f0706be..2b696735e4d7 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -701,7 +701,7 @@ bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID(label); + const ImGuiID id = window->GetID_AssertUnique(label); const ImVec2 label_size = CalcTextSize(label, NULL, true); ImVec2 pos = window->DC.CursorPos; @@ -783,7 +783,7 @@ bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiBu if (window->SkipItems) return false; - const ImGuiID id = window->GetID(str_id); + const ImGuiID id = window->GetID_AssertUnique(str_id); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); const float default_size = GetFrameHeight(); ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f); @@ -1121,7 +1121,7 @@ bool ImGui::Checkbox(const char* label, bool* v) ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID(label); + const ImGuiID id = window->GetID_AssertUnique(label); const ImVec2 label_size = CalcTextSize(label, NULL, true); const float square_sz = GetFrameHeight(); @@ -1225,7 +1225,7 @@ bool ImGui::RadioButton(const char* label, bool active) ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID(label); + const ImGuiID id = window->GetID_AssertUnique(label); const ImVec2 label_size = CalcTextSize(label, NULL, true); const float square_sz = GetFrameHeight(); @@ -2453,7 +2453,7 @@ bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID(label); + const ImGuiID id = window->GetID_AssertUnique(label); const float w = CalcItemWidth(); const ImVec2 label_size = CalcTextSize(label, NULL, true); @@ -3044,7 +3044,7 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_dat ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID(label); + const ImGuiID id = window->GetID_AssertUnique(label); const float w = CalcItemWidth(); const ImVec2 label_size = CalcTextSize(label, NULL, true); @@ -4134,7 +4134,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar) BeginGroup(); - const ImGuiID id = window->GetID(label); + const ImGuiID id = window->GetID_AssertUnique(label); const ImVec2 label_size = CalcTextSize(label, NULL, true); const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);