Keep exception and string display from populating string caches - #648
Merged
Merged
Conversation
lagergren
force-pushed
the
lagergren/569-7-exceptionhandle
branch
from
September 26, 2026 08:37
f2b41e3 to
ce01cc5
Compare
lagergren
force-pushed
the
lagergren/569-7-exceptionhandle
branch
2 times, most recently
from
September 26, 2026 08:58
51687a0 to
7ebb554
Compare
lagergren
force-pushed
the
lagergren/569-7-exceptionhandle
branch
from
September 26, 2026 09:05
7ebb554 to
4003ca2
Compare
ggleyzer
reviewed
Sep 26, 2026
ggleyzer
reviewed
Sep 26, 2026
lagergren
force-pushed
the
lagergren/569-7-exceptionhandle
branch
from
September 26, 2026 21:37
611fb1e to
41b2cd6
Compare
ggleyzer
approved these changes
Sep 26, 2026
…emoizing
Two handle toString()s did more than display should, in the same way.
ExceptionHandle.toString() read its message with getField(null, "text") and rendered it with
StringHandle.getStringValue(). getField() reads the field layout, which is null until
ensureFieldLayout() has built it, and when the property is absent it calls
missingPropertyException(), allocating a DeferredCallHandle around a freshly constructed exception
handle - so rendering an exception could construct another one. This matters beyond a debugger,
because WrapperException.toString() delegates here and therefore runs on every stack-trace print.
peekField() returns the same handle in the case that renders: for a non-transient field of a built
layout both it and getField() end at m_aFields[field.getIndex()]. In every other case getField()
allocates, throws, or routes through a null frame, while peekField() returns null - and at the call
site null and a DeferredCallHandle are alike "not a StringHandle", so the rendered text is the same
either way. A transient "text" would previously have called getTransientField(null, field) with a
null frame, so that path was already broken rather than newly unsupported.
isFieldLayoutComputed() is the guard peekField() needs: false by default on TypeComposition, and the
real answer on ClassComposition, which owns the map.
StringHandle.toString() called getStringValue(), which memoizes:
return sValue == null ? (m_sValue = new String(m_achValue)) : sValue;
The write is idempotent, but it is still a write performed by the act of observing. Both sites now
read the cache when it is present and build a throwaway String when it is not, which is the same
content getStringValue() would have produced and stored.
xString's builder goes with it: Handy.quotedString(s) is exactly the append('"')/appendString/
append('"') sequence it was doing by hand, so the method is now one expression and matches how
ExceptionHandle renders the same thing.
Verified by running two modules under master's javatools and this branch's - one throwing a caught
and an uncaught exception, one carrying a string with a quote, a backslash and a newline through
both a console print and an exception message. Both render identically, timestamp aside.
No unit test: both StringHandle and the handles peekField() reads require a TypeComposition to
construct, xString.makeHandle() goes through INSTANCE.getCanonicalClass(), and ObjectHandle.toString()
dereferences the composition - so a test needs either a live container or a fake spanning 25 abstract
methods. That is disproportionate here, and the equivalences above are by construction.
Exercise actual StringHandle, ExceptionHandle, wrapper rendering and Java stack traces using the installed XDK. Both regressions fail with the previous rendering methods because display populates the string cache; the corrected implementation preserves cold and warm cache state and escaped output. Remove isFieldLayoutComputed: normal composition and handle construction already require the layout. Do not claim a reachable uninitialized-layout display failure. Validation: 19 XDK cases passed without skips; javatools 414 cases with 36 existing skips and javatools_utils 120 with 2 existing skips, no failures or errors. Spotless and whitespace checks passed.
lagergren
force-pushed
the
lagergren/569-7-exceptionhandle
branch
from
September 26, 2026 21:55
41b2cd6 to
3dae54e
Compare
lagergren
enabled auto-merge (squash)
September 26, 2026 22:04
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.
Rendering a StringHandle in a Java debugger populated its Java String cache. ExceptionHandle display did the same to its text handle, including when Java printed the wrapper exception's stack trace. This PR keeps the rendered text unchanged while leaving those caches alone.
Reproducer and debugger impact
A debugger that evaluates toString() can change the state being inspected:
That can confuse investigation of when the cache is first populated: inspecting the handle itself warms the cache. ExceptionHandle.toString(), its WrapperException.toString(), and Java stack-trace printing have the corresponding text-cache path.
The reproduced impact is memoization during inspection. This PR does not claim a changed application result, a hang, or an uninitialized-layout crash. Display still allocates ordinary Java strings as needed.
Changes
getField(null, "text")read and quotes the string's characters without calling the memoizinggetStringValue().Field-access review
For a valid constructed ExceptionHandle,
textis an existing ordinary field. Its compositionlayout is prepared before construction, and GenericHandle initializes the corresponding field
array.
getField(null, "text")therefore reads the stored value; it does not populate theStringHandle's cache. Even an unassigned field value can be handled by the existing instanceof
check.
The unwanted mutation was in the subsequent
getStringValue()call. There was no demonstratedfailure of this normal field read to justify a separate accessor. Both the speculative layout
guard and the
peekField()helper are removed. GenericHandle, ClassComposition andTypeComposition now have no net changes in this PR.
Verification
Final diff: 3 files, +110 / −6. The field-access simplification is
611fb1e9e.
After restoring the ordinary field read, the existing regression tests passed unchanged:
19 XDK cases, zero failures/errors/skips, including both display/cache cases. Formatting and
whitespace checks passed.
The exception regression itself first reads
getField(null, "text")and confirms that theString cache is still empty. It then checks that handle display, wrapper display and Java
stack-trace printing leave the cache empty, and that an already populated cache is preserved.
The earlier before/after comparison temporarily restored the previous rendering methods.
Both cases failed at the cache assertion: expected null, but display populated the cache.
The output assertions preceding those checks passed. The temporary changes were restored.
That comparison tests the rendering change; it does not establish a need for an alternate
field accessor.
The tests use the distribution already provisioned by the XDK test task; no new build
dependency or silent skip was added.
Earlier broader verification at e650872 also passed: javatools 414 cases, 36 existing skips;
javatools_utils 120 cases, 2 existing skips; no failures or errors.