agents/skills/java-memory-leaks/SKILL.md
This skill guides the process of identifying, debugging, and fixing Java memory leaks in Android Chrome, typically identified by LeakCanary in instrumentation tests.
Confirm the Leak:
@DoNotBatch). Batched
tests can share state and make leak traces ambiguous or hard to reproduce.@EnableLeakChecks or
else LeakCanary will not run.Analyze Leak Trace:
Leaking: YES).Leaking: UNKNOWN nodes in between to find where the chain should
be broken.Identify the Break Point:
destroy() on components that hold resources or
observers.Apply Fix Patterns:
removeObserver() is called in onDestroy() or equivalent lifecycle
teardown.DestroyObserver, consider making it one and
registering it with ActivityLifecycleDispatcher.onDestroy().@Nullable and add explicit
null checks where needed. This is safer and satisfies NullAway without
suppressions.@Nullable causes too
much "collateral damage" (requiring null checks in many places), you can
use @SuppressWarnings("NullAway") on the onDestroy() method to set
the field to null while keeping it non-null for the rest of the
lifecycle.private @Nullable CustomTabActivityTabProvider mTabProvider;
@Override
public void onDestroy() {
mTabProvider = null; // Breaks leak trace
}
removeTab()), ensure
this is done by the owner of the component, not by an observer or
registrar. Observers should not have side effects that mutate the
observed object during teardown.Verify:
autoninja -C out/Debug chrome_public_test_apk.out/Debug/bin/run_chrome_public_test_apk -f "YourLeakTest*".onDestroy() can cause
NullPointerExceptions if other components try to use them during their own
destruction (e.g., to unregister observers). Ensure that cleanup methods like
unregisterObserver handle null references gracefully.
public void unregisterActivityTabObserver(CustomTabTabObserver observer) {
mActivityTabObservers.removeObserver(observer);
if (mTabProvider == null) return; // Guard against NPE during destruction
Tab activeTab = mTabProvider.getTab();
...
}
Before:
public class MyComponent implements DestroyObserver {
private final LongLivedProvider mProvider; // Retains activity
public MyComponent(LongLivedProvider provider) {
mProvider = provider;
}
@Override
public void onDestroy() {
// Provider still holds reference to this component, leaking activity
}
}
After (Preferred):
public class MyComponent implements DestroyObserver {
private @Nullable LongLivedProvider mProvider;
public MyComponent(LongLivedProvider provider) {
mProvider = provider;
}
@Override
public void onDestroy() {
if (mProvider != null) {
mProvider.removeObserver(this); // If applicable
mProvider = null; // Break the leak chain
}
}
}