@@ -635,6 +635,16 @@ local function ApplyFitPush(macroEditBox, delta, layoutQueue, layoutSeen)
635635 if not sharedChrome and delta ~= 0 and parent .explicitHeight
636636 and parent .height and parent .SetHeight then
637637 parent :SetHeight (parent .height + delta )
638+ -- A resized ancestor still has to re-place its children, so it goes
639+ -- on the SAME queue as the pass-through ones. Previously it was laid
640+ -- out by SetHeight itself, twice (baseMethods:SetHeight lays out
641+ -- self AND self.parent) and immediately -- outside the dedupe, once
642+ -- per box. Measured: 90 of one open's 289 layout passes came from
643+ -- those two lines alone.
644+ if not layoutSeen [parent ] then
645+ layoutSeen [parent ] = true
646+ layoutQueue [# layoutQueue + 1 ] = parent
647+ end
638648 elseif parent .DoLayout and not layoutSeen [parent ] then
639649 layoutSeen [parent ] = true
640650 layoutQueue [# layoutQueue + 1 ] = parent
@@ -650,13 +660,16 @@ fitFlushDriver:SetScript("OnUpdate", function(self)
650660 pendingFitPushes = {}
651661 self :Hide ()
652662 local layoutQueue , layoutSeen = {}, {}
663+ local batched = UI and UI .SuspendLayout
664+ if batched then UI :SuspendLayout () end
653665 for _ , push in ipairs (pushes ) do
654666 -- A widget released (or pooled) since queueing has no parent chain of
655667 -- its own any more; its push is meaningless, skip it.
656668 if push [1 ].parent then
657669 ApplyFitPush (push [1 ], push [2 ], layoutQueue , layoutSeen )
658670 end
659671 end
672+ if batched and UI .ResumeLayout then UI :ResumeLayout () end
660673 for _ , container in ipairs (layoutQueue ) do
661674 container :DoLayout ()
662675 end
@@ -683,11 +696,18 @@ local function FitMacroEditBoxToContent(macroEditBox, text)
683696 -- MEASURE the rendered text height with a hidden FontString in the box's
684697 -- own font (wraps included) instead of estimating rows x font size --
685698 -- estimates drifted by about a row and showed a spare empty line.
686- local meter = macroEditBox .gseHeightMeter
699+ -- On the FRAME, not the widget table. Since #2014 MultiLineEditBox is a
700+ -- pooled type, and resetForReuse strips every key added after construction
701+ -- -- including this one. A FontString cannot be destroyed, so caching it on
702+ -- the widget meant a fresh one on every reuse, piling up hidden regions on a
703+ -- frame that is reused forever. That also slows the pool down: its reset
704+ -- walks {frame:GetRegions()} each time. The frame object survives reuse
705+ -- unchanged, so the meter parked on it is found again.
706+ local meter = macroEditBox .frame .gseHeightMeter
687707 if not meter then
688708 meter = macroEditBox .frame :CreateFontString (nil , " ARTWORK" )
689709 meter :Hide ()
690- macroEditBox .gseHeightMeter = meter
710+ macroEditBox .frame . gseHeightMeter = meter
691711 end
692712 local fontPath , fontSize , fontFlags = eb :GetFont ()
693713 if fontPath then meter :SetFont (fontPath , fontSize or 14 , fontFlags or " " ) end
@@ -1756,6 +1776,18 @@ function GSE.HydrateClassActionIcons(classid)
17561776 end
17571777end
17581778
1779+ -- One sequence's worth of the hydration below. This is what the editor
1780+ -- actually needs -- the icons for the sequence on screen -- and it is called
1781+ -- as each sequence is opened rather than sweeping the library on every open.
1782+ local function hydrateSequenceIcons (sequence )
1783+ if type (sequence ) ~= " table" or type (sequence .Versions ) ~= " table" then return end
1784+ for _ , versionData in ipairs (sequence .Versions ) do
1785+ if type (versionData ) == " table" then
1786+ hydrateActionIcons (versionData .Actions )
1787+ end
1788+ end
1789+ end
1790+
17591791function GSE .HydrateLoadedSequenceActionIcons (scanStats , saveChanges )
17601792 if type (GSE .Library ) ~= " table" then return 0 , 0 , 0 end
17611793
@@ -3247,6 +3279,12 @@ function GSE.CreateEditor()
32473279 editframe .pendingScrollRestore = nil
32483280 local batchLayout = not _G .GSE_NoLayoutBatch
32493281 if batchLayout and UI and UI .SuspendLayout then UI :SuspendLayout () end
3282+ -- Paired with the report in finishDraw. Same reasoning as ManageTree's:
3283+ -- block count and elapsed time are what turn "the editor freezes" into a
3284+ -- number. Silent unless the Editor debug module is on.
3285+ -- @debug@
3286+ local drawStartedAt = GSE .NowMs ()
3287+ -- @end-debug@
32503288 editframe .rawEditor = nil
32513289 SetOuterEditorScrollBarEnabled (true )
32523290 if tcontainer .SetListPadding then
@@ -4834,6 +4872,14 @@ function GSE.CreateEditor()
48344872 return layoutcontainer , finalizeToolbar , CreateAddButtonRow , CreateChildAddButtonRow
48354873 end
48364874 local function drawAction (pcontainer , action , version , keyPath , treepath )
4875+ -- Counted against CountActionBlocks below. The chunk-vs-synchronous
4876+ -- gate trusts that count, so if it undercounts, a big sequence
4877+ -- silently skips chunking and builds every block in one frame --
4878+ -- exactly the freeze the chunking exists to prevent. Divergence
4879+ -- between the two numbers is the tell.
4880+ -- @debug@
4881+ editframe .lastDrawnBlocks = (editframe .lastDrawnBlocks or 0 ) + 1
4882+ -- @end-debug@
48374883 local function drawChild (childContainer , childAction , childKeyPath , childTreepath )
48384884 local q = editframe .incBuildQueue
48394885 if q then
@@ -5962,6 +6008,15 @@ function GSE.CreateEditor()
59626008
59636009 local function finishDraw ()
59646010 if tcontainer .DoLayout then tcontainer :DoLayout () end
6011+ -- @debug@
6012+ GSE .PrintDebugMessage (
6013+ string.format (" DrawSequenceEditor: %d blocks counted, %d drawn, %s, in %.0f ms" ,
6014+ editframe .lastDrawBlockCount or 0 , editframe .lastDrawnBlocks or 0 ,
6015+ (editframe .lastDrawBlockCount or 0 ) > 12 and " chunked" or " one frame" ,
6016+ GSE .NowMs () - drawStartedAt ),
6017+ Statics .DebugModules [" Editor" ]
6018+ )
6019+ -- @end-debug@
59656020 if editframe .scrollContainer and editframe .scrollContainer .DoLayout then
59666021 editframe .scrollContainer :DoLayout ()
59676022 if editframe .scrollContainer .SetScroll then
@@ -5994,6 +6049,10 @@ function GSE.CreateEditor()
59946049 return n
59956050 end
59966051 local totalBlocks = CountActionBlocks (macro )
6052+ -- @debug@
6053+ editframe .lastDrawBlockCount = totalBlocks
6054+ editframe .lastDrawnBlocks = 0
6055+ -- @end-debug@
59976056
59986057 local INCREMENTAL_MIN_BLOCKS = 12
59996058 if not (C_Timer and C_Timer .After ) or totalBlocks <= INCREMENTAL_MIN_BLOCKS then
@@ -7621,10 +7680,55 @@ function GSE.CreateEditor()
76217680 return editframe
76227681end
76237682
7683+ -- An open this slow is a defect, not a preference, so it is reported to the
7684+ -- user even with debug off -- once, with the numbers needed to act on it.
7685+ -- Below the threshold it stays on the debug module like every other timing.
7686+ -- @debug@
7687+ local EDITOR_SLOW_OPEN_MS = 2000
7688+ local function ReportOpenTiming (message , elapsed )
7689+ if elapsed and elapsed > EDITOR_SLOW_OPEN_MS then
7690+ GSE .Print (message , Statics .DebugModules [" Editor" ])
7691+ else
7692+ GSE .PrintDebugMessage (message , Statics .DebugModules [" Editor" ])
7693+ end
7694+ end
7695+
7696+ -- @end-debug@
7697+
76247698function GSE .ShowSequences ()
7699+ -- End-to-end open cost. ManageTree and DrawSequenceEditor time themselves;
7700+ -- this brackets everything, so a gap between them and this total says the
7701+ -- time is going somewhere neither of those covers -- which is exactly how
7702+ -- the whole-library icon scan below was found (14ms + 123ms inside a
7703+ -- seventeen second open).
7704+ -- @debug@
7705+ local openStartedAt = GSE .NowMs ()
7706+ -- @end-debug@
76257707 local editframe = GSE .CreateEditor ()
7708+ -- @debug@
7709+ local afterCreate = GSE .NowMs ()
7710+ -- @end-debug@
76267711 editframe .ManageTree ()
7627- if GSE .HydrateLoadedSequenceActionIcons then GSE .HydrateLoadedSequenceActionIcons () end
7712+ -- @debug@
7713+ local afterTree = GSE .NowMs ()
7714+ -- @end-debug@
7715+ -- The whole-library icon hydration used to run HERE, on every open, and it
7716+ -- was the freeze: it force-loads every class (GSE.EnsureClassLoaded ->
7717+ -- DecodeMessage per sequence, i.e. decompress + deserialise the entire
7718+ -- library) and then walks every action of every version of every sequence
7719+ -- resolving icons, synchronously, before the window appears. A large
7720+ -- library made that seventeen seconds -- with ManageTree at 14ms and the
7721+ -- block draw at 123ms on the same open, so all of it was this.
7722+ --
7723+ -- Nothing on screen needs it: the tree carries sequence icons, not action
7724+ -- icons, and it built fine before this ran. Action icons are needed only
7725+ -- for the sequence actually being drawn, so hydration moved to
7726+ -- GSE.GUILoadEditor, per sequence, as each one is opened. That also
7727+ -- restores the lazy-load design this pass was defeating -- every other
7728+ -- read path uses GSE.EnsureSequenceLoaded for one sequence at a time.
7729+ --
7730+ -- The full-library pass is still available to the icon-scan diagnostic,
7731+ -- which is a deliberate, user-initiated sweep and reports what it changed.
76287732 local lastSequencePath = GSE .GUI .GetLastSequenceEditorPath and GSE .GUI .GetLastSequenceEditorPath ()
76297733 local classID = tostring (GSE .GetCurrentClassID and GSE .GetCurrentClassID () or " " )
76307734
@@ -7703,6 +7807,20 @@ function GSE.ShowSequences()
77037807
77047808 SetSequenceEditorOpenPreference (true , " sequences" )
77057809 editframe :Show ()
7810+ -- Split, because "the open is slow" has three candidates and they behave
7811+ -- very differently: CreateEditor builds the whole window and only on the
7812+ -- FIRST open (later opens reuse it -- which is why Keybindings feels
7813+ -- instant if Sequences was opened first), ManageTree is O(sequences), and
7814+ -- select+show covers loading the sequence and drawing its blocks.
7815+ -- @debug@
7816+ local openTotal = GSE .NowMs () - openStartedAt
7817+ ReportOpenTiming (
7818+ string.format (" ShowSequences: %.0f ms (CreateEditor %.0f, ManageTree %.0f, select+show %.0f)" ,
7819+ openTotal , afterCreate - openStartedAt ,
7820+ afterTree - afterCreate , GSE .NowMs () - afterTree ),
7821+ openTotal
7822+ )
7823+ -- @end-debug@
77067824end
77077825
77087826local function remoteSeqences (message , seqName )
@@ -7779,6 +7897,10 @@ function GSE.GUICreateNewSequence(editor, name, recordedstring)
77797897 editor .newname = nil
77807898 editor .Sequence = sequence
77817899 editor .ClassID = classid
7900+ -- A recorded sequence arrives with actions and no icons, and the tree lands
7901+ -- on its config node, so nothing would call GUILoadEditor for it this
7902+ -- session. Hydrate it here or its blocks draw blank until the next open.
7903+ hydrateSequenceIcons (sequence )
77827904 if GSE .GUI .ResetUndo then GSE .GUI .ResetUndo (editor ) end
77837905 editor .ManageTree ()
77847906 editor .treeContainer :SelectByValue (
@@ -7861,6 +7983,9 @@ function GSE.GUILoadEditor(editor, key, recordedstring)
78617983 editor .newname = nil
78627984 editor .Sequence = sequence
78637985 editor .ClassID = classid
7986+ -- Fill in any missing action icons for THIS sequence only, now that it is
7987+ -- decoded and before its blocks draw -- see the note in GSE.ShowSequences.
7988+ hydrateSequenceIcons (sequence )
78647989 if GSE .GUI .ResetUndo then GSE .GUI .ResetUndo (editor ) end
78657990end
78667991
0 commit comments