Skip to content

(rsg) Fix stack overflow from ContentNode parentField notification cascade#905

Open
jeremy-albinet wants to merge 2 commits intolvcabral:masterfrom
jeremy-albinet:fix/grid-contentnode-stack-overflow
Open

(rsg) Fix stack overflow from ContentNode parentField notification cascade#905
jeremy-albinet wants to merge 2 commits intolvcabral:masterfrom
jeremy-albinet:fix/grid-contentnode-stack-overflow

Conversation

@jeremy-albinet
Copy link

@jeremy-albinet jeremy-albinet commented Mar 2, 2026

Summary

Fixes recursive SceneGraph observer cascades that can cause stack overflows in ContentNode update loops (issue #904), using a field-level notification dispatcher instead of global recursion guards.

Root Cause

ContentNode parent-field observers can trigger setValue() on other nodes, which can synchronously notify back and form a recursive loop.

Changes

  • Field.notifyObservers() now uses a queued, iterative dispatcher.
  • Notifications are coalesced per field within a dispatch batch.
  • Removed global recursion depth protection in Node.setValue().
  • Added regression test:
    • test/brsTypes/ContentNodeRecursion.test.js

Why this approach

  • Avoids stack growth from recursive observer chains.
  • Keeps notifications deterministic and local to field dispatch logic.
  • Avoids node-type-specific/static guards that can hide legitimate notifications.

Validation

  • npm test -- test/brsTypes/ContentNodeRecursion.test.js --runInBand
  • npm test -- test/brsTypes/Serializer.test.js --runInBand
  • npm test -- test/cli/cli.test.js --runInBand -t "SceneGraph Observers Test"

@jeremy-albinet
Copy link
Author

A note on the approach: I want to be upfront that the MAX_SETVALUE_DEPTH guard in Node.ts is more of a safety net than a proper fix — it's a magic number that silently drops setValue calls, which is blunt. The ContentNode.ts reentrancy guard is reasonable but uses a static flag that could be overly broad in edge cases.

The cleanest change here is the ArrayGrid.ts one (setValueSilent for item setup), which is correct regardless.

The deeper question is: why doesn't real Roku firmware recurse here? It likely either queues field notifications asynchronously or has built-in reentrancy protection at the observer dispatch level. Matching that behavior would be the proper fix.

I'm happy to rework this if you have a preferred approach — you know the engine internals far better than I do. Feel free to cherry-pick just the parts that make sense.

@jeremy-albinet jeremy-albinet marked this pull request as draft March 2, 2026 00:26
@lvcabral lvcabral self-requested a review March 2, 2026 00:38
@lvcabral
Copy link
Owner

lvcabral commented Mar 2, 2026

@jeremy-albinet thanks for the PR, I had this recursiveness issue on my backlog to take a look. A similar approach exists in Field to prevent stack overflow. I'm not sure how Roku handles this, as you said, it must have some queue.

Please use npm run prettier:write before submit, to format the source, otherwise the build fails.

I will review and test your code and update my review.

@lvcabral
Copy link
Owner

lvcabral commented Mar 2, 2026

@jeremy-albinet do you have simple way to reproduce this issue ? Can you add it on the CLI tests?
Check how it was done in test/cli/resources/observer-app

Replace global recursion guards with field-level queued/coalesced notification dispatch.

Remove Node setValue depth cap and ContentNode static parent guard, and add a regression test that reproduces cross-node parentField observer loops.
@jeremy-albinet jeremy-albinet force-pushed the fix/grid-contentnode-stack-overflow branch from f07dc4b to a1b4c8a Compare March 2, 2026 00:58
@jeremy-albinet
Copy link
Author

jeremy-albinet commented Mar 2, 2026

Updated this PR with a cleaner fix and amended commit (a1b4c8a8).

What changed:

  • Replaced recursion-guard style handling with field-level queued/coalesced observer dispatch in Field.
  • Removed global Node.setValue() depth cap.
  • Added a regression test that reproduces cross-node parent-field observer recursion and verifies it no longer crashes.

@lvcabral lvcabral marked this pull request as ready for review March 2, 2026 01:16
…ecursion fix

Add two CLI test apps exercising distinct recursion vectors:
- contentnode-recursion-app: cross-field observer recursion via 1200
  duplicate parentField-propagated observers (stack overflow without fix)
- contentnode-parentfield-app: parentField notification cycle with
  observer stacking inside callbacks (escalating callback counts)

Replace old monkeypatched unit test with proper end-to-end CLI tests.
Add JSDoc on Field.flushNotificationQueue and CHANGELOG entry.
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 2, 2026

@jeremy-albinet
Copy link
Author

Pushed the CLI repro tests you asked for.

Two test apps (same pattern as observer-app): one with mass duplicate observers, one with observer stacking inside callbacks — different recursion paths, both overflow without the fix.

Also cleaned up: removed the old unit test, added CHANGELOG entry and JSDoc on the dispatch method.

Happy to adjust anything — JSDoc verbosity, changelog wording, test structure — feel free to push changes directly to my branch too.

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