Skip to content

Fix line-by-line reveal off-by-one and stage display memory leaks#3145

Merged
vassbo merged 2 commits intoChurchApps:devfrom
vicholz:fix/line-reveal-and-stage-display-perf
Apr 9, 2026
Merged

Fix line-by-line reveal off-by-one and stage display memory leaks#3145
vassbo merged 2 commits intoChurchApps:devfrom
vicholz:fix/line-reveal-and-stage-display-perf

Conversation

@vicholz
Copy link
Copy Markdown
Contributor

@vicholz vicholz commented Apr 9, 2026

Changes

This PR fixes two user-facing bugs in FreeShow:

  1. "Reveal line by line" off-by-one: The last line of a slide was never revealed when using the line-by-line reveal feature, caused by a mismatch between how lines are counted vs how they are filtered for rendering.
  2. Stage display lag/freeze: The stage display output would get progressively out of sync, lag, and eventually freeze after cycling through several slides/shows, caused by multiple compounding memory leaks and performance issues.

Modules/Code Changed

src/frontend/components/helpers/showActions.ts - Fixed getItemWithMostLines to use text.value !== undefined instead of text.value.length, aligning the line counting filter with the rendering filter in TextboxLines.svelte. Previously, lines with empty-string text values (e.g., blank lines between verses) were not counted, causing maxRevealLines to be smaller than the number of rendered lines, so the last line(s) could never be revealed.

