BaseBundle.java:: Clear underlying parcel when no longer referernced.

Follow up to ag/18795008.
Lazy Bundles, (aosp/1787847), introduced a change in behavior where a Parcel
created as part of initializing a Bundle is dependent on the next ART GCrun to be
recycled, causing a short term memory-leak. We previously force recycled
the parcel when bundle.clear() was called.

This commit takes it a step further and adds  reference counting for the number of
lazy values in mMap. When this reaches 0, we can safely eager recycle the
underlying parcel.

This still does not work when parcel references have been shared outside
the owning bundle (i.e, when mOwnsLazyValues is false.) We have two
options here:

1. Allow the GC to eventually claim the parcel (current behaviour).
2. Track lazy values refs globally across bundles. High complexity cost,
   and likely high performance impact.

We can not unmwrap the lazy values before copying, in case the
originating classloader can not instantiate the given class type. See
go/lazy-bundle

In initializeFromParcelLocked(), renamed boolean parameter recycleParcel to ownsParcel since the method no longer recycles in all cases. Also, only create lazy values if ownsParcel = true so that we don't have lazy values referencing the passed in parcel in case the caller doesn't own the parcel (which means the parcel could be recycled out of our control).

Bug: 233216232
Test: Reproduced linked bug on-device
Test: atest android.os.cts.ParcelTest android.os.cts.BundleTest
android.os.BundleTest android.os.ParcelTest android.os.BundleRecylingTest

Change-Id: I8a878a6d0055b04b2611909b4b17977ba5677a4f
This commit is contained in:
Hani Kazmi
2022-06-20 09:09:29 +00:00
parent 78c33bb414
commit a0d9fc3b28
3 changed files with 167 additions and 55 deletions

View File

