Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 95e32f0f authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Hand-rolled linked list for Parcel performance.

While recently looking at pprof data, we noticed that Parcel.obtain()
and recycle() were some of the most heavily used methods inside the
system process.  On the surface, this makes sense because these
methods are typically invoked at least twice for every Binder call.

Internally these methods had been maintaining a pool of cached Parcel
objects to avoid GC pressure, but unfortunately it was using an
array which resulted in O(n) scanning under heavy load, increasing
lock contention encountered by all Binder calls.

This change greatly reduces that contention by using a hand-rolled
linked list approach, mirroring android.os.Message.  For a 1-thread
benchmark, this new approach has almost 2x throughput, and for a
16-thread benchmark it has almost 8x throughput.

As part of making this change we evaluated several approaches,
including using pure-GC with no pooling, a single AtomicReference,
and a pool of several AtomicReferences.  To measure these we wrote
ParcelPoolBenchmark which simulates various levels of Binder load
using 1, 4 and 16 threads.  Below are the relative benchmark results
compared to the previous approach before this CL:

              1 thread   4 threads   16 threads
Pure GC        131.74%      32.76%       43.90%
Single AR       95.22%      25.54%       13.66%
Pooled AR       57.65%      16.21%       11.55%
Linked list     52.66%      18.06%       12.55%

On balance, the linked list approach performs well across the board,
and we bias towards it over the two AtomicReference approaches since
it performs slightly better on the single-threaded case, which is
the most representative of typical Binder load across all processes.

