From ea42c2112154373cb9b18cea8815cc7949f56cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 9 Jul 2026 21:06:57 -0300 Subject: [PATCH] Fix Android CI. --- ...l_gui_html_optimization_plan_2026_07_09.md | 405 ++++++++++++++++++ src/eepp/window/backend/SDL2/windowsdl2.cpp | 2 + src/eepp/window/backend/SDL3/windowsdl3.cpp | 2 + 3 files changed, 409 insertions(+) create mode 100644 .agent/plans/eepp_superluminal_gui_html_optimization_plan_2026_07_09.md diff --git a/.agent/plans/eepp_superluminal_gui_html_optimization_plan_2026_07_09.md b/.agent/plans/eepp_superluminal_gui_html_optimization_plan_2026_07_09.md new file mode 100644 index 000000000..22f5f205d --- /dev/null +++ b/.agent/plans/eepp_superluminal_gui_html_optimization_plan_2026_07_09.md @@ -0,0 +1,405 @@ +# eepp Superluminal GUI/HTML Optimization Plan - 2026-07-09 + +## Goal + +Use the Superluminal capture from `2026-07-09_19-52-19_eepp-unit_tests` to identify cheap, low-risk optimization work in eepp's GUI, text, and HTML compatibility layers. This plan intentionally ignores unit-test-specific hotspots such as `EE::Graphics::Image::diff()` and focuses on code paths likely to matter in real applications and `ecode`. + +Session analyzed: + +```text +/home/downloads/2026-07-09_19-52-19_eepp-unit_tests.slp +``` + +MCP could not open the packaged `.slp` directly, so it was extracted to `/tmp/superluminal-eepp-unit-tests` and the contained `.session` was queried. + +## Capture Notes + +- Capture duration: ~29.7 s. +- Process: `eepp-unit_tests`. +- Hot thread: `Job Scheduler 1`, ~20.69 s executing. +- Dominant ignored costs: + - `EE::Graphics::Image::diff`: ~1.93 s exclusive / ~10.74 s inclusive. + - libm `pow`, `cbrt`, `cos`, `atan2`, `sin`: mostly driven by image comparison and rendering tests. + - repeated application/window teardown costs are visible but not the main target here. + +Existing related plan: + +```text +.agent/plans/eepp_css_selector_optimization_plan.md +``` + +CSS selector matching still appears in this capture, but that file already covers the larger selector-indexing and correctness work. Items below only include selector-adjacent tasks when they are smaller or complementary. + +## Top Relevant Hotspots + +Approximate aggregate timings from the capture: + +| Area | Evidence | +| --- | --- | +| Dirty layout invalidation | `UISceneNode::invalidateLayout`: ~120 ms exclusive / ~159 ms inclusive, 1031 calls | +| HTML rich text rebuild | `UIRichText::rebuildRichText`: ~1.24 s inclusive; internal lambda ~37.8 ms exclusive / ~1.19 s inclusive | +| Block layout | `BlockLayouter::updateLayout`: ~1.57 s inclusive, dominated by `UIRichText::rebuildRichText` and `RichText::updateLayout` | +| RichText layout/wrap | `RichText::updateLayout`: ~609 ms inclusive; `RichTextInlineLayouter::layoutNoFloats`: ~507 ms inclusive; `LineWrap::computeLineBreaksInternal`: ~299 ms inclusive | +| White-space inheritance lookup | `getEffectiveWhiteSpaceCollapse`: ~406 ms inclusive, 3235 calls | +| Text width measurement | `Text::updateWidthCache`: ~142 ms inclusive, 916 calls | +| Font glyph/kerning lookup | `FontTrueType::getGlyphByIndex`: ~338 ms inclusive, 2189 calls; `getKerning`: ~72 ms inclusive, 525 calls | +| CSS length parsing | `StyleSheetLength::fromString`: ~44.7 ms exclusive / ~106.9 ms inclusive, 872 calls | +| CSS style selection | `StyleSheet::getElementStyles`: ~154 ms inclusive; `StyleSheetSelector::select`: ~116 ms inclusive; `StyleSheetSelectorRule::matches`: ~55 ms inclusive | +| HTML load/style reload | `UIHTML_KittyHomeSmallDoesNotHang` branch: `loadLayoutFromString` ~199 ms, recursive `reloadStyle` ~101 ms, `updateDirtyLayouts` ~185 ms | + +## Ranked Plan + +### 1. Reduce `UISceneNode::invalidateLayout()` dirty-list scanning + +Files: + +```text +src/eepp/ui/uiscenenode.cpp +include/eepp/ui/uiscenenode.hpp +include/eepp/ui/uilayout.hpp +``` + +Current hot section: `src/eepp/ui/uiscenenode.cpp:1173`. + +The expensive part is the second pass over `mDirtyLayouts`, where every new dirty layout may walk ancestors of every already-dirty layout to remove descendants. In this capture, most exclusive time is in `layout->getParent()`, `it->isLayout()`, and climbing ancestors. + +Proposed cheap path: + +- Add a fast path for the common case where the dirty set is empty or has one element. +- Track a per-layout `dirtyAncestor` or contiguous-layout-root/depth helper so descendant checks can avoid repeated full ancestor walks. +- Consider storing dirty layouts in depth order and stopping descendant checks early when the existing layout is not deeper than the new node. +- Keep current behavior as the correctness fallback until measured. + +Validation: + +- Existing layout invalidation tests. +- Add a focused test with nested layouts separated by non-layout widgets to preserve the current "only contiguous layout path coalesces" invariant. + +### 2. Cache white-space collapse and text-transform during rich-text rebuild + +Files: + +```text +src/eepp/ui/uirichtext.cpp +include/eepp/ui/uirichtext.hpp +``` + +Current hot sections: + +```text +src/eepp/ui/uirichtext.cpp:1490 +src/eepp/ui/uirichtext.cpp:1846 +src/eepp/ui/uirichtext.cpp:1969 +src/eepp/ui/uirichtext.cpp:2003 +``` + +`getEffectiveWhiteSpaceCollapse()` walks ancestors and calls `isType()` / `hasLocalProperty()` repeatedly. It was called 3235 times and cost ~406 ms inclusive. `getEffectiveTextTransform` performs a similar ancestor walk inside `rebuildRichText()`. + +Proposed cheap path: + +- Carry inherited `WhiteSpaceCollapse` and `TextTransform` as traversal state in `UIRichText::rebuildRichText()` instead of recomputing from each text node. +- Reuse the computed `collapse` for the inline-span post-processing at `uirichtext.cpp:2003` instead of calling `getEffectiveWhiteSpaceCollapse()` again. +- For correctness, update traversal state only when entering `UIRichText` / `UITextSpan` nodes with local relevant properties. + +Validation: + +- Existing `UIHTML.*white*`, `UIRichText.*`, and text-transform tests. +- Add a nested span test with inherited `white-space`, local `white-space-collapse`, and nested text-transform override. + +### 3. Fast-parse common CSS lengths without allocations + +Files: + +```text +src/eepp/ui/css/stylesheetlength.cpp +include/eepp/ui/css/stylesheetlength.hpp +``` + +Current hot section: `src/eepp/ui/css/stylesheetlength.cpp:391`. + +`StyleSheetLength::fromString()` allocates `num`, allocates `unit` via `substr`, lower-hashes the whole string before knowing whether it is a keyword, and may repeatedly call `String::fromString()` while popping characters. + +Proposed cheap path: + +- First scan a trimmed `std::string_view`. +- If the first non-sign character is numeric or `.`, parse numeric prefix directly and pass the unit suffix as `std::string_view`. +- Add `unitFromString(std::string_view)` and avoid constructing `unit`. +- Only call `String::hashToLower()` for non-numeric keyword candidates. +- Keep the existing function-parser path for `calc()`, `min()`, `max()`, and `clamp()`. + +Validation: + +- CSS length parser tests for signed values, decimals, percentages, unitless `0`, `pxAsDp`, keywords, and function strings. + +### 4. Avoid duplicate content-offset and size work in `UIRichText::rebuildRichText()` + +Files: + +```text +src/eepp/ui/uirichtext.cpp +``` + +Current hot section: `src/eepp/ui/uirichtext.cpp:2038`. + +The custom-block branch repeatedly calls `container->getPixelsContentOffset()` and `container->getPixelsSize()` while computing available width, then repeats equivalent work for `customSize`. It also calls `setPixelsSize()` in paths where the computed size may be unchanged. + +Proposed cheap path: + +- Compute container content offset, current pixel size, inner width, and layout policies once before traversing children. +- In the fill-parent branch, compare against the current child size before calling `setPixelsSize()`. +- Reuse the computed available width for both `setPixelsSize()` and custom block width. + +Validation: + +- Existing inline-block, block, float, and wrap-content layout tests. +- Add a focused test that verifies no layout change when a fill-parent child already has the computed size. + +### 5. Eliminate redundant `RichText::updateLayout()` calls for inline-block baselines + +Files: + +```text +src/eepp/ui/uirichtext.cpp +src/eepp/ui/blocklayouter.cpp +include/eepp/graphics/richtext.hpp +``` + +Current hot sections: + +```text +src/eepp/ui/uirichtext.cpp:1531 +src/eepp/ui/uirichtext.cpp:2144 +src/eepp/ui/blocklayouter.cpp:125 +src/eepp/ui/blocklayouter.cpp:127 +``` + +`getAtomicInlineBoxBaseline()` calls `rt->updateLayout()` to read lines, and later the inline-block path may call `getRichTextPtr()->updateLayout()` again. `BlockLayouter::updateLayout()` also does a full rebuild/update sequence. + +Proposed cheap path: + +- Add an `ensureLayoutUpdated()` or dirty-generation check on `RichText` so repeated calls in the same rebuild are no-ops. +- When `getAtomicInlineBoxBaseline()` only needs the last in-flow line baseline, expose a cached baseline from the child widget/layouter after its own layout. +- Avoid calling `updateLayout()` at `uirichtext.cpp:2144` if baseline lookup already forced the same layout generation. + +Validation: + +- Inline-block baseline tests, nested `details/summary`, and rich-text baseline tests. + +### 6. Share glyph advance/kerning work between text width and line wrap + +Files: + +```text +src/eepp/graphics/text.cpp +src/eepp/graphics/linewrap.cpp +src/eepp/graphics/fonttruetype.cpp +include/eepp/graphics/font.hpp +include/eepp/graphics/fonttruetype.hpp +``` + +Current hot sections: + +```text +src/eepp/graphics/text.cpp:1543 +src/eepp/graphics/linewrap.cpp:70 +src/eepp/graphics/fonttruetype.cpp:736 +src/eepp/graphics/fonttruetype.cpp:947 +``` + +Both `Text::updateWidthCache()` and `LineWrap::computeLineBreaksInternal()` scan strings and repeatedly call `getGlyph()` and `getKerning()`. The line-wrap source annotation shows most time inside glyph advance lookup; width cache shows the same pattern. + +Proposed cheap path: + +- Introduce a small text-run metrics helper used by both width-cache and line-wrap code. +- Cache space advance once per run. +- For TTF fonts, resolve glyph index once per codepoint and use glyph-index APIs for both glyph lookup and kerning. +- Return or reference just the metrics needed by these loops instead of copying full `Glyph` objects when only `advance`, `lsbDelta`, and `rsbDelta` are needed. + +Validation: + +- Existing font rendering tests. +- Focused width and wrap tests for tabs, newlines, CR, kerning-enabled proportional fonts, and monospace fast path. + +### 7. Add a cheaper kerning path from already-known glyph indices + +Files: + +```text +src/eepp/graphics/fonttruetype.cpp +include/eepp/graphics/fonttruetype.hpp +``` + +Current hot section: `src/eepp/graphics/fonttruetype.cpp:947`. + +`FontTrueType::getKerning()` computes a cache key from codepoints, then calls `getGlyph()` for both codepoints, then calls `getGlyphIndex()` again. There is already a `getKerningFromGlyphIndex()` entry point nearby. + +Proposed cheap path: + +- Let the text-run helper call `getKerningFromGlyphIndex()` when it already resolved indices. +- Confirm whether `getKerningFromGlyphIndex()` preserves fallback-font correctness. If not, add an index-plus-font-id key variant. +- Keep the public codepoint path unchanged for external callers. + +Validation: + +- Kerning tests across normal TTF, fallback glyphs, outline thickness, bold, and italic. + +### 8. Reserve and reuse rich-text inline layout buffers + +Files: + +```text +src/eepp/graphics/richtext.cpp +include/eepp/graphics/richtext.hpp +``` + +Relevant profiler entries: + +```text +RichTextInlineLayouter::rebuildFragments: ~75.5 ms inclusive +RichTextInlineLayouter::appendLayoutRuns: ~25.2 ms inclusive +std::vector::emplace_back: ~15 ms inclusive +RichText::clear: ~21.9 ms inclusive +``` + +Proposed cheap path: + +- Preserve vector capacity across `RichText::clear()` for inline items, render paragraphs, spans, and layout runs. +- Reserve based on previous rebuild sizes or current child/text-node count before `UIRichText::rebuildRichText()` starts pushing items. +- Keep memory bounded by shrinking only after unusually large transient layouts. + +Validation: + +- RichText layout tests and ASAN run. +- Add a stress test that rebuilds a long inline-heavy text repeatedly and verifies stable output. + +### 9. Batch style reload side effects during HTML/layout loading + +Files: + +```text +src/eepp/ui/uiscenenode.cpp +src/eepp/ui/uiwidget.cpp +src/eepp/ui/uistyle.cpp +src/eepp/ui/uihtmlwidget.cpp +``` + +Capture branch: + +```text +UIHTML_KittyHomeSmallDoesNotHang + UISceneNode::loadLayoutFromString: ~199 ms + UIWidget::reloadStyle recursion: ~101 ms + UIStyle::onStateChange / applyStyleSheetProperty visible below it +``` + +Proposed cheap path: + +- During `loadLayoutNodes()`, defer inherited property propagation and layout invalidation until a subtree is attached and initial style application is complete. +- Reuse the existing attributes transaction mechanism where possible. +- Add a scene-level "loading style batch" guard that queues dirty style/layout once per node instead of repeatedly during recursive construction. + +Validation: + +- Existing layout loading tests. +- HTML fixture tests involving inherited color/font/white-space and immediate layout after load. + +### 10. Broaden hot accessor/type-check inlining beyond selector code + +Files: + +```text +include/eepp/scene/node.hpp +include/eepp/ui/uiwidget.hpp +include/eepp/ui/uinode.hpp +include/eepp/ui/uilayout.hpp +``` + +The selector optimization plan already lists some inlining work. This capture shows the same issue in rich-text and layout code: + +```text +Node::getParent: ~37.7 ms exclusive +Node::getType: ~36.1 ms exclusive +UIWidget::isType: ~37.6 ms exclusive +UINode::isType: ~26.9 ms exclusive +UIHTMLWidget::isType / getType: ~32.0 ms combined +``` + +Proposed cheap path: + +- Execute the accessor-inline phase from the CSS selector plan, but validate impact on rich-text/layout too. +- Include `getType()`/`isType()` candidates for `UINode`, `UIWidget`, `UILayout`, `UIHTMLWidget`, `UIRichText`, and table element subclasses where definitions are trivial. +- Keep ABI/ODR constraints in mind; remove matching out-of-line definitions where required. + +Validation: + +- Full build and unit test smoke. +- `git diff --check` and one focused HTML/rich-text filter. + +### 11. Keep CSS selector optimization in the existing dedicated plan + +Files: + +```text +.agent/plans/eepp_css_selector_optimization_plan.md +src/eepp/ui/css/stylesheet.cpp +src/eepp/ui/css/stylesheetselector.cpp +src/eepp/ui/css/stylesheetselectorrule.cpp +``` + +Capture evidence still supports that plan: + +```text +StyleSheet::getElementStyles: ~154 ms inclusive, 1084 calls +StyleSheetSelector::select: ~116 ms inclusive +StyleSheetSelectorRule::matches: ~55 ms inclusive +``` + +Do not duplicate that work here. When executing this plan, treat selector indexing, sibling combinator correctness, and class/hash matching as owned by `eepp_css_selector_optimization_plan.md`. + +Small complementary task: + +- If a style reload batching change is implemented first, rerun the same capture or filtered HTML tests before starting selector indexing. Batching may reduce selector call volume and change the measured priority. + +### 12. Add a repeatable profiling harness for these focused cases + +Files: + +```text +projects/scripts/ +.agent/plans/ +src/tests/unit_tests/ +``` + +The full unit-test capture is useful but heavily distorted by screenshot/image diff work. Add a small documented workflow for targeted captures: + +- Full HTML fixture subset. +- RichText-only subset. +- Text/font width/wrap subset. +- CSS parser/style subset. + +Suggested filters: + +```text +UIHTML.* +UIRichText.* +RichText.* +FontRendering.*Text* +``` + +The goal is not new product code, but a repeatable before/after measurement path before implementing larger changes. + +## Suggested Execution Order + +1. Fast local wins: CSS length parsing, white-space collapse reuse, duplicate content-offset/size work. +2. Dirty layout invalidation coalescing. +3. RichText update-layout de-duplication and buffer reuse. +4. Text/glyph/kerning metrics helper. +5. Style reload batching. +6. Existing CSS selector optimization plan. + +## Non-Goals For This Pass + +- Optimizing `Image::diff()` or image comparison tests. +- Rewriting the whole HTML layout architecture. +- Large selector-index rewrites already described in `eepp_css_selector_optimization_plan.md`. +- Changing browser compatibility semantics for whitespace, inline-block baselines, floats, or flex/grid blockification. diff --git a/src/eepp/window/backend/SDL2/windowsdl2.cpp b/src/eepp/window/backend/SDL2/windowsdl2.cpp index 2f918463c..086483b25 100644 --- a/src/eepp/window/backend/SDL2/windowsdl2.cpp +++ b/src/eepp/window/backend/SDL2/windowsdl2.cpp @@ -323,7 +323,9 @@ void WindowSDL::setGLContextThread() { void WindowSDL::unsetGLContextThread() { SDL_GL_MakeCurrent( mSDLWindow, nullptr ); +#if SDL_VERSION_ATLEAST( 2, 0, 16 ) SDL_TLSCleanup(); +#endif mGLContextMutex.unlock(); } diff --git a/src/eepp/window/backend/SDL3/windowsdl3.cpp b/src/eepp/window/backend/SDL3/windowsdl3.cpp index af1043014..180e458c9 100644 --- a/src/eepp/window/backend/SDL3/windowsdl3.cpp +++ b/src/eepp/window/backend/SDL3/windowsdl3.cpp @@ -298,7 +298,9 @@ void WindowSDL::setGLContextThread() { void WindowSDL::unsetGLContextThread() { SDL_GL_MakeCurrent( mSDLWindow, nullptr ); +#if SDL_VERSION_ATLEAST( 3, 2, 0 ) SDL_CleanupTLS(); +#endif mGLContextMutex.unlock(); }