From 06523e5417c72a8e37f24783751dd485811fedc9 Mon Sep 17 00:00:00 2001 From: 4DBug <4DBug@github.com> Date: Thu, 8 Oct 2026 12:31:10 -0500 Subject: [PATCH] Perf: ColorEdit4 layout-only fast path for fully clipped rows, cached drag/slider value text, @native on color/group widgets; t_widget_cost + t_colors_hash --- build/ImGui.linemap.json | 2 +- build/ImGui.luau | 55 ++++++++++++++++++++- imgui/imgui.lua | 9 ++++ imgui/imgui_widgets.lua | 45 ++++++++++++++++- tools/test/roblox_mock.luau | 2 +- tools/test/scripts/t_colors_hash.luau | 32 ++++++++++++ tools/test/scripts/t_widget_cost.luau | 71 +++++++++++++++++++++++++++ 7 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 tools/test/scripts/t_colors_hash.luau create mode 100644 tools/test/scripts/t_widget_cost.luau diff --git a/build/ImGui.linemap.json b/build/ImGui.linemap.json index bc88215..5366462 100644 --- a/build/ImGui.linemap.json +++ b/build/ImGui.linemap.json @@ -1 +1 @@ -[[12, "imgui.lua"], [13799, "imgui_demo.lua"], [14732, "imgui_demo_2.lua"], [17807, "imgui_demo_apps.lua"], [19066, "imgui_demo_tools.lua"], [20392, "imgui_demo_widgets.lua"], [22316, "imgui_docking.lua"], [25444, "imgui_dragdrop.lua"], [25736, "imgui_draw.lua"], [30974, "imgui_h.lua"], [33172, "imgui_impl_roblox.lua"], [34298, "imgui_impl_roblox_fonts.lua"], [40701, "imgui_internal.lua"], [43873, "imgui_tables.lua"], [48220, "imgui_tabs.lua"], [49387, "imgui_widgets.lua"], [57921, "imstb_rectpack.lua"], [58367, "imstb_textedit.lua"], [59608, "imstb_truetype.lua"], [62076, "imstd_minstdio.lua"]] \ No newline at end of file +[[12, "imgui.lua"], [13808, "imgui_demo.lua"], [14741, "imgui_demo_2.lua"], [17816, "imgui_demo_apps.lua"], [19075, "imgui_demo_tools.lua"], [20401, "imgui_demo_widgets.lua"], [22325, "imgui_docking.lua"], [25453, "imgui_dragdrop.lua"], [25745, "imgui_draw.lua"], [30983, "imgui_h.lua"], [33181, "imgui_impl_roblox.lua"], [34307, "imgui_impl_roblox_fonts.lua"], [40710, "imgui_internal.lua"], [43882, "imgui_tables.lua"], [48229, "imgui_tabs.lua"], [49396, "imgui_widgets.lua"], [57971, "imstb_rectpack.lua"], [58417, "imstb_textedit.lua"], [59658, "imstb_truetype.lua"], [62126, "imstd_minstdio.lua"]] \ No newline at end of file diff --git a/build/ImGui.luau b/build/ImGui.luau index 1aac436..4fb0544 100644 --- a/build/ImGui.luau +++ b/build/ImGui.luau @@ -2004,6 +2004,7 @@ end -- Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) -- Groups are currently a mishmash of functionalities which should perhaps be clarified and separated. -- FIXME-OPT: Could we safely early out on ->SkipItems? +@native function ImGui.BeginGroup() local g = GImGui local window = g.CurrentWindow @@ -2035,6 +2036,7 @@ function ImGui.BeginGroup() end end +@native function ImGui.EndGroup() local g = GImGui local window = g.CurrentWindow @@ -3909,6 +3911,7 @@ end --- @param button ImGuiMouseButton --- @param flags ImGuiInputFlags --- @param owner_id? ImGuiID +@native function ImGui.IsMouseClickedEx(button, flags, owner_id) if owner_id == nil then owner_id = ImGuiKeyOwner_Any end @@ -5126,6 +5129,7 @@ end --- @param p_min ImVec2 --- @param p_max ImVec2 --- @param rounding float +@native function ImGui.RenderFrameBorder(p_min, p_max, rounding) local g = GImGui local window = g.CurrentWindow @@ -7986,6 +7990,7 @@ function ImGui.GetCurrentWindowRead() end --- @return ImGuiWindow +@native function ImGui.GetCurrentWindow() local g = GImGui g.CurrentWindow.WriteAccessed = true @@ -8707,6 +8712,7 @@ end --- @param g float --- @param b float --- @return float, float, float +@native function ImGui.ColorConvertRGBtoHSV(r, g, b) local K = 0.0 if g < b then @@ -8729,6 +8735,7 @@ end --- @param s float --- @param v float --- @return float, float, float +@native function ImGui.ColorConvertHSVtoRGB(h, s, v) if s == 0.0 then -- gray @@ -10009,6 +10016,7 @@ end --- @param popup_flags ImGuiPopupFlags --- @param id ImGuiID +@native function ImGui.IsPopupOpenRequestForItem(popup_flags, id) local g = GImGui local mouse_button = ImGui.GetMouseButtonFromPopupFlags(popup_flags) @@ -10023,6 +10031,7 @@ end --- @param str_id? string --- @param popup_flags? ImGuiPopupFlags +@native function ImGui.OpenPopupOnItemClick(str_id, popup_flags) if popup_flags == nil then popup_flags = 0 end @@ -52376,6 +52385,7 @@ end --- @param max number --- @param format string --- @param flags ImGuiSliderFlags +@native function ImGui.DragBehavior(id, data_type, v, v_speed, min, max, format, flags) IM_ASSERT((flags == 1 or bit32.band(flags, ImGuiSliderFlags.InvalidMask_) == 0), "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.") @@ -52444,6 +52454,22 @@ end --- @param max number --- @param format string --- @param flags ImGuiSliderFlags +-- [Luau] memo of string.format(format, value) for drag/slider value text (same values are redrawn every frame). +-- ponytail: whole-memo reset at 4096 entries, LRU if values churn +local fmt_memo, fmt_memo_n = {}, 0 +local DRAG_TEXT_ALIGN = ImVec2(0.5, 0.5) +function FormatCached(format, v) + local m = fmt_memo[format] + if m == nil then m = {}; fmt_memo[format] = m end + local r = m[v] + if r == nil then + if fmt_memo_n >= 4096 then table.clear(fmt_memo); m = {}; fmt_memo[format] = m; fmt_memo_n = 0 end + r = ImFormatString(format, v); m[v] = r; fmt_memo_n += 1 + end + return r +end + +@native function ImGui.DragScalar(label, data_type, data, v_speed, min, max, format, flags) if v_speed == nil then v_speed = 1.0 end if flags == nil then flags = 0 end @@ -52537,7 +52563,7 @@ function ImGui.DragScalar(label, data_type, data, v_speed, min, max, format, fla -- TODO: ImGui.DataTypeFormatString() should be used here instead of ImFormatString() -- Display value using user-provided display format so user can add prefix/suffix/decorations to the value - ImGui.RenderTextClipped(frame_bb.Min, frame_bb.Max, ImFormatString(format, data), nil, nil, ImVec2(0.5, 0.5)) + ImGui.RenderTextClipped(frame_bb.Min, frame_bb.Max, FormatCached(format, data), nil, nil, DRAG_TEXT_ALIGN) if label_size.x > 0.0 then ImGui.RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, 1, label_end, false) @@ -52570,6 +52596,7 @@ end --- @param v_max? int --- @param format? string --- @param flags? ImGuiSliderFlags +@native function ImGui.DragInt(label, v, v_speed, v_min, v_max, format, flags) if v_speed == nil then v_speed = 1.0 end if v_min == nil then v_min = 0 end @@ -53159,7 +53186,7 @@ function ImGui.SliderScalar(label, data_type, data, min, max, format, flags) -- if g.LogEnabled then -- ImGui.LogSetNextTextDecoration("{", "}") -- end - ImGui.RenderTextClipped(frame_bb.Min, frame_bb.Max, ImFormatString(format, data), nil, nil, ImVec2(0.5, 0.5)) + ImGui.RenderTextClipped(frame_bb.Min, frame_bb.Max, FormatCached(format, data), nil, nil, DRAG_TEXT_ALIGN) if label_size.x > 0.0 then ImGui.RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, 1, label_end, false) @@ -55231,6 +55258,7 @@ local fmt_table_float = { --- @param label string --- @param col float[] --- @param flags ImGuiColorEditFlags +@native function ImGui.ColorEdit4(label, col, flags) if flags == nil then flags = 0 end local window = ImGui.GetCurrentWindow() @@ -55245,6 +55273,27 @@ function ImGui.ColorEdit4(label, col, flags) local w_full = ImGui.CalcItemWidth() g.NextItemData:ClearFlags() + -- [Luau] Fast path for a row that is entirely clipped (e.g. long lists such as Style Editor > Colors): nothing + -- inside would draw, be hovered or receive input, so only reproduce the layout (same group size, baseline and + -- last item rect as the full path). Not taken while anything could depend on the inner items: an active item + -- or popup, keyboard navigation, logging, or the ID debug hook. + if label_display_end == 1 and bit32.band(flags, bit32.bor(ImGuiColorEditFlags.NoInputs, ImGuiColorEditFlags.NoSmallPreview)) == 0 + and g.ActiveId == 0 and g.OpenPopupStack.Size == 0 and not g.NavCursorVisible and not g.NavAnyRequest + and not g.LogEnabled and g.DebugHookIdInfoId == 0 then + local w_button = square_sz + style.ItemInnerSpacing.x + local w = ImMax(w_full - w_button, 1.0) + w_button + local p = window.DC.CursorPos + local cr = window.ClipRect + if p.y + square_sz < cr.Min.y or p.y > cr.Max.y or p.x + w < cr.Min.x or p.x > cr.Max.x then + ImGui.BeginGroup() + local bb = ImRect(p.x, p.y, p.x + w, p.y + square_sz) + ImGui.ItemSize(ImVec2(w, square_sz), style.FramePadding.y) + ImGui.ItemAdd(bb, 0) + ImGui.EndGroup() + return false + end + end + ImGui.BeginGroup() ImGui.PushID(label) local set_current_color_edit_id = (g.ColorEditCurrentID == 0) @@ -55956,6 +56005,7 @@ end --- @param col ImVec4 --- @param flags? ImGuiColorEditFlags --- @param size_arg? ImVec2 +@native function ImGui.ColorButton(desc_id, col, flags, size_arg) if flags == nil then flags = 0 end if size_arg == nil then size_arg = ImVec2(0, 0) end @@ -62370,6 +62420,7 @@ return { FLT_MIN = FLT_MIN, FONT_ATLAS_DEFAULT_TEX_DATA_H = FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_W = FONT_ATLAS_DEFAULT_TEX_DATA_W, + FormatCached = FormatCached, GetDefaultFontDataProggyClean = GetDefaultFontDataProggyClean, GetDefaultFontDataProggyForever = GetDefaultFontDataProggyForever, GetMinimumStepAtDecimalPrecision = GetMinimumStepAtDecimalPrecision, diff --git a/imgui/imgui.lua b/imgui/imgui.lua index be7bb5f..5330f4c 100644 --- a/imgui/imgui.lua +++ b/imgui/imgui.lua @@ -1993,6 +1993,7 @@ end -- Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) -- Groups are currently a mishmash of functionalities which should perhaps be clarified and separated. -- FIXME-OPT: Could we safely early out on ->SkipItems? +@native function ImGui.BeginGroup() local g = GImGui local window = g.CurrentWindow @@ -2024,6 +2025,7 @@ function ImGui.BeginGroup() end end +@native function ImGui.EndGroup() local g = GImGui local window = g.CurrentWindow @@ -3898,6 +3900,7 @@ end --- @param button ImGuiMouseButton --- @param flags ImGuiInputFlags --- @param owner_id? ImGuiID +@native function ImGui.IsMouseClickedEx(button, flags, owner_id) if owner_id == nil then owner_id = ImGuiKeyOwner_Any end @@ -5115,6 +5118,7 @@ end --- @param p_min ImVec2 --- @param p_max ImVec2 --- @param rounding float +@native function ImGui.RenderFrameBorder(p_min, p_max, rounding) local g = GImGui local window = g.CurrentWindow @@ -7975,6 +7979,7 @@ function ImGui.GetCurrentWindowRead() end --- @return ImGuiWindow +@native function ImGui.GetCurrentWindow() local g = GImGui g.CurrentWindow.WriteAccessed = true @@ -8696,6 +8701,7 @@ end --- @param g float --- @param b float --- @return float, float, float +@native function ImGui.ColorConvertRGBtoHSV(r, g, b) local K = 0.0 if g < b then @@ -8718,6 +8724,7 @@ end --- @param s float --- @param v float --- @return float, float, float +@native function ImGui.ColorConvertHSVtoRGB(h, s, v) if s == 0.0 then -- gray @@ -9998,6 +10005,7 @@ end --- @param popup_flags ImGuiPopupFlags --- @param id ImGuiID +@native function ImGui.IsPopupOpenRequestForItem(popup_flags, id) local g = GImGui local mouse_button = ImGui.GetMouseButtonFromPopupFlags(popup_flags) @@ -10012,6 +10020,7 @@ end --- @param str_id? string --- @param popup_flags? ImGuiPopupFlags +@native function ImGui.OpenPopupOnItemClick(str_id, popup_flags) if popup_flags == nil then popup_flags = 0 end diff --git a/imgui/imgui_widgets.lua b/imgui/imgui_widgets.lua index bc2a8f2..90f8304 100644 --- a/imgui/imgui_widgets.lua +++ b/imgui/imgui_widgets.lua @@ -2990,6 +2990,7 @@ end --- @param max number --- @param format string --- @param flags ImGuiSliderFlags +@native function ImGui.DragBehavior(id, data_type, v, v_speed, min, max, format, flags) IM_ASSERT((flags == 1 or bit32.band(flags, ImGuiSliderFlags.InvalidMask_) == 0), "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.") @@ -3058,6 +3059,22 @@ end --- @param max number --- @param format string --- @param flags ImGuiSliderFlags +-- [Luau] memo of string.format(format, value) for drag/slider value text (same values are redrawn every frame). +-- ponytail: whole-memo reset at 4096 entries, LRU if values churn +local fmt_memo, fmt_memo_n = {}, 0 +local DRAG_TEXT_ALIGN = ImVec2(0.5, 0.5) +function FormatCached(format, v) + local m = fmt_memo[format] + if m == nil then m = {}; fmt_memo[format] = m end + local r = m[v] + if r == nil then + if fmt_memo_n >= 4096 then table.clear(fmt_memo); m = {}; fmt_memo[format] = m; fmt_memo_n = 0 end + r = ImFormatString(format, v); m[v] = r; fmt_memo_n += 1 + end + return r +end + +@native function ImGui.DragScalar(label, data_type, data, v_speed, min, max, format, flags) if v_speed == nil then v_speed = 1.0 end if flags == nil then flags = 0 end @@ -3151,7 +3168,7 @@ function ImGui.DragScalar(label, data_type, data, v_speed, min, max, format, fla -- TODO: ImGui.DataTypeFormatString() should be used here instead of ImFormatString() -- Display value using user-provided display format so user can add prefix/suffix/decorations to the value - ImGui.RenderTextClipped(frame_bb.Min, frame_bb.Max, ImFormatString(format, data), nil, nil, ImVec2(0.5, 0.5)) + ImGui.RenderTextClipped(frame_bb.Min, frame_bb.Max, FormatCached(format, data), nil, nil, DRAG_TEXT_ALIGN) if label_size.x > 0.0 then ImGui.RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, 1, label_end, false) @@ -3184,6 +3201,7 @@ end --- @param v_max? int --- @param format? string --- @param flags? ImGuiSliderFlags +@native function ImGui.DragInt(label, v, v_speed, v_min, v_max, format, flags) if v_speed == nil then v_speed = 1.0 end if v_min == nil then v_min = 0 end @@ -3773,7 +3791,7 @@ function ImGui.SliderScalar(label, data_type, data, min, max, format, flags) -- if g.LogEnabled then -- ImGui.LogSetNextTextDecoration("{", "}") -- end - ImGui.RenderTextClipped(frame_bb.Min, frame_bb.Max, ImFormatString(format, data), nil, nil, ImVec2(0.5, 0.5)) + ImGui.RenderTextClipped(frame_bb.Min, frame_bb.Max, FormatCached(format, data), nil, nil, DRAG_TEXT_ALIGN) if label_size.x > 0.0 then ImGui.RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, 1, label_end, false) @@ -5845,6 +5863,7 @@ local fmt_table_float = { --- @param label string --- @param col float[] --- @param flags ImGuiColorEditFlags +@native function ImGui.ColorEdit4(label, col, flags) if flags == nil then flags = 0 end local window = ImGui.GetCurrentWindow() @@ -5859,6 +5878,27 @@ function ImGui.ColorEdit4(label, col, flags) local w_full = ImGui.CalcItemWidth() g.NextItemData:ClearFlags() + -- [Luau] Fast path for a row that is entirely clipped (e.g. long lists such as Style Editor > Colors): nothing + -- inside would draw, be hovered or receive input, so only reproduce the layout (same group size, baseline and + -- last item rect as the full path). Not taken while anything could depend on the inner items: an active item + -- or popup, keyboard navigation, logging, or the ID debug hook. + if label_display_end == 1 and bit32.band(flags, bit32.bor(ImGuiColorEditFlags.NoInputs, ImGuiColorEditFlags.NoSmallPreview)) == 0 + and g.ActiveId == 0 and g.OpenPopupStack.Size == 0 and not g.NavCursorVisible and not g.NavAnyRequest + and not g.LogEnabled and g.DebugHookIdInfoId == 0 then + local w_button = square_sz + style.ItemInnerSpacing.x + local w = ImMax(w_full - w_button, 1.0) + w_button + local p = window.DC.CursorPos + local cr = window.ClipRect + if p.y + square_sz < cr.Min.y or p.y > cr.Max.y or p.x + w < cr.Min.x or p.x > cr.Max.x then + ImGui.BeginGroup() + local bb = ImRect(p.x, p.y, p.x + w, p.y + square_sz) + ImGui.ItemSize(ImVec2(w, square_sz), style.FramePadding.y) + ImGui.ItemAdd(bb, 0) + ImGui.EndGroup() + return false + end + end + ImGui.BeginGroup() ImGui.PushID(label) local set_current_color_edit_id = (g.ColorEditCurrentID == 0) @@ -6570,6 +6610,7 @@ end --- @param col ImVec4 --- @param flags? ImGuiColorEditFlags --- @param size_arg? ImVec2 +@native function ImGui.ColorButton(desc_id, col, flags, size_arg) if flags == nil then flags = 0 end if size_arg == nil then size_arg = ImVec2(0, 0) end diff --git a/tools/test/roblox_mock.luau b/tools/test/roblox_mock.luau index 698638e..3dc1da5 100644 --- a/tools/test/roblox_mock.luau +++ b/tools/test/roblox_mock.luau @@ -160,7 +160,7 @@ ROBLOX = setmetatable({ Color3 = { new = function(r, g, b) return { R = r or 0, G = g or 0, B = b or 0 } end }, Content = { fromObject = function(o) return { Object = o } end }, warn = function(...) print("WARN", ...) end, - os = { clock = function() return FAKE_TIME end, time = os.time }, + os = { clock = function() return FAKE_TIME end, time = os.time, realclock = os.clock }, }, { __index = realG }) local mod = newInst("ModuleScript", "ImGui", ReplicatedStorage); rawset(mod, "_src", "ImGui.luau") diff --git a/tools/test/scripts/t_colors_hash.luau b/tools/test/scripts/t_colors_hash.luau new file mode 100644 index 0000000..ed2db35 --- /dev/null +++ b/tools/test/scripts/t_colors_hash.luau @@ -0,0 +1,32 @@ +-- Checksum of draw data + layout of Style Editor > Colors while scrolling its list (fast-path equivalence check) +local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +ImGui.CreateContext() +G.ImGui_ImplRoblox.Init() +local io = ImGui.GetIO() +local realBTI = ImGui.BeginTabItem +ImGui.BeginTabItem = function(label, p_open, flags) + if label == "Colors" then flags = bit32.bor(flags or 0, G.ImGuiTabItemFlags.SetSelected) end + return realBTI(label, p_open, flags) +end +local frame, h = 0, 0 +local function mix(v) h = (h * 31 + math.floor(v * 64 + 0.5)) % 2147483647 end +game:GetService("RunService").Stepped:Connect(function() + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + io.DeltaTime = 1 / 60 + io:AddMousePosEvent(200, 400 + (frame % 5) * 13) + if frame > 5 then io:AddMouseWheelEvent(0, (frame % 60 < 35) and -1 or 1) end + ImGui.NewFrame() + ImGui.SetNextWindowPos(ImVec2(20, 20)); ImGui.SetNextWindowSize(ImVec2(550, 700)) + ImGui.Begin("Style Editor"); ImGui.ShowStyleEditor(); ImGui.End() + ImGui.Render() + for _, dl in ImGui.GetDrawData().CmdLists:iter() do + local vb, id = dl.VtxBuffer.Buf, dl.IdxBuffer.Data + for i = 1, dl.VtxBuffer.Size do local o = (i - 1) * 36; mix(buffer.readf64(vb, o)); mix(buffer.readf64(vb, o + 8)); mix(buffer.readu32(vb, o + 32) / 64) end + for i = 1, dl.IdxBuffer.Size do mix(id[i]) end + end + for _, w in ipairs(ImGui.GetCurrentContext().Windows.Data) do mix(w.ContentSize.x); mix(w.ContentSize.y); mix(w.Scroll.y); mix(w.ScrollMax.y) end + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) + if frame == ARG_FRAMES - 1 then print("COLORSHASH", h) end +end) diff --git a/tools/test/scripts/t_widget_cost.luau b/tools/test/scripts/t_widget_cost.luau new file mode 100644 index 0000000..8c98ab7 --- /dev/null +++ b/tools/test/scripts/t_widget_cost.luau @@ -0,0 +1,71 @@ +-- Per-widget cost: wraps every ImGui.* function with self-time timing, shows the Style Editor on its Colors tab +-- (or the demo with --open-only ...), prints a table sorted by total self time and by cost per call. +local G = require(game:GetService("ReplicatedStorage"):WaitForChild("ImGui")) +local ImGui, ImVec2 = G.ImGui, G.ImVec2 +ImGui.CreateContext() +G.ImGui_ImplRoblox.Init() +local io = ImGui.GetIO() +local STYLE = type(ARG_OPEN) ~= "table" + +local realBTI = ImGui.BeginTabItem +ImGui.BeginTabItem = function(label, p_open, flags) + if label == "Colors" and STYLE then flags = bit32.bor(flags or 0, G.ImGuiTabItemFlags.SetSelected) end + return realBTI(label, p_open, flags) +end + +local self_t, calls = {}, {} +local stack_child = {} -- time spent in nested wrapped calls, per depth +local depth, enabled = 0, false +local clock = os.realclock or os.clock -- harness fakes os.clock; realclock is the real one +for name, fn in pairs(ImGui) do + if type(fn) == "function" and name ~= "NewFrame" and name ~= "Render" and name ~= "EndFrame" then + ImGui[name] = function(...) + if not enabled then return fn(...) end + depth += 1; stack_child[depth] = 0 + local t0 = clock() + local r = table.pack(fn(...)) + local dt = clock() - t0 + local own = dt - stack_child[depth] + depth -= 1 + if depth > 0 then stack_child[depth] += dt end + self_t[name] = (self_t[name] or 0) + own; calls[name] = (calls[name] or 0) + 1 + return table.unpack(r, 1, r.n) + end + end +end + +local frame = 0 +game:GetService("RunService").Stepped:Connect(function() + frame += 1 + G.ImGui_ImplRoblox.NewFrame() + io:AddMousePosEvent(5, 5) + ImGui.NewFrame() + enabled = frame > 10 + if STYLE then + ImGui.SetNextWindowPos(ImVec2(20, 20)); ImGui.SetNextWindowSize(ImVec2(550, 900)) + ImGui.Begin("Style Editor"); ImGui.ShowStyleEditor(); ImGui.End() + else + ImGui.ShowDemoWindow(true) + end + enabled = false + ImGui.Render() + G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData()) + if frame == ARG_FRAMES - 1 then + local nf = frame - 10 + local names = {} + local total = 0 + for k, v in pairs(self_t) do table.insert(names, k); total += v end + table.sort(names, function(a, b) return self_t[a] > self_t[b] end) + print(string.format("TOTAL %.3f ms/frame (wrapped calls, incl. wrapper overhead)", total / nf * 1000)) + print("== by self time per frame") + for i = 1, math.min(20, #names) do local k = names[i] + print(string.format("%7.3f ms %6.1f%% %5d calls %6.1f us/call %s", self_t[k] / nf * 1000, 100 * self_t[k] / total, calls[k] / nf, self_t[k] / calls[k] * 1e6, k)) end + table.sort(names, function(a, b) return self_t[a] / calls[a] > self_t[b] / calls[b] end) + print("== by cost per call (>= 1 call/frame)") + local shown = 0 + for _, k in ipairs(names) do + if calls[k] >= nf and shown < 15 then shown += 1 + print(string.format("%6.1f us/call %5d calls %s", self_t[k] / calls[k] * 1e6, calls[k] / nf, k)) end + end + end +end)