Bug: 165032569
Test: ./frameworks/base/libs/hwui/tests/scripts/prep_generic.sh little && atest CorePerfTests:android.os.ParcelObtainPerfTest
Change-Id: I190b1c8f7fd59855c3c2d36032512279691e2c04
parent f311e0b2
Loading
Loading
Loading
Loading
+88 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package android.os;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ParcelObtainPerfTest {
    private static final int ITERATIONS = 1_000_000;

    @Test
    public void timeContention_01() throws Exception {
        timeContention(1);
    }

    @Test
    public void timeContention_04() throws Exception {
        timeContention(4);
    }

    @Test
    public void timeContention_16() throws Exception {
        timeContention(16);
    }

    private static void timeContention(int numThreads) throws Exception {
        final long start = SystemClock.elapsedRealtime();
        {
            final ObtainThread[] threads = new ObtainThread[numThreads];
            for (int i = 0; i < numThreads; i++) {
                final ObtainThread thread = new ObtainThread(ITERATIONS / numThreads);
                thread.start();
                threads[i] = thread;
            }
            for (int i = 0; i < numThreads; i++) {
                threads[i].join();
            }
        }
        final long duration = SystemClock.elapsedRealtime() - start;

        final Bundle results = new Bundle();
        results.putLong("duration", duration);
        InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
    }

    public static class ObtainThread extends Thread {
        public int iterations;

        public ObtainThread(int iterations) {
            this.iterations = iterations;
        }

        @Override
        public void run() {
            while (iterations-- > 0) {
                final Parcel data = Parcel.obtain();
                final Parcel reply = Parcel.obtain();
                try {
                    data.writeInt(32);
                    reply.writeInt(32);
                } finally {
                    reply.recycle();
                    data.recycle();
                }
            }
        }
    }
}
+0 −15
Original line number Diff line number Diff line
@@ -158,21 +158,6 @@ public class ParcelPerfTest {
        }
    }

    @Test
    public void timeObtainRecycle() {
        // Use up the pooled instances.
        // A lot bigger than the actual size but in case someone increased it.
        final int POOL_SIZE = 100;
        for (int i = 0; i < POOL_SIZE; i++) {
            Parcel.obtain();
        }

        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            Parcel.obtain().recycle();
        }
    }

    @Test
    public void timeWriteException() {
        timeWriteException(false);
+77 −41
Original line number Diff line number Diff line
@@ -33,6 +33,8 @@ import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;

import com.android.internal.annotations.GuardedBy;

import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
import dalvik.system.VMRuntime;
@@ -222,9 +224,31 @@ public final class Parcel {
     */
    private static boolean sParcelExceptionStackTrace;

    private static final int POOL_SIZE = 6;
    private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];
    private static final Parcel[] sHolderPool = new Parcel[POOL_SIZE];
    private static final Object sPoolSync = new Object();

    /** Next item in the linked list pool, if any */
    @GuardedBy("sPoolSync")
    private Parcel mPoolNext;

    /** Head of a linked list pool of {@link Parcel} objects */
    @GuardedBy("sPoolSync")
    private static Parcel sOwnedPool;
    /** Head of a linked list pool of {@link Parcel} objects */
    @GuardedBy("sPoolSync")
    private static Parcel sHolderPool;

    /** Total size of pool with head at {@link #sOwnedPool} */
    @GuardedBy("sPoolSync")
    private static int sOwnedPoolSize = 0;
    /** Total size of pool with head at {@link #sHolderPool} */
    @GuardedBy("sPoolSync")
    private static int sHolderPoolSize = 0;

    /**
     * We're willing to pool up to 32 objects, which is sized to accommodate
     * both a data and reply Parcel for the maximum of 16 Binder threads.
     */
    private static final int POOL_SIZE = 32;

    // Keep in sync with frameworks/native/include/private/binder/ParcelValTypes.h.
    private static final int VAL_NULL = -1;
@@ -420,22 +444,27 @@ public final class Parcel {
     */
    @NonNull
    public static Parcel obtain() {
        final Parcel[] pool = sOwnedPool;
        synchronized (pool) {
            Parcel p;
            for (int i=0; i<POOL_SIZE; i++) {
                p = pool[i];
                if (p != null) {
                    pool[i] = null;
                    if (DEBUG_RECYCLE) {
                        p.mStack = new RuntimeException();
        Parcel res = null;
        synchronized (sPoolSync) {
            if (sOwnedPool != null) {
                res = sOwnedPool;
                sOwnedPool = res.mPoolNext;
                res.mPoolNext = null;
                sOwnedPoolSize--;
            }
                    p.mReadWriteHelper = ReadWriteHelper.DEFAULT;
                    return p;
        }

        // When no cache found above, create from scratch; otherwise prepare the
        // cached object to be used
        if (res == null) {
            res = new Parcel(0);
        } else {
            if (DEBUG_RECYCLE) {
                res.mStack = new RuntimeException();
            }
            res.mReadWriteHelper = ReadWriteHelper.DEFAULT;
        }
        return new Parcel(0);
        return res;
    }

    /**
@@ -446,19 +475,21 @@ public final class Parcel {
        if (DEBUG_RECYCLE) mStack = null;
        freeBuffer();

        final Parcel[] pool;
        if (mOwnsNativeParcelObject) {
            pool = sOwnedPool;
            synchronized (sPoolSync) {
                if (sOwnedPoolSize < POOL_SIZE) {
                    mPoolNext = sOwnedPool;
                    sOwnedPool = this;
                    sOwnedPoolSize++;
                }
            }
        } else {
            mNativePtr = 0;
            pool = sHolderPool;
        }

        synchronized (pool) {
            for (int i=0; i<POOL_SIZE; i++) {
                if (pool[i] == null) {
                    pool[i] = this;
                    return;
            synchronized (sPoolSync) {
                if (sHolderPoolSize < POOL_SIZE) {
                    mPoolNext = sHolderPool;
                    sHolderPool = this;
                    sHolderPoolSize++;
                }
            }
        }
@@ -3496,22 +3527,27 @@ public final class Parcel {

    /** @hide */
    static protected final Parcel obtain(long obj) {
        final Parcel[] pool = sHolderPool;
        synchronized (pool) {
            Parcel p;
            for (int i=0; i<POOL_SIZE; i++) {
                p = pool[i];
                if (p != null) {
                    pool[i] = null;
                    if (DEBUG_RECYCLE) {
                        p.mStack = new RuntimeException();
        Parcel res = null;
        synchronized (sPoolSync) {
            if (sHolderPool != null) {
                res = sHolderPool;
                sHolderPool = res.mPoolNext;
                res.mPoolNext = null;
                sHolderPoolSize--;
            }
                    p.init(obj);
                    return p;
        }

        // When no cache found above, create from scratch; otherwise prepare the
        // cached object to be used
        if (res == null) {
            res = new Parcel(obj);
        } else {
            if (DEBUG_RECYCLE) {
                res.mStack = new RuntimeException();
            }
            res.init(obj);
        }
        return new Parcel(obj);
        return res;
    }

    private Parcel(long nativePtr) {