Register one script change listener per script instead of per error - #71
Closed
rubensworks wants to merge 1 commit into
Closed
Register one script change listener per script instead of per error#71rubensworks wants to merge 1 commit into
rubensworks wants to merge 1 commit into
Conversation
Every created script error registered its own script change listener, to resolve that error when the script changes. Those listeners were only removed once their exception was collected AND a new exception factory was created, which does not happen while a script variable holds on to its value, so a script that keeps failing accumulated a listener per failed evaluation. Removing them afterwards was quadratic on top of that, because each one was removed separately from a list: 10000 errors left 10000 listeners behind, which took 28ms to expunge. Exceptions are now collected per script, behind a single listener that resolves all of them when that script changes, and drops them one by one as they are collected. Registering a new exception expunges the collected ones first, so failing scripts no longer accumulate them at all. These listeners belong to the scripting data they were registered on, so they are also forgotten when the server stops. Related to #67 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JCW1HmWhLT27jh57t9d7B6
Coverage Report for CI Build 33628141471Coverage increased (+1.4%) to 50.984%Details
Uncovered ChangesNo uncovered changes found. Coverage Regressions4 previously-covered lines in 2 files lost coverage.
Coverage Stats
💛 - Coveralls |
Member
Author
|
Superseded by #74, which carries the same change against Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the second half of #67: a script that keeps failing accumulates script change listeners without bound, and cleaning them up is quadratic. This is what makes the reporter's unguarded filter degrade over time rather than just being slow.
Problem
resolveOnScriptChangeregisters a script change listener for every createdEvaluationException, so it can be resolved when its script changes. Those listeners are only removed once the exception has been collected ANDexpungeStaleEvaluationExceptions()runs, and that only happens when a new exception factory is created, i.e. inGraalScript#getValue. AScriptVariablecaches its value, sogetValueis not called again while the script stays valid, and the exceptions created by every failing evaluation pile up inScriptingData's listener list.Removal is quadratic on top of that: each listener is removed separately from an
ArrayList, so expunging k of n listeners is O(n·k), in one tick.Measured with a throwaway test on
master-26-lts:Fix
Exceptions are now grouped per script behind a single
ScriptExceptionsListener, which:resolveOnScriptChangealso expunges first, so a repeatedly failing script no longer accumulates exceptions between evaluations.These listeners belong to one
ScriptingDatainstance, so they are forgotten inonServerStoppingalongside it. Without that, listeners registered before a server stop would keep the old keys around and never be registered on the next server's scripting data.Tests
EvaluationExceptionResolutionHelpersTestcovers the listener count for many exceptions, one listener per script rather than per exception, resolution on script change (and only for the matching script), and listener removal once the exceptions are collected. Verified that the same test file fails onmaster-26-ltswithout the fix:testExceptionsAreResolvedOnScriptChangepasses both before and after, as the behaviour it checks is unchanged../gradlew spotlessApply buildpasses with the fix. I could not runrunGameTestServerin this environment.Note
This removes the leak, not the cost of the exception itself. An exception escaping a script measures around 0.45ms in a benchmark against 0.018ms for the same script without one, and the cost is the guest-to-host boundary: the same JS error caught inside JS is 0.010ms, and reading
getMessage()adds nothing measurable. Scripts in hot paths should still avoid throwing.🤖 Generated with Claude Code
https://claude.ai/code/session_01JCW1HmWhLT27jh57t9d7B6
Generated by Claude Code