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

Commit c5ae4b9e authored by Winson Chiu's avatar Winson Chiu
Browse files

Avoid taking PMS snapshot lock when snapshot is still valid

Maintains 2 integers, one for snapshot current version and one for
snapshot pending version, where the former is updated to the latter
whenever a snapshot is rebuilt.

Calling threads can compare the volatile integers to see if the
snapshot is valid without taking mSnapshotLock. This avoids blocking
parallel threads using the snapshot.

Snapshot rebuilds are guaranteed to be accurate to the point in
time when one is requested, not the time that the snapshot is returned,
and this is enforced by caching the most recent pending version
before rebuilding the snapshot.

The thread that rebuilds will continue with its result regardless of
whether the snapshot was invalidated in between pending version read
and rebuild finish, and update the current version to the pending
version read. If an invalidation came in from the background, this
committed version will no longer be equivalent to the latest pending
version in the field, and so when the rebuild thread releases,
future/waiting threads will still see the snapshot as invalid and
cause a further rebuild.

This ensures changes are never lost and the snapshot will always
eventually be brought up to date.

Bug: 232282785

Test: presubmit

Change-Id: I99656d573c7cec88069ec2eb35a1c7be48c782e0
parent a47d028e
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -94,12 +94,13 @@ import java.util.Set;
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public interface Computer extends PackageDataSnapshot {

    int getVersion();

    /**
     * Administrative statistics: record that the snapshot has been used.  Every call
     * to use() increments the usage counter.
     */
    default void use() {
    }
    Computer use();
    /**
     * Fetch the snapshot usage counter.
     * @return The number of times this snapshot was used.
+11 −2
Original line number Diff line number Diff line
@@ -367,6 +367,8 @@ public class ComputerEngine implements Computer {
        return (v1 > v2) ? -1 : ((v1 < v2) ? 1 : 0);
    };

    private final int mVersion;

    // The administrative use counter.
    private int mUsed = 0;

@@ -424,7 +426,8 @@ public class ComputerEngine implements Computer {
        return mLocalAndroidApplication;
    }

    ComputerEngine(PackageManagerService.Snapshot args) {
    ComputerEngine(PackageManagerService.Snapshot args, int version) {
        mVersion = version;
        mSettings = new Settings(args.settings);
        mIsolatedOwners = args.isolatedOwners;
        mPackages = args.packages;
@@ -464,11 +467,17 @@ public class ComputerEngine implements Computer {
        mService = args.service;
    }

    @Override
    public int getVersion() {
        return mVersion;
    }

    /**
     * Record that the snapshot was used.
     */
    public final void use() {
    public final Computer use() {
        mUsed++;
        return this;
    }

    /**
+1 −1
Original line number Diff line number Diff line
@@ -30,7 +30,7 @@ import com.android.internal.annotations.VisibleForTesting;
public final class ComputerLocked extends ComputerEngine {

    ComputerLocked(PackageManagerService.Snapshot args) {
        super(args);
        super(args, -1);
    }

    protected ComponentName resolveComponentName() {
+46 −50
Original line number Diff line number Diff line
@@ -271,8 +271,8 @@ import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

/**
@@ -1038,22 +1038,15 @@ public class PackageManagerService implements PackageSender, TestUtilityService
    // times during the PackageManagerService constructor but it should not be modified thereafter.
    private ComputerLocked mLiveComputer;

    // A lock-free cache for frequently called functions.
    private volatile Computer mSnapshotComputer;
    private static final AtomicReference<Computer> sSnapshot = new AtomicReference<>();

    // If true, the snapshot is invalid (stale).  The attribute is static since it may be
    // set from outside classes.  The attribute may be set to true anywhere, although it
    // should only be set true while holding mLock.  However, the attribute id guaranteed
    // to be set false only while mLock and mSnapshotLock are both held.
    private static final AtomicBoolean sSnapshotInvalid = new AtomicBoolean(true);

    static final ThreadLocal<ThreadComputer> sThreadComputer =
            ThreadLocal.withInitial(ThreadComputer::new);
    // If this differs from Computer#getVersion, the snapshot is invalid (stale).
    private static final AtomicInteger sSnapshotPendingVersion = new AtomicInteger(1);

    /**
     * This lock is used to make reads from {@link #sSnapshotInvalid} and
     * {@link #mSnapshotComputer} atomic inside {@code snapshotComputer()}.  This lock is
     * not meant to be used outside that method.  This lock must be taken before
     * This lock is used to make reads from {@link #sSnapshotPendingVersion} and
     * {@link #sSnapshot} atomic inside {@code snapshotComputer()} when the versions mismatch.
     * This lock is not meant to be used outside that method. This lock must be taken before
     * {@link #mLock} is taken.
     */
    private final Object mSnapshotLock = new Object();
@@ -1077,48 +1070,53 @@ public class PackageManagerService implements PackageSender, TestUtilityService
            // yet invalidated the snapshot.  Always give the thread the live computer.
            return mLiveComputer;
        }

        var oldSnapshot = sSnapshot.get();
        var pendingVersion = sSnapshotPendingVersion.get();

        if (oldSnapshot != null && oldSnapshot.getVersion() == pendingVersion) {
            return oldSnapshot.use();
        }

        synchronized (mSnapshotLock) {
            // This synchronization block serializes access to the snapshot computer and
            // to the code that samples mSnapshotInvalid.
            Computer c = mSnapshotComputer;
            if (sSnapshotInvalid.getAndSet(false) || (c == null)) {
                // The snapshot is invalid if it is marked as invalid or if it is null.  If it
                // is null, then it is currently being rebuilt by rebuildSnapshot().
                synchronized (mLock) {
                    // Rebuild the snapshot if it is invalid.  Note that the snapshot might be
                    // invalidated as it is rebuilt.  However, the snapshot is still
                    // self-consistent (the lock is being held) and is current as of the time
                    // this function is entered.
                    rebuildSnapshot();
            // Re-capture pending version in case a new invalidation occurred since last check
            var rebuildSnapshot = sSnapshot.get();
            var rebuildVersion = sSnapshotPendingVersion.get();

                    // Guaranteed to be non-null.  mSnapshotComputer is only be set to null
                    // temporarily in rebuildSnapshot(), which is guarded by mLock().  Since
                    // the mLock is held in this block and since rebuildSnapshot() is
                    // complete, the attribute can not now be null.
                    c = mSnapshotComputer;
            // Check the versions again while the lock is held, in case the rebuild time caused
            // multiple threads to wait on the snapshot lock. When the first thread finishes
            // a rebuild, the snapshot is now valid and the other waiting threads can use it
            // without kicking off their own rebuilds.
            if (rebuildSnapshot != null && rebuildSnapshot.getVersion() == rebuildVersion) {
                return rebuildSnapshot.use();
            }

            synchronized (mLock) {
                // Fetch version one last time to ensure that the rebuilt snapshot matches
                // the latest invalidation, which could have come in between entering the
                // SnapshotLock and mLock sync blocks.
                rebuildVersion = sSnapshotPendingVersion.get();

                // Build the snapshot for this version
                var newSnapshot = rebuildSnapshot(rebuildSnapshot, rebuildVersion);
                sSnapshot.set(newSnapshot);
                return newSnapshot.use();
            }
            c.use();
            return c;
        }
    }

    /**
     * Rebuild the cached computer.  mSnapshotComputer is temporarily set to null to block other
     * threads from using the invalid computer until it is rebuilt.
     */
    @GuardedBy({ "mLock", "mSnapshotLock"})
    private void rebuildSnapshot() {
        final long now = SystemClock.currentTimeMicro();
        final int hits = mSnapshotComputer == null ? -1 : mSnapshotComputer.getUsed();
        mSnapshotComputer = null;
        final Snapshot args = new Snapshot(Snapshot.SNAPPED);
        mSnapshotComputer = new ComputerEngine(args);
        final long done = SystemClock.currentTimeMicro();
    private Computer rebuildSnapshot(@Nullable Computer oldSnapshot, int newVersion) {
        var now = SystemClock.currentTimeMicro();
        var hits = oldSnapshot == null ? -1 : oldSnapshot.getUsed();
        var args = new Snapshot(Snapshot.SNAPPED);
        var newSnapshot = new ComputerEngine(args, newVersion);
        var done = SystemClock.currentTimeMicro();

        if (mSnapshotStatistics != null) {
            mSnapshotStatistics.rebuild(now, done, hits);
        }
        return newSnapshot;
    }

    /**
@@ -1138,7 +1136,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService
        if (TRACE_SNAPSHOTS) {
            Log.i(TAG, "snapshot: onChange(" + what + ")");
        }
        sSnapshotInvalid.set(true);
        sSnapshotPendingVersion.incrementAndGet();
    }

    /**
@@ -1665,7 +1663,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService
        mRequiredSdkSandboxPackage = testParams.requiredSdkSandboxPackage;

        mLiveComputer = createLiveComputer();
        mSnapshotComputer = null;
        mSnapshotStatistics = null;

        mPackages.putAll(testParams.packages);
@@ -1855,9 +1852,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService
            // cached computer is the same as the live computer until the end of the
            // constructor, at which time the invalidation method updates it.
            mSnapshotStatistics = new SnapshotStatistics();
            sSnapshotInvalid.set(true);
            sSnapshotPendingVersion.incrementAndGet();
            mLiveComputer = createLiveComputer();
            mSnapshotComputer = null;
            registerObservers(true);
        }

+0 −52
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 com.android.server.pm;

/**
 * This class records the Computer being used by a thread and the Computer's reference
 * count.  There is a thread-local copy of this class.
 */
public final class ThreadComputer implements AutoCloseable {
    Computer mComputer = null;
    int mRefCount = 0;

    void acquire(Computer c) {
        if (mRefCount != 0 && mComputer != c) {
            throw new RuntimeException("computer mismatch, count = " + mRefCount);
        }
        mComputer = c;
        mRefCount++;
    }

    void acquire() {
        if (mRefCount == 0 || mComputer == null) {
            throw new RuntimeException("computer acquire on empty ref count");
        }
        mRefCount++;
    }

    void release() {
        if (--mRefCount == 0) {
            mComputer = null;
        }
    }

    @Override
    public void close() {
        release();
    }
}