Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memory leak in stored exceptions #9960

Open
zbynek opened this issue May 21, 2024 · 8 comments
Open

Memory leak in stored exceptions #9960

zbynek opened this issue May 21, 2024 · 8 comments

Comments

@zbynek
Copy link
Contributor

zbynek commented May 21, 2024

GWT version: 2.11
Browser (with version): Chrome, any recent
Operating System: Windows (probably others)

Description

Storing an exception (e.g. in static final field) that was thrown from an instance method keeps instances of otherwise unused objects in the JS memory heap.


Steps to reproduce
  • check out https://github.com/zbynek/stacktracer/
  • navigate to that folder, run in terminal mvn gwt:codeserver -pl *-client -am "-Dgwt.style=DETAILED"
  • in another terminal mvn jetty:run -pl *-server -am -Denv=dev
  • open in browser http://localhost:8080/
  • in dev tools go to Memory tab, run garbage collection
  • create heap snapshot, observe that Ephemeral instance is there

Expected

  • Instance of Ephemeral not in heap

Actual

  • Instance of Ephemeral in heap (detailed name com_example_Ephemeral_Ephemeral__IV), referenced as symbol from the backingJsObject of the exception
Known workarounds

If the exception is accessible from your code (not coming from library code), use JSInterop to remove the backing object like this:

private void removeBackingObject(Throwable t) {
		Object back  = Js.asPropertyMap(t).get("backingJsObject");
		if (Js.isTruthy(back)) {
			Js.asPropertyMap(back).set("stack", JsArray.of());
		}
	}

If the exception comes from code you do not control, the above workaround can be combined with super-sourcing.

Possible fix

It may not be possible to fix this in general, but for exceptions that override fillInStackTrace in a way that avoids calling fillInStackTrace on the Throwable superclass, GWT could skip initialization of backingJsObject to maintain the Java semantic of "I don't care where this exception came from".

@zbynek
Copy link
Contributor Author

zbynek commented May 21, 2024

This is relevant for me because of exceptions that are permanently stored and repeatedly rethrown from a library code , see javacc/javacc#99 and javacc/javacc#284. (I know that the need to store and rethrow exception means they are no longer exceptional and should be replaced by boolean flags, but such change to JavaCC would require more work than the fix suggested here, which might be useful in other cases as well).

@niloc132
Copy link
Contributor

Can you confirm that this is a leak in the sense of "I was expecting zero instances and I got one, after throwing the same exception three times"? It looks as though each throw loses the previous instances at least, so the objects live as long as the exception's backing object hasn't yet been thrown again? I'm not certain we can nicely clear that out, since it is in the underlying JS exception - if Java rethrows the exception back to JS, that backing object is what is caught in JS.

I'm not quite sure why the browser holds on to the instance of the method that was part of the stack trace, it doesn't seem accessible.


get("backingJsObject")

Note that this will not work if jsinterop exports are disabled, or if the Throwable type isn't listed in the jsinterop includes. Also, stack is a JS string (rather than an array, you should probably assign an empty string back to it)... why does clearing it remove those references?


It looks like there is other cleanup that could happen in Throwable as well, linkBack() can be simplified, and fixIE() outright removed.


If the stack property on the backing JS object is the culprit, would it also be possible to provide a rebind rule for StackTraceCreator that mimics CollectorNull when no stack trace should be created? If I remember right, javacc defines its own Error types that could in theory be checked in the Collector.getStackTrace impl and return an empty array?

Or, let the new implementation just rewrite the stack property with itself as a new string? That appears to resolve the leak as well.

@zbynek
Copy link
Contributor Author

zbynek commented May 31, 2024

"I was expecting zero instances and I got one, after throwing the same exception three times"?

Yes.

why does clearing it remove those references?

Not sure about the why, but we've been using the workaround in GeoGebra for a while now and it works well.

would it also be possible to provide a rebind rule for StackTraceCreator that mimics CollectorNull when no stack trace should be created

Do you mean this StackTraceCreator should also reset the stack property of the native exception as a side effect of getStackTrace?

@niloc132
Copy link
Contributor

niloc132 commented Jun 2, 2024

I've been thinking about this a bit more, and I am suspecting this is a side effect of Chromium's async stack trace support. Replacing the stack trace (even with itself) is enough to tell it that you control the stack trace, so the debugger need not keep track of the information about higher frames any longer. This would make sense in your case, where you don't expect to use that exception again for its existing trace, but if I'm correct, might break the debugging experience for other users if applied to GWT itself.

Given the nature of your code, I would expect in prod mode that the Ephemeral.checkEven() method is made into a static (and top-level) function, so the Ephemeral class isn't even part of the stack trace. Instead, there just an Ephemeral instance in the local scope of one of the frames, which seems to make sense for the "stack trace info" theory - the debugger would want to have access to other locals in each frame. Making that method static and moving it to App would also confirm this.

Perhaps worth filing a bug on the Chromium bug tracker to confirm that this apparent leak is required for some reason (such as for async stack debugging), or if it is actually a bug?

Another possible workaround could be a patch for javacc (or editing generated code) that calls the super constructor with writableStackTrace set to false? I'm not sure if that would stop the leak, or just stop GWT from trying to translate it.

@zbynek
Copy link
Contributor Author

zbynek commented Jun 17, 2024

Chromium issue https://issues.chromium.org/issues/347649756

Given the nature of your code, I would expect in prod mode that the Ephemeral.checkEven() method is made into a static (and top-level) function, so the Ephemeral class isn't even part of the stack trace.

Indeed, in prod the method is static and there is no Ephemeral in the heap.

calls the super constructor with writableStackTrace

That's what the minimal example is already doing (https://github.com/zbynek/stacktracer/blob/main/stacktracer-client/src/main/java/com/example/SingletonException.java#L7), but it does not resolve the issue (seems that it only prevents the Java stacktrace setup). Also this flag is only viable for exceptions that extend java.lang.Exception directly, for subclasses of IOException (as in javacc/javacc#284) there is no way to pass that flag.

@niloc132
Copy link
Contributor

Having looked more closely than I have, would it make sense to resolve this by altering Throwable so that writableStackTrace==false would omit creating the stack trace, or am I misinterpreting what that flag would do in other circumstances?

Or, is it a decent workaround to build a app-specific collector that won't apply to SingletonException and its subtypes (via an instanceof check)?

@zbynek
Copy link
Contributor Author

zbynek commented Jun 27, 2024

My idea was to add a flag, let's say we name it stackTraceFilledIn, that would be false by default and set to true when fillInStackTrace is called, i.e. in all the default cases. The only ways to make that flag false would be when writeableStackTrace is true or fillInStackTrace is overridden in a way that does not call super.fillInStackTrace.
Then this flag could be used to clear the stack of the backingJsObject, either by assigning an empty string or by assigning self.

If I understand your first suggestion correctly, you're proposing to use writeableStackTrace flag in the same way, but that approach won't work for exceptions extending IOException, since those cannot pass the flag (AFAIK).

@zbynek
Copy link
Contributor Author

zbynek commented Jul 17, 2024

Apparently the Chromium issue is known since 2017 https://issues.chromium.org/issues/42210375

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

No branches or pull requests

2 participants