src/server/stage/components/Stagebox.svelte - Added onDestroy lifecycle cleanup for a setInterval that was leaking on every component teardown. Since Slide.svelte wraps each Stagebox in {#key $stageLayout} (destroying/recreating on layout changes), each slide change leaked an interval. After cycling through many slides, hundreds of orphaned intervals would overwhelm the event loop.

src/server/stage/util/socket.ts - Replaced the per-request socket.on("STAGE", receiver) pattern in awaitRequest() with a single persistent listener and a pendingRequests Map keyed by listenerId. The old approach added a new listener for every dynamic value request; concurrent requests caused O(n) processing per incoming message.

src/server/stage/helpers/show.ts - Added pruneCache() to cap the cached object at 100 entries and remove isRequested entries older than 60 seconds. Both data structures were module-level and grew without bound for the lifetime of the page.

src/frontend/components/output/Output.svelte - Replaced 11+ JSON.stringify double-serialization comparisons in reactive $: statements with cached-previous-value patterns. Each reactive statement was serializing both the old and new value on every store update; now only the new value is serialized and compared against the cached string.

src/frontend/utils/sendData.ts - Added a MAX_SENT_CHANNELS cap (50) to the sent deduplication cache, pruning oldest entries when exceeded. The cache previously retained JSON.stringify(msg.data) for every channel indefinitely.

src/frontend/utils/listeners.ts - Increased the outputs store subscription debounce from 1ms to 15ms. The previous 1ms debounce was essentially no debounce, causing a flood of IPC messages to all output windows during rapid slide navigation.

src/frontend/components/output/layers/SlideContent.svelte - Added a generation counter (updateGeneration) to the four-level nested setTimeout chain in updateItems(). Previously, only the outermost timeout was cancelable; rapid re-invocations could leave inner timeouts from stale calls executing against outdated state.

New Logic

Generation counter for nested timeouts (SlideContent.svelte): Each call to updateItems() increments updateGeneration. Every nested setTimeout callback checks if (gen !== updateGeneration) return before executing, ensuring stale transition chains from prior invocations are discarded.

Persistent socket listener with pending-request map (socket.ts): A single socket.on("STAGE", ...) listener is registered once via initRequestListener(). Each awaitRequest() call stores its { resolve, timeout } in a pendingRequests Map keyed by listenerId. When a response arrives, the listener looks up and resolves the matching pending request, avoiding repeated add/remove listener cycles.

Cache pruning (show.ts): pruneCache() is called on each replaceDynamicValues() invocation. It trims cached to the most recent 100 entries and removes isRequested timestamps older than 60 seconds.

Breaking Changes

None. All changes are internal bug fixes and performance improvements with no API or behavioral changes for end users.

Testing

Manual Testing Steps

  1. Create a slide with multiple text lines including empty lines between verses
  2. Enable "reveal line by line" on the text item
  3. Advance through the lines using next slide - verify the last line is now revealed
  4. Open a stage display (web-based or output window)
  5. Cycle through 20+ slides rapidly
  6. Verify the stage display remains in sync and does not freeze or lag

Automated Tests

No automated tests were added - this project does not have an existing test suite for these components.

Notes

  • The {#key $stageLayout} in src/server/stage/components/Slide.svelte was evaluated for removal but kept for correctness, since Stagebox.svelte has non-reactive initialization (let itemStyles = getStyles(...)) that requires full teardown/recreate. With the interval leak now fixed via onDestroy, the {#key} no longer causes resource leaks.
  • The outputs debounce was set to 15ms as a balance between responsiveness (smooth visual updates) and preventing message storms. This can be tuned if needed.

Fix the "reveal line by line" feature where the last line was never
revealed due to a mismatch between line counting (getItemWithMostLines)
and the rendering filter (TextboxLines.svelte).

Fix stage display output getting out of sync, lagging, and freezing
after cycling through slides by addressing multiple compounding issues:
leaked setInterval in Stagebox.svelte, socket listener accumulation in
awaitRequest, unbounded caches, excessive JSON.stringify in reactive
statements, insufficient debouncing, and uncancelable nested timeouts.

Made-with: Cursor
@vassbo vassbo changed the base branch from main to dev April 9, 2026 07:01
@vassbo
Copy link
Copy Markdown
Collaborator

vassbo commented Apr 9, 2026

Nice, thanks. Seems good to me.

@vassbo vassbo merged commit b33a77c into ChurchApps:dev Apr 9, 2026
vassbo added a commit that referenced this pull request Apr 9, 2026
* Handle full-section Planning Center repeats (#3099)

* Fixed some timeline keyframes not editable #3100

* Timer now flashes in StageShow #3101

* Fixed timer item creating a new timer

* Fixed some text inputs not working #3103

* Trim metadata txt import #3050

* Disable full group in groups mode #3094

* Media item thumbnails in StageShow #3003

* Added slide keyframe color
- Tweaks

* Updated Italian language

* Tweaks

* Ignore planning center keywords (#3117)

* Initial plan

* fix: ignore planning center song keywords

Agent-Logs-Url: https://github.com/otonielpv/FreeShow/sessions/ded90b8b-4d74-468d-bb88-5514bbbe4321

Co-authored-by: otonielpv <61138950+otonielpv@users.noreply.github.com>

* test: polish planning center keyword coverage

Agent-Logs-Url: https://github.com/otonielpv/FreeShow/sessions/ded90b8b-4d74-468d-bb88-5514bbbe4321

Co-authored-by: otonielpv <61138950+otonielpv@users.noreply.github.com>

* Eliminar planningCenterSongKeywords.test.ts

* fix: expand planning center keyword filtering

Agent-Logs-Url: https://github.com/otonielpv/FreeShow/sessions/60851d75-a7c2-4dc5-866c-cbf925d3dc85

Co-authored-by: otonielpv <61138950+otonielpv@users.noreply.github.com>

* Eliminar planningCenterSongKeywords.test.ts

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* Fixed shortcut uppercase not working #3125

* Updated languages

* Fixing bonjour issue #1637

* Slide Out transition inverted to make sense

* Tweaks

* Slide timeline keyframe curve editor (#3143)

* Easing curve editor

* Working easing editor

* Cleanup timeline easing

* Fixes

* Add Continuous Loop Scrolling Option for Text Items (#3144)

* Commented around code area to work on

* Pre merge commit

* Completed functional continuous scrolling

* Cleaned up slightly - Issues with jumping and whitespace

* Fixed jumping problem

* Finished UI integration for continuous scrolling effect

* Cleaned up comments and changes

* Fixed error with continuous looping true and scrolling type none

* Cleaned up code

* Removed continuous option and made all scrolling continuous

* Updated Swedish language

* Fixed opening slide timeline breaking some timeline playback #3121

* Tweaks

* Fix line-by-line reveal off-by-one and stage display memory leaks (#3145)

* Fix line-by-line reveal off-by-one and stage display memory leaks

Fix the "reveal line by line" feature where the last line was never
revealed due to a mismatch between line counting (getItemWithMostLines)
and the rendering filter (TextboxLines.svelte).

Fix stage display output getting out of sync, lagging, and freezing
after cycling through slides by addressing multiple compounding issues:
leaked setInterval in Stagebox.svelte, socket listener accumulation in
awaitRequest, unbounded caches, excessive JSON.stringify in reactive
statements, insufficient debouncing, and uncancelable nested timeouts.

Made-with: Cursor

* Cleanup

---------

Co-authored-by: Victor <vholz@salesforce.com>
Co-authored-by: Kristoffer <kristoffervassbo@gmail.com>

* Fixed MIDI velocity issue #3132

* Fixes

* Fixed potential memory leaks

* Trying to fix startup issue

* Fixed scripture style template override style

* Style override font size should be relative to actual font size #3039

* Start project item by name action #3137

* Fixed local provider shows overwritten #3041

* Slide timeline fixes

* Tweaks

* Fixes

* Version update
- Package audit fix

---------

Co-authored-by: Otoniel Pérez Velarde <61138950+otonielpv@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Joshua Sean Coulter <144730072+TheFlugeler@users.noreply.github.com>
Co-authored-by: Victor <vicholz@gmail.com>
Co-authored-by: Victor <vholz@salesforce.com>
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