@@ -29,6 +29,7 @@ import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import java.io.Serializable; import java.io.Serializable;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
@@ -113,17 +114,21 @@ public class BaseBundle {
*/ */
private boolean mParcelledByNative; private boolean mParcelledByNative;
/* /**
* Flag indicating if mParcelledData is only referenced in this bundle. * Flag indicating if mParcelledData is only referenced in this bundle.
* mParcelledData could be referenced by other bundles if mMap contains lazy values, * mParcelledData could be referenced elsewhere if mMap contains lazy values,
* and bundle data is copied to another bundle using putAll or the copy constructors. * and bundle data is copied to another bundle using putAll or the copy constructors.
*/ */
boolean mOwnsLazyValues = true; boolean mOwnsLazyValues = true;
/* /** Tracks how many lazy values are referenced in mMap */
private int mLazyValues = 0;
/**
* As mParcelledData is set to null when it is unparcelled, we keep a weak reference to * As mParcelledData is set to null when it is unparcelled, we keep a weak reference to
* it to aid in recycling it. Do not use this reference otherwise. * it to aid in recycling it. Do not use this reference otherwise.
*/ * Is non-null iff mMap contains lazy values.
*/
private WeakReference<Parcel> mWeakParcelledData = null; private WeakReference<Parcel> mWeakParcelledData = null;
/** /**
@@ -310,7 +315,8 @@ public class BaseBundle {
synchronized (this) { synchronized (this) {
final Parcel source = mParcelledData; final Parcel source = mParcelledData;
if (source != null) { if (source != null) {
initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative); Preconditions.checkState(mOwnsLazyValues);
initializeFromParcelLocked(source, /*ownsParcel*/ true, mParcelledByNative);
} else { } else {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "unparcel " Log.d(TAG, "unparcel "
@@ -401,11 +407,23 @@ public class BaseBundle {
} }
} }
mMap.setValueAt(i, object); mMap.setValueAt(i, object);
mLazyValues--;
if (mOwnsLazyValues) {
Preconditions.checkState(mLazyValues >= 0, "Lazy values ref count below 0");
// No more lazy values in mMap, so we can recycle the parcel early rather than
// waiting for the next GC run
if (mLazyValues == 0) {
Preconditions.checkState(mWeakParcelledData.get() != null,
"Parcel recycled earlier than expected");
recycleParcel(mWeakParcelledData.get());
mWeakParcelledData = null;
}
}
} }
return (clazz != null) ? clazz.cast(object) : (T) object; return (clazz != null) ? clazz.cast(object) : (T) object;
} }
private void initializeFromParcelLocked(@NonNull Parcel parcelledData, boolean recycleParcel, private void initializeFromParcelLocked(@NonNull Parcel parcelledData, boolean ownsParcel,
boolean parcelledByNative) { boolean parcelledByNative) {
if (isEmptyParcel(parcelledData)) { if (isEmptyParcel(parcelledData)) {
if (DEBUG) { if (DEBUG) {
@@ -437,9 +455,10 @@ public class BaseBundle {
map.erase(); map.erase();
map.ensureCapacity(count); map.ensureCapacity(count);
} }
int numLazyValues = 0;
try { try {
recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, numLazyValues = parcelledData.readArrayMap(map, count, !parcelledByNative,
/* lazy */ true, mClassLoader); /* lazy */ ownsParcel, mClassLoader);
} catch (BadParcelableException e) { } catch (BadParcelableException e) {
if (sShouldDefuse) { if (sShouldDefuse) {
Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e); Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
@@ -448,14 +467,19 @@ public class BaseBundle {
throw e; throw e;
} }
} finally { } finally {
mMap = map; mWeakParcelledData = null;
if (recycleParcel) { if (ownsParcel) {
recycleParcel(parcelledData); if (numLazyValues == 0) {
mWeakParcelledData = null; recycleParcel(parcelledData);
} else { } else {
mWeakParcelledData = new WeakReference<>(parcelledData); mWeakParcelledData = new WeakReference<>(parcelledData);
}
} }
mLazyValues = numLazyValues;
mParcelledByNative = false; mParcelledByNative = false;
mMap = map;
// Set field last as it is volatile
mParcelledData = null; mParcelledData = null;
} }
if (DEBUG) { if (DEBUG) {
@@ -592,13 +616,17 @@ public class BaseBundle {
/** /**
* Removes all elements from the mapping of this Bundle. * Removes all elements from the mapping of this Bundle.
* Recycles the underlying parcel if it is still present.
*/ */
public void clear() { public void clear() {
unparcel(); unparcel();
if (mOwnsLazyValues && mWeakParcelledData != null) { if (mOwnsLazyValues && mWeakParcelledData != null) {
recycleParcel(mWeakParcelledData.get()); recycleParcel(mWeakParcelledData.get());
mWeakParcelledData = null;
} }
mWeakParcelledData = null;
mLazyValues = 0;
mOwnsLazyValues = true;
mMap.clear(); mMap.clear();
} }
@@ -1844,8 +1872,8 @@ public class BaseBundle {
// had been constructed with parcel or to make sure they trigger deserialization of the // had been constructed with parcel or to make sure they trigger deserialization of the
// bundle immediately; neither of which is obvious. // bundle immediately; neither of which is obvious.
synchronized (this) { synchronized (this) {
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle); mOwnsLazyValues = false;
unparcel(/* itemwise */ true); initializeFromParcelLocked(parcel, /*ownsParcel*/ false, isNativeBundle);
} }
return; return;
} }
@@ -1862,6 +1890,7 @@ public class BaseBundle {
+ ": " + length + " bundle bytes starting at " + offset); + ": " + length + " bundle bytes starting at " + offset);
p.setDataPosition(0); p.setDataPosition(0);
mOwnsLazyValues = true;
mParcelledByNative = isNativeBundle; mParcelledByNative = isNativeBundle;
mParcelledData = p; mParcelledData = p;
} }

View File

@@ -71,7 +71,6 @@ import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.IntFunction; import java.util.function.IntFunction;
import java.util.function.Supplier;
/** /**
* Container for a message (data and object references) that can * Container for a message (data and object references) that can
@@ -5242,20 +5241,20 @@ public final class Parcel {
* Reads a map into {@code map}. * Reads a map into {@code map}.
* *
* @param sorted Whether the keys are sorted by their hashes, if so we use an optimized path. * @param sorted Whether the keys are sorted by their hashes, if so we use an optimized path.
* @param lazy Whether to populate the map with lazy {@link Supplier} objects for * @param lazy Whether to populate the map with lazy {@link Function} objects for
* length-prefixed values. See {@link Parcel#readLazyValue(ClassLoader)} for more * length-prefixed values. See {@link Parcel#readLazyValue(ClassLoader)} for more
* details. * details.
* @return whether the parcel can be recycled or not. * @return a count of the lazy values in the map
* @hide * @hide
*/ */
boolean readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted, int readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted,
boolean lazy, @Nullable ClassLoader loader) { boolean lazy, @Nullable ClassLoader loader) {
boolean recycle = true; int lazyValues = 0;
while (size > 0) { while (size > 0) {
String key = readString(); String key = readString();
Object value = (lazy) ? readLazyValue(loader) : readValue(loader); Object value = (lazy) ? readLazyValue(loader) : readValue(loader);
if (value instanceof LazyValue) { if (value instanceof LazyValue) {
recycle = false; lazyValues++;
} }
if (sorted) { if (sorted) {
map.append(key, value); map.append(key, value);
@@ -5267,7 +5266,7 @@ public final class Parcel {
if (sorted) { if (sorted) {
map.validate(); map.validate();
} }
return recycle; return lazyValues;
} }
/** /**

View File

@@ -23,6 +23,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@@ -34,7 +35,6 @@ import androidx.test.filters.SmallTest;
import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
@@ -56,16 +56,11 @@ public class BundleRecyclingTest {
private Parcel mParcelSpy; private Parcel mParcelSpy;
private Bundle mBundle; private Bundle mBundle;
@Before
public void setUp() throws Exception {
setUpBundle(/* hasLazy */ true);
}
@Test @Test
public void bundleClear_whenUnparcelledWithoutLazy_recyclesParcelOnce() { public void bundleClear_whenUnparcelledWithoutLazy_recyclesParcelOnce() {
setUpBundle(/* hasLazy */ false); setUpBundle(/* lazy */ 0, /* nonLazy */ 1);
// Will unparcel and immediately recycle parcel // Will unparcel and immediately recycle parcel
assertNotNull(mBundle.getString("key")); assertNotNull(mBundle.getString("nonLazy0"));
verify(mParcelSpy, times(1)).recycle(); verify(mParcelSpy, times(1)).recycle();
assertFalse(mBundle.isDefinitelyEmpty()); assertFalse(mBundle.isDefinitelyEmpty());
@@ -77,6 +72,7 @@ public class BundleRecyclingTest {
@Test @Test
public void bundleClear_whenParcelled_recyclesParcel() { public void bundleClear_whenParcelled_recyclesParcel() {
setUpBundle(/* lazy */ 1);
assertTrue(mBundle.isParcelled()); assertTrue(mBundle.isParcelled());
verify(mParcelSpy, times(0)).recycle(); verify(mParcelSpy, times(0)).recycle();
@@ -89,24 +85,10 @@ public class BundleRecyclingTest {
verify(mParcelSpy, times(1)).recycle(); verify(mParcelSpy, times(1)).recycle();
} }
@Test
public void bundleClear_whenUnparcelledWithLazyValueUnwrapped_recyclesParcel() {
// Will unparcel with a lazy value, and immediately unwrap the lazy value,
// with no lazy values left at the end of getParcelable
assertNotNull(mBundle.getParcelable("key", CustomParcelable.class));
verify(mParcelSpy, times(0)).recycle();
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
assertTrue(mBundle.isDefinitelyEmpty());
// Should not recycle again
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
}
@Test @Test
public void bundleClear_whenUnparcelledWithLazy_recyclesParcel() { public void bundleClear_whenUnparcelledWithLazy_recyclesParcel() {
setUpBundle(/* lazy */ 1);
// Will unparcel but keep the CustomParcelable lazy // Will unparcel but keep the CustomParcelable lazy
assertFalse(mBundle.isEmpty()); assertFalse(mBundle.isEmpty());
verify(mParcelSpy, times(0)).recycle(); verify(mParcelSpy, times(0)).recycle();
@@ -122,6 +104,8 @@ public class BundleRecyclingTest {
@Test @Test
public void bundleClear_whenClearedWithSharedParcel_doesNotRecycleParcel() { public void bundleClear_whenClearedWithSharedParcel_doesNotRecycleParcel() {
setUpBundle(/* lazy */ 1);
Bundle copy = new Bundle(); Bundle copy = new Bundle();
copy.putAll(mBundle); copy.putAll(mBundle);
@@ -136,6 +120,8 @@ public class BundleRecyclingTest {
@Test @Test
public void bundleClear_whenClearedWithCopiedParcel_doesNotRecycleParcel() { public void bundleClear_whenClearedWithCopiedParcel_doesNotRecycleParcel() {
setUpBundle(/* lazy */ 1);
// Will unparcel but keep the CustomParcelable lazy // Will unparcel but keep the CustomParcelable lazy
assertFalse(mBundle.isEmpty()); assertFalse(mBundle.isEmpty());
@@ -151,7 +137,101 @@ public class BundleRecyclingTest {
verify(mParcelSpy, never()).recycle(); verify(mParcelSpy, never()).recycle();
} }
private void setUpBundle(boolean hasLazy) { @Test
public void bundleGet_whenUnparcelledWithLazyValueUnwrapped_recyclesParcel() {
setUpBundle(/* lazy */ 1);
// Will unparcel with a lazy value, and immediately unwrap the lazy value,
// with no lazy values left at the end of getParcelable
// Ref counting should immediately recycle
assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
verify(mParcelSpy, times(1)).recycle();
// Should not recycle again
assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
}
@Test
public void bundleGet_whenMultipleLazy_recyclesParcelWhenAllUnwrapped() {
setUpBundle(/* lazy */ 2);
assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
verify(mParcelSpy, times(0)).recycle();
assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
verify(mParcelSpy, times(0)).recycle();
assertNotNull(mBundle.getParcelable("lazy1", CustomParcelable.class));
verify(mParcelSpy, times(1)).recycle();
// Should not recycle again
assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
assertTrue(mBundle.isDefinitelyEmpty());
}
@Test
public void bundleGet_whenLazyAndNonLazy_recyclesParcelWhenAllUnwrapped() {
setUpBundle(/* lazy */ 1, /* nonLazy */ 1);
assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
verify(mParcelSpy, times(1)).recycle();
// Should not recycle again
assertNotNull(mBundle.getString("nonLazy0"));
assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
}
@Test
public void bundleGet_whenLazyAndNonLazy_doesNotRecycleWhenOnlyNonLazyRetrieved() {
setUpBundle(/* lazy */ 1, /* nonLazy */ 1);
assertNotNull(mBundle.getString("nonLazy0"));
verify(mParcelSpy, times(0)).recycle();
assertNotNull(mBundle.getString("nonLazy0"));
verify(mParcelSpy, times(0)).recycle();
assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
verify(mParcelSpy, times(1)).recycle();
}
@Test
public void bundleGet_withWithSharedParcel_doesNotRecycleParcel() {
setUpBundle(/* lazy */ 1);
Bundle copy = new Bundle();
copy.putAll(mBundle);
assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
mBundle.clear();
assertNotNull(copy.getParcelable("lazy0", CustomParcelable.class));
copy.clear();
verify(mParcelSpy, never()).recycle();
}
@Test
public void bundleGet_getAfterLazyCleared_doesNotRecycleAgain() {
setUpBundle(/* lazy */ 1);
mBundle.clear();
verify(mParcelSpy, times(1)).recycle();
assertNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
verify(mParcelSpy, times(1)).recycle();
}
private void setUpBundle(int lazy) {
setUpBundle(lazy, /* nonLazy */ 0);
}
private void setUpBundle(int lazy, int nonLazy) {
AtomicReference<Parcel> parcel = new AtomicReference<>(); AtomicReference<Parcel> parcel = new AtomicReference<>();
StaticMockitoSession session = mockitoSession() StaticMockitoSession session = mockitoSession()
.strictness(Strictness.STRICT_STUBS) .strictness(Strictness.STRICT_STUBS)
@@ -166,7 +246,7 @@ public class BundleRecyclingTest {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.setClassLoader(getClass().getClassLoader()); bundle.setClassLoader(getClass().getClassLoader());
Parcel p = createBundle(hasLazy); Parcel p = createBundle(lazy, nonLazy);
bundle.readFromParcel(p); bundle.readFromParcel(p);
p.recycle(); p.recycle();
@@ -179,13 +259,17 @@ public class BundleRecyclingTest {
/** /**
* Create a test bundle, parcel it and return the parcel. * Create a test bundle, parcel it and return the parcel.
*/ */
private Parcel createBundle(boolean hasLazy) { private Parcel createBundle(int lazy, int nonLazy) {
final Bundle source = new Bundle(); final Bundle source = new Bundle();
if (hasLazy) {
source.putParcelable("key", new CustomParcelable(13, "Tiramisu")); for (int i = 0; i < nonLazy; i++) {
} else { source.putString("nonLazy" + i, "Tiramisu");
source.putString("key", "tiramisu");
} }
for (int i = 0; i < lazy; i++) {
source.putParcelable("lazy" + i, new CustomParcelable(13, "Tiramisu"));
}
return getParcelledBundle(source); return getParcelledBundle(source);
} }