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

Commit 7ffd4c59 authored by wilsonshih's avatar wilsonshih
Browse files

Extract snapshot persist thread from TaskSnapshotPersister(1/N)

Extract the IO thread and queue handler to SnapshotPersistQueue from
TaskSnapshotPersister, which will handle the snapshot IO tasks for both
Task and Activity snapshot.
The Persister and Loader class just need to create a different
PersistInfoProvider if something need to specify with different config.

Bug: 207481538
Test: atest TaskSnapshotPersisterLoaderTest TaskSnapshotCacheTest
TaskSnapshotLowResDisabledTest

Change-Id: Id739aca9eaeff7935ebf4b5f9a8ebc7bcd2779c7
parent 383978a4
Loading
Loading
Loading
Loading
+374 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.wm;

import static android.graphics.Bitmap.CompressFormat.JPEG;

import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;

import android.annotation.NonNull;
import android.annotation.TestApi;
import android.graphics.Bitmap;
import android.os.Process;
import android.os.SystemClock;
import android.util.AtomicFile;
import android.util.Slog;
import android.window.TaskSnapshot;

import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.TaskSnapshotPersister.PersistInfoProvider;
import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayDeque;

/**
 * Singleton worker thread to queue up persist or delete tasks of {@link TaskSnapshot}s to disk.
 */
class SnapshotPersistQueue {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
    private static final long DELAY_MS = 100;
    private static final int MAX_STORE_QUEUE_DEPTH = 2;
    private static final int COMPRESS_QUALITY = 95;

    @GuardedBy("mLock")
    private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
    @GuardedBy("mLock")
    private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
    @GuardedBy("mLock")
    private boolean mQueueIdling;
    @GuardedBy("mLock")
    private boolean mPaused;
    private boolean mStarted;
    private final Object mLock = new Object();
    private final UserManagerInternal mUserManagerInternal;

    SnapshotPersistQueue() {
        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
    }

    Object getLock() {
        return mLock;
    }

    void systemReady() {
        start();
    }

    /**
     * Starts persisting.
     */
    void start() {
        if (!mStarted) {
            mStarted = true;
            mPersister.start();
        }
    }

    /**
     * Temporarily pauses/unpauses persisting of task snapshots.
     *
     * @param paused Whether task snapshot persisting should be paused.
     */
    void setPaused(boolean paused) {
        synchronized (mLock) {
            mPaused = paused;
            if (!paused) {
                mLock.notifyAll();
            }
        }
    }

    @TestApi
    void waitForQueueEmpty() {
        while (true) {
            synchronized (mLock) {
                if (mWriteQueue.isEmpty() && mQueueIdling) {
                    return;
                }
            }
            SystemClock.sleep(DELAY_MS);
        }
    }

    @GuardedBy("mLock")
    void sendToQueueLocked(WriteQueueItem item) {
        mWriteQueue.offer(item);
        item.onQueuedLocked();
        ensureStoreQueueDepthLocked();
        if (!mPaused) {
            mLock.notifyAll();
        }
    }

    @GuardedBy("mLock")
    private void ensureStoreQueueDepthLocked() {
        while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
            final StoreWriteQueueItem item = mStoreQueueItems.poll();
            mWriteQueue.remove(item);
            Slog.i(TAG, "Queue is too deep! Purged item with index=" + item.mId);
        }
    }

    private void deleteSnapshot(int index, int userId, PersistInfoProvider provider) {
        final File protoFile = provider.getProtoFile(index, userId);
        final File bitmapLowResFile = provider.getLowResolutionBitmapFile(index, userId);
        protoFile.delete();
        if (bitmapLowResFile.exists()) {
            bitmapLowResFile.delete();
        }
        final File bitmapFile = provider.getHighResolutionBitmapFile(index, userId);
        if (bitmapFile.exists()) {
            bitmapFile.delete();
        }
    }

    private final Thread mPersister = new Thread("TaskSnapshotPersister") {
        public void run() {
            android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            while (true) {
                WriteQueueItem next;
                boolean isReadyToWrite = false;
                synchronized (mLock) {
                    if (mPaused) {
                        next = null;
                    } else {
                        next = mWriteQueue.poll();
                        if (next != null) {
                            if (next.isReady()) {
                                isReadyToWrite = true;
                                next.onDequeuedLocked();
                            } else {
                                mWriteQueue.addLast(next);
                            }
                        }
                    }
                }
                if (next != null) {
                    if (isReadyToWrite) {
                        next.write();
                    }
                    SystemClock.sleep(DELAY_MS);
                }
                synchronized (mLock) {
                    final boolean writeQueueEmpty = mWriteQueue.isEmpty();
                    if (!writeQueueEmpty && !mPaused) {
                        continue;
                    }
                    try {
                        mQueueIdling = writeQueueEmpty;
                        mLock.wait();
                        mQueueIdling = false;
                    } catch (InterruptedException e) {
                    }
                }
            }
        }
    };

    abstract static class WriteQueueItem {
        protected final PersistInfoProvider mPersistInfoProvider;
        WriteQueueItem(@NonNull PersistInfoProvider persistInfoProvider) {
            mPersistInfoProvider = persistInfoProvider;
        }
        /**
         * @return {@code true} if item is ready to have {@link WriteQueueItem#write} called
         */
        boolean isReady() {
            return true;
        }

        abstract void write();

        /**
         * Called when this queue item has been put into the queue.
         */
        void onQueuedLocked() {
        }

        /**
         * Called when this queue item has been taken out of the queue.
         */
        void onDequeuedLocked() {
        }
    }

    StoreWriteQueueItem createStoreWriteQueueItem(int id, int userId, TaskSnapshot snapshot,
            PersistInfoProvider provider) {
        return new StoreWriteQueueItem(id, userId, snapshot, provider);
    }

    class StoreWriteQueueItem extends WriteQueueItem {
        private final int mId;
        private final int mUserId;
        private final TaskSnapshot mSnapshot;

        StoreWriteQueueItem(int id, int userId, TaskSnapshot snapshot,
                PersistInfoProvider provider) {
            super(provider);
            mId = id;
            mUserId = userId;
            mSnapshot = snapshot;
        }

        @GuardedBy("mLock")
        @Override
        void onQueuedLocked() {
            mStoreQueueItems.offer(this);
        }

        @GuardedBy("mLock")
        @Override
        void onDequeuedLocked() {
            mStoreQueueItems.remove(this);
        }

        @Override
        boolean isReady() {
            return mUserManagerInternal.isUserUnlocked(mUserId);
        }

        @Override
        void write() {
            if (!mPersistInfoProvider.createDirectory(mUserId)) {
                Slog.e(TAG, "Unable to create snapshot directory for user dir="
                        + mPersistInfoProvider.getDirectory(mUserId));
            }
            boolean failed = false;
            if (!writeProto()) {
                failed = true;
            }
            if (!writeBuffer()) {
                failed = true;
            }
            if (failed) {
                deleteSnapshot(mId, mUserId, mPersistInfoProvider);
            }
        }

        boolean writeProto() {
            final TaskSnapshotProto proto = new TaskSnapshotProto();
            proto.orientation = mSnapshot.getOrientation();
            proto.rotation = mSnapshot.getRotation();
            proto.taskWidth = mSnapshot.getTaskSize().x;
            proto.taskHeight = mSnapshot.getTaskSize().y;
            proto.insetLeft = mSnapshot.getContentInsets().left;
            proto.insetTop = mSnapshot.getContentInsets().top;
            proto.insetRight = mSnapshot.getContentInsets().right;
            proto.insetBottom = mSnapshot.getContentInsets().bottom;
            proto.letterboxInsetLeft = mSnapshot.getLetterboxInsets().left;
            proto.letterboxInsetTop = mSnapshot.getLetterboxInsets().top;
            proto.letterboxInsetRight = mSnapshot.getLetterboxInsets().right;
            proto.letterboxInsetBottom = mSnapshot.getLetterboxInsets().bottom;
            proto.isRealSnapshot = mSnapshot.isRealSnapshot();
            proto.windowingMode = mSnapshot.getWindowingMode();
            proto.appearance = mSnapshot.getAppearance();
            proto.isTranslucent = mSnapshot.isTranslucent();
            proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
            proto.id = mSnapshot.getId();
            final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
            final File file = mPersistInfoProvider.getProtoFile(mId, mUserId);
            final AtomicFile atomicFile = new AtomicFile(file);
            FileOutputStream fos = null;
            try {
                fos = atomicFile.startWrite();
                fos.write(bytes);
                atomicFile.finishWrite(fos);
            } catch (IOException e) {
                atomicFile.failWrite(fos);
                Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
                return false;
            }
            return true;
        }

        boolean writeBuffer() {
            if (TaskSnapshotController.isInvalidHardwareBuffer(mSnapshot.getHardwareBuffer())) {
                Slog.e(TAG, "Invalid task snapshot hw buffer, taskId=" + mId);
                return false;
            }
            final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
                    mSnapshot.getHardwareBuffer(), mSnapshot.getColorSpace());
            if (bitmap == null) {
                Slog.e(TAG, "Invalid task snapshot hw bitmap");
                return false;
            }

            final Bitmap swBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false /* isMutable */);

            final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId);
            try {
                FileOutputStream fos = new FileOutputStream(file);
                swBitmap.compress(JPEG, COMPRESS_QUALITY, fos);
                fos.close();
            } catch (IOException e) {
                Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
                return false;
            }

            if (!mPersistInfoProvider.enableLowResSnapshots()) {
                swBitmap.recycle();
                return true;
            }

            final Bitmap lowResBitmap = Bitmap.createScaledBitmap(swBitmap,
                    (int) (bitmap.getWidth() * mPersistInfoProvider.lowResScaleFactor()),
                    (int) (bitmap.getHeight() * mPersistInfoProvider.lowResScaleFactor()),
                    true /* filter */);
            swBitmap.recycle();

            final File lowResFile = mPersistInfoProvider.getLowResolutionBitmapFile(mId, mUserId);
            try {
                FileOutputStream lowResFos = new FileOutputStream(lowResFile);
                lowResBitmap.compress(JPEG, COMPRESS_QUALITY, lowResFos);
                lowResFos.close();
            } catch (IOException e) {
                Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e);
                return false;
            }
            lowResBitmap.recycle();

            return true;
        }
    }

    DeleteWriteQueueItem createDeleteWriteQueueItem(int id, int userId,
            PersistInfoProvider provider) {
        return new DeleteWriteQueueItem(id, userId, provider);
    }

    private class DeleteWriteQueueItem extends WriteQueueItem {
        private final int mId;
        private final int mUserId;

        DeleteWriteQueueItem(int id, int userId, PersistInfoProvider provider) {
            super(provider);
            mId = id;
            mUserId = userId;
        }

        @Override
        void write() {
            deleteSnapshot(mId, mUserId, mPersistInfoProvider);
        }
    }
}
+42 −17
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
import com.android.server.wm.TaskSnapshotPersister.PersistInfoProvider;
import com.android.server.wm.utils.InsetUtils;

import com.google.android.collect.Sets;
@@ -93,6 +94,8 @@ class TaskSnapshotController {
    @VisibleForTesting
    static final int SNAPSHOT_MODE_NONE = 2;

    static final String SNAPSHOTS_DIRNAME = "snapshots";

    private final WindowManagerService mService;

    private final TaskSnapshotCache mCache;
@@ -119,11 +122,14 @@ class TaskSnapshotController {
     * Flag indicating if task snapshot is enabled on this device.
     */
    private boolean mTaskSnapshotEnabled;
    private final PersistInfoProvider mPersistInfoProvider;

    TaskSnapshotController(WindowManagerService service) {
    TaskSnapshotController(WindowManagerService service, SnapshotPersistQueue persistQueue) {
        mService = service;
        mPersister = new TaskSnapshotPersister(mService, Environment::getDataSystemCeDirectory);
        mLoader = new TaskSnapshotLoader(mPersister);
        mPersistInfoProvider = createPersistInfoProvider(service,
                Environment::getDataSystemCeDirectory);
        mPersister = new TaskSnapshotPersister(persistQueue, mPersistInfoProvider);
        mLoader = new TaskSnapshotLoader(mPersistInfoProvider);
        mCache = new TaskSnapshotCache(mService, mLoader);
        mIsRunningOnTv = mService.mContext.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_LEANBACK);
@@ -137,8 +143,36 @@ class TaskSnapshotController {
                        .getBoolean(com.android.internal.R.bool.config_disableTaskSnapshots);
    }

    void systemReady() {
        mPersister.start();
    static PersistInfoProvider createPersistInfoProvider(WindowManagerService service,
            TaskSnapshotPersister.DirectoryResolver resolver) {
        final float highResTaskSnapshotScale = service.mContext.getResources().getFloat(
                com.android.internal.R.dimen.config_highResTaskSnapshotScale);
        final float lowResTaskSnapshotScale = service.mContext.getResources().getFloat(
                com.android.internal.R.dimen.config_lowResTaskSnapshotScale);

        if (lowResTaskSnapshotScale < 0 || 1 <= lowResTaskSnapshotScale) {
            throw new RuntimeException("Low-res scale must be between 0 and 1");
        }
        if (highResTaskSnapshotScale <= 0 || 1 < highResTaskSnapshotScale) {
            throw new RuntimeException("High-res scale must be between 0 and 1");
        }
        if (highResTaskSnapshotScale <= lowResTaskSnapshotScale) {
            throw new RuntimeException("High-res scale must be greater than low-res scale");
        }

        final float lowResScaleFactor;
        final boolean enableLowResSnapshots;
        if (lowResTaskSnapshotScale > 0) {
            lowResScaleFactor = lowResTaskSnapshotScale / highResTaskSnapshotScale;
            enableLowResSnapshots = true;
        } else {
            lowResScaleFactor = 0;
            enableLowResSnapshots = false;
        }
        final boolean use16BitFormat = service.mContext.getResources().getBoolean(
                com.android.internal.R.bool.config_use16BitTaskSnapshotPixelFormat);
        return new PersistInfoProvider(resolver, SNAPSHOTS_DIRNAME,
                enableLowResSnapshots, lowResScaleFactor, use16BitFormat);
    }

    void onTransitionStarting(DisplayContent displayContent) {
@@ -247,7 +281,7 @@ class TaskSnapshotController {
    TaskSnapshot getSnapshot(int taskId, int userId, boolean restoreFromDisk,
            boolean isLowResolution) {
        return mCache.getSnapshot(taskId, userId, restoreFromDisk, isLowResolution
                && mPersister.enableLowResSnapshots());
                && mPersistInfoProvider.enableLowResSnapshots());
    }

    /**
@@ -310,7 +344,7 @@ class TaskSnapshotController {
        final boolean isShowWallpaper = mainWindow.hasWallpaper();

        if (pixelFormat == PixelFormat.UNKNOWN) {
            pixelFormat = mPersister.use16BitFormat() && activity.fillsParent()
            pixelFormat = mPersistInfoProvider.use16BitFormat() && activity.fillsParent()
                    && !(isWindowTranslucent && isShowWallpaper)
                    ? PixelFormat.RGB_565
                    : PixelFormat.RGBA_8888;
@@ -416,7 +450,7 @@ class TaskSnapshotController {
        if (checkIfReadyToSnapshot(task) == null) {
            return null;
        }
        final int pixelFormat = mPersister.use16BitFormat()
        final int pixelFormat = mPersistInfoProvider.use16BitFormat()
                    ? PixelFormat.RGB_565
                    : PixelFormat.RGBA_8888;
        return createImeSnapshot(task, pixelFormat);
@@ -636,15 +670,6 @@ class TaskSnapshotController {
        mPersister.removeObsoleteFiles(persistentTaskIds, runningUserIds);
    }

    /**
     * Temporarily pauses/unpauses persisting of task snapshots.
     *
     * @param paused Whether task snapshot persisting should be paused.
     */
    void setPersisterPaused(boolean paused) {
        mPersister.setPaused(paused);
    }

    /**
     * Called when screen is being turned off.
     */
+11 −9
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.hardware.HardwareBuffer;
import android.util.Slog;
import android.window.TaskSnapshot;

import com.android.server.wm.TaskSnapshotPersister.PersistInfoProvider;
import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;

import java.io.File;
@@ -48,10 +49,10 @@ class TaskSnapshotLoader {

    private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotLoader" : TAG_WM;

    private final TaskSnapshotPersister mPersister;
    private final PersistInfoProvider mPersistInfoProvider;

    TaskSnapshotLoader(TaskSnapshotPersister persister) {
        mPersister = persister;
    TaskSnapshotLoader(PersistInfoProvider persistInfoProvider) {
        mPersistInfoProvider = persistInfoProvider;
    }

    static class PreRLegacySnapshotConfig {
@@ -137,14 +138,15 @@ class TaskSnapshotLoader {
     * @return The loaded {@link TaskSnapshot} or {@code null} if it couldn't be loaded.
     */
    TaskSnapshot loadTask(int taskId, int userId, boolean loadLowResolutionBitmap) {
        final File protoFile = mPersister.getProtoFile(taskId, userId);
        final File protoFile = mPersistInfoProvider.getProtoFile(taskId, userId);
        if (!protoFile.exists()) {
            return null;
        }
        try {
            final byte[] bytes = Files.readAllBytes(protoFile.toPath());
            final TaskSnapshotProto proto = TaskSnapshotProto.parseFrom(bytes);
            final File highResBitmap = mPersister.getHighResolutionBitmapFile(taskId, userId);
            final File highResBitmap = mPersistInfoProvider
                    .getHighResolutionBitmapFile(taskId, userId);

            PreRLegacySnapshotConfig legacyConfig = getLegacySnapshotConfig(proto.taskWidth,
                    proto.legacyScale, highResBitmap.exists(), loadLowResolutionBitmap);
@@ -152,16 +154,16 @@ class TaskSnapshotLoader {
            boolean forceLoadReducedJpeg =
                    legacyConfig != null && legacyConfig.mForceLoadReducedJpeg;
            File bitmapFile = (loadLowResolutionBitmap || forceLoadReducedJpeg)
                    ? mPersister.getLowResolutionBitmapFile(taskId, userId) : highResBitmap;
                    ? mPersistInfoProvider.getLowResolutionBitmapFile(taskId, userId)
                    : highResBitmap;

            if (!bitmapFile.exists()) {
                return null;
            }

            final Options options = new Options();
            options.inPreferredConfig = mPersister.use16BitFormat() && !proto.isTranslucent
                    ? Config.RGB_565
                    : Config.ARGB_8888;
            options.inPreferredConfig = mPersistInfoProvider.use16BitFormat()
                    && !proto.isTranslucent ? Config.RGB_565 : Config.ARGB_8888;
            final Bitmap bitmap = BitmapFactory.decodeFile(bitmapFile.getPath(), options);
            if (bitmap == null) {
                Slog.w(TAG, "Failed to load bitmap: " + bitmapFile.getPath());
+72 −377

File changed.

Preview size limit exceeded, changes collapsed.

+2 −2
Original line number Diff line number Diff line
@@ -626,12 +626,12 @@ class TransitionController {
            mWakeT.apply();
            // Usually transitions put quite a load onto the system already (with all the things
            // happening in app), so pause task snapshot persisting to not increase the load.
            mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(true);
            mAtm.mWindowManager.mSnapshotPersistQueue.setPaused(true);
            mTransitionPlayerProc.setRunningRemoteAnimation(true);
        } else if (mPlayingTransitions.isEmpty()) {
            mWakeT.setEarlyWakeupEnd();
            mWakeT.apply();
            mAtm.mWindowManager.mTaskSnapshotController.setPersisterPaused(false);
            mAtm.mWindowManager.mSnapshotPersistQueue.setPaused(false);
            mTransitionPlayerProc.setRunningRemoteAnimation(false);
            mRemotePlayer.clear();
            return;
Loading