Skip to content

Conversation

@Julusian
Copy link
Member

@Julusian Julusian commented Jan 16, 2026

This adds a layer of caching to the new graphics rendering.

This layer is intended to avoid re-executing stable expressions, and to reduce the cost of computing a cache key for the graphics properties.
Because of the unlimited number of image elements, the json drawing object could easily reach multiple mb, making it a little costly to stringify and hash as a cache key.
Instead, this is computing each element separately (minus the children) and computing a hash for each element whenever it changes. The overall hash is then computed from these individual hashes.

No attempt is made at this stage to add any caching to the drawing process, that could be a follow up if there appears to be a benefit to doing so.

Summary by CodeRabbit

  • Refactor

    • Unified change-reporting to a single reportChange flow for more consistent redraw/save behavior
  • New Features

    • Per-element conversion cache and content-hash based render keys for faster button rendering
    • Expression-aware element parsing with variable tracking for dynamic graphics
    • Preview renderer now uses a text layout cache for more efficient previews
  • Bug Fixes

    • Corrected line coordinate calculations in layered rendering
  • Improvements

    • More targeted redraws and finer feedback/style-override change tracking

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

Warning

Rate limit exceeded

@Julusian has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 15 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 64ab779 and ebd5358.

📒 Files selected for processing (4)
  • companion/lib/Graphics/Controller.ts
  • companion/lib/Graphics/ConvertGraphicsElements/Helper.ts
  • companion/lib/Graphics/ElementConversionCache.ts
  • companion/test/Graphics/ConvertGraphicsElements.test.ts
📝 Walkthrough

Walkthrough

Centralizes entity-list change signaling into a single reportChange API, adds a per-element ElementConversionCache with content-hash and expression-aware graphics conversion, and threads cache/forceNewIds through preset/preview call sites while updating control/entity wiring to use the new change reporting.

Changes

Cohort / File(s) Summary
Control wiring: reportChange
companion/lib/Controls/ControlTypes/Button/Base.ts, companion/lib/Controls/ControlTypes/Button/Layered.ts, companion/lib/Controls/ControlTypes/Button/Preset.ts, companion/lib/Controls/ControlTypes/ExpressionVariable.ts, companion/lib/Controls/ControlTypes/Triggers/Trigger.ts
Replaced commitChange/invalidateControl callbacks with unified reportChange(options: ControlEntityListChangeProps). Added abstract/private entityListReportChange handlers; layered/preset hook into cache invalidation.
Entity list API & pools
companion/lib/Controls/Entities/EntityListPoolBase.ts, companion/lib/Controls/Entities/EntityListPoolButton.ts, companion/lib/Controls/Entities/EntityListPoolExpressionVariable.ts, companion/lib/Controls/Entities/EntityListPoolTrigger.ts
Added ControlEntityListChangeProps and switched pool props to require reportChange. Replaced commit/invalidate call sites with explicit reportChange({...}) payloads (redraw, noSave, changedElementIds, invalidateAllElements).
Entity list & instance changes
companion/lib/Controls/Entities/EntityList.ts, companion/lib/Controls/Entities/EntityInstance.ts
moveEntity now returns the moved ControlEntityInstance. ControlEntityInstance exposes styleOverrideAffectedElementIds. replaceStyleOverride/removeStyleOverride now return the override or null (not boolean).
Graphics conversion & cache
companion/lib/Graphics/ElementConversionCache.ts, companion/lib/Graphics/ConvertGraphicsElements.ts, companion/lib/Graphics/ConvertGraphicsElements/Helper.ts, companion/lib/Graphics/ConvertGraphicsElements/Util.ts
Added ElementConversionCache and ElementConversionCacheEntry, introduced ParseElementsContext and ElementExpressionHelper, refactored conversion pipeline to be expression-aware, added per-element contentHash computation, and made ConvertSomeButtonGraphicsElementForDrawing accept a cache parameter.
Rendering cache key & model
companion/lib/Graphics/Controller.ts, shared-lib/lib/Model/StyleLayersModel.ts, shared-lib/lib/Graphics/LayeredRenderer.ts
Render cache keys now use collected element content hashes; ButtonGraphicsDrawBase gained contentHash: string. Line coordinate handling adjusted to fractional (0–1) multipliers.
Presets / ID behavior & call sites
companion/lib/Instance/Connection/PresetsLayered.ts, companion/lib/Instance/Connection/ChildHandler.ts, companion/lib/ImportExport/Controller.ts, companion/lib/Preview/ElementStream.ts
Added forceNewIds to layered preset conversion paths (default false) and threaded through helpers. Updated call sites to pass new args; several ConvertSomeButtonGraphicsElementForDrawing calls gained the extra cache parameter (often null).
Tests & usage updates
companion/test/Controls/Entities/EntityListPool.test.ts, companion/test/Graphics/ConvertGraphicsElements.test.ts, companion/test/TextParser.perf.ts
Tests updated to use reportChange and ControlEntityListChangeProps. Added extensive conversion/cache/content-hash tests and adjusted imports/perf test imports.
UI / preview changes & deps
webui/src/Buttons/.../LayeredButtonPreviewRenderer.tsx, shared-lib/package.json, webui/package.json
Preview renderer now initializes a QuickLRU-backed text layout cache and passes it to image creation. quick-lru dependency added to package manifests.

Poem

✨ One report to call the tune, small and neat,
per-element caches hum beneath the heat.
hashes steady, expressions track the flow,
redraws whisper where the changes go.
🎨 Nice work — welcome contributions glow!

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding a graphics elements caching layer to the graphics rendering system.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Julusian Julusian marked this pull request as ready for review January 16, 2026 20:35
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
companion/lib/Controls/Entities/EntityList.ts (1)

212-219: Code structure is safe but could be clearer for maintainability.

The method is protected in practice—oldIndex always comes from findParentAndIndex() which guarantees a valid index in [0, length−1], and the caller validates with if (!oldInfo) return false. The newIndex -= 1 adjustment handles the removal correctly.

However, clamping newIndex to this.#entities.length (rather than length − 1) and relying on implicit assumptions about index validity makes the code fragile. Consider documenting that both indices are expected to be valid (or adding a validation guard) to make the contract explicit and prevent future bugs if this method is called differently.

companion/lib/Graphics/ConvertGraphicsElements/Helper.ts (1)

216-277: Consider aligning createHelper signature with actual usage.
ParseElementsContext.createHelper advertises propOverrides?: VariableValues, but the implementation ignores it. If overrides must be scoped via withPropOverrides, dropping the param avoids accidental misuse. Based on learnings, this keeps composite override scoping explicit.

companion/lib/Graphics/ConvertGraphicsElements/Util.ts (1)

14-46: Minor consideration: JSON.stringify key ordering for nested objects

The top-level keys are sorted for determinism (line 20), which is great! However, when typeof value === 'object' is true (line 33-35), JSON.stringify(value) is called on nested objects. JavaScript's JSON.stringify doesn't guarantee key ordering for nested objects by default.

For most practical cases this should be fine since the same object structure will serialize the same way within a single runtime. But if you ever need cross-session cache key stability (e.g., for persistent caching), you might want to use a deterministic JSON serializer.

Not blocking - just something to keep in mind if cache behavior ever seems inconsistent! 🙂

companion/lib/Controls/ControlTypes/ExpressionVariable.ts (1)

132-142: Small copy-paste artifact in comment

The comment on line 137 says "Elements are not relevant for triggers", but this is the ExpressionVariable control, not a trigger. Looks like a copy-paste from Trigger.ts (see lines 195-205 in that file from the relevant snippets).

Not a big deal, but a quick fix would help future readers! 😊

📝 Suggested comment fix
 	`#entityListReportChange`(options: ControlEntityListChangeProps): void {
 		if (!options.noSave) {
 			this.commitChange(false)
 		}
 
-		// Elements are not relevant for triggers
+		// Elements are not relevant for expression variables
 
 		if (options.redraw) {
 			this.triggerRedraw()
 		}
 	}
companion/lib/Graphics/ConvertGraphicsElements.ts (1)

98-199: Well-structured caching logic in convertElements

A few observations and a minor suggestion:

  1. The cache-hit path is clean - check cache, use if present, otherwise compute and store.

  2. The reference merging (lines 158-163) correctly aggregates usedVariables and compositeElements into global references.

  3. Minor suggestion (line 162-163): The braces-less if with the complex expression reads a bit dense. Consider adding braces for clarity:

-           if (cacheEntry.compositeElement?.elementId)
-               context.globalReferences.compositeElements.add(cacheEntry.compositeElement.elementId)
+           if (cacheEntry.compositeElement?.elementId) {
+               context.globalReferences.compositeElements.add(cacheEntry.compositeElement.elementId)
+           }

This is purely a readability nit - feel free to keep as-is if you prefer the current style! 😊

@Julusian Julusian force-pushed the feat/graphics-element-caching branch from ff81923 to 8dae3a9 Compare January 16, 2026 21:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
companion/lib/Graphics/Controller.ts (1)

303-348: Cache key drops location when topbar is actually shown.
On Line 314–321 the condition removes location when decoration is FollowDefault and topbar is enabled — the common case. That makes the cache ignore location precisely when topbar rendering is location-sensitive. Suggest computing showTopBar and only deleting location when it’s false.

🛠 Suggested fix
-							if (canvasElement?.decoration !== ButtonGraphicsDecorationType.TopBar && globalShowTopBar) {
-								// Location is not needed in the cache key if topbar is not shown
-								delete cacheKeyObj.location
-							}
+							const showTopBar =
+								canvasElement?.decoration === ButtonGraphicsDecorationType.TopBar || globalShowTopBar
+							if (!showTopBar) {
+								// Location is not needed in the cache key if topbar is not shown
+								delete cacheKeyObj.location
+							}
🧹 Nitpick comments (2)
companion/lib/Graphics/ElementConversionCache.ts (1)

23-27: Minor documentation suggestion: "thread-safe" terminology.

Since JavaScript is single-threaded, the "thread-safe" terminology in the comments (lines 26, 68-69, 100) might be slightly misleading. The pattern is really about batching invalidations to process them in a controlled manner, avoiding mid-operation inconsistencies.

Feel free to keep it as-is if it's clear enough in context, just wanted to mention it! 😊

companion/test/Graphics/ConvertGraphicsElements.test.ts (1)

1660-1727: Avoid hard‑coding the composite hash to reduce test brittleness.
Tying the test to an exact hash makes refactors to the hashing algorithm noisy. Consider asserting the prefix/suffix pattern instead.

♻️ Suggested adjustment
- expect(groupElement.children[0].id).toBe(
-  `comp1-44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a/inner1`
- )
+ expect(groupElement.children[0].id).toMatch(/^comp1-[^/]+\/inner1$/)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
webui/src/Buttons/EditButton/LayeredButtonEditor/Preview/LayeredButtonPreviewRenderer.tsx (1)

164-166: Optional: hoist the cache size to a named constant for easier tuning.
Keeps the knob visible and makes later adjustments more straightforward.

♻️ Suggested tweak
 const PAD_X = 10
 const PAD_Y = 10
+const TEXT_LAYOUT_CACHE_SIZE = 200
@@
-		const textLayoutCache: TextLayoutCache = new QuickLRU({ maxSize: 200 })
+		const textLayoutCache: TextLayoutCache = new QuickLRU({ maxSize: TEXT_LAYOUT_CACHE_SIZE })

@Julusian Julusian merged commit e5fca32 into develop Jan 17, 2026
18 checks passed
@Julusian Julusian deleted the feat/graphics-element-caching branch January 17, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants