Optimization of app memory recycling in Android (1): R version

Version based on: Android R

0. Preface

Android Q has added a framework-side app memory recycling optimization solution. When the app's oom adj changes specifically, the framework side will process the application's memory. As the version evolves, this part of the optimization work has been improved. The author will analyze the optimization process of this part in detail for Android R and Android S respectively.

This article is for Android R.

Note: The word "compression" mentioned in this article actually refers to memory recycling optimization, because it is only when the exact logic is revealed that it is clear whether it is anonymous page recycling or file page recycling. Before that, we tentatively determined it as compact. deal with.

1. CachedAppOptimizer class

The memory compression management on the App side is completed in the CachedAppOptimizer class.

We learned in previous blog posts "oom_adj Update Principle (1)" and "oom_adj Update Principle (2)" that AMS manages the update, calculation, and application of oom_adj through the OomAdjuster class. A CachedAppOptimizer object is also instantiated in OomAdjuster:

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java

    OomAdjuster(ActivityManagerService service, ProcessList processList, ActiveUids activeUids,
            ServiceThread adjusterThread) {
        mService = service;
        ...
        mCachedAppOptimizer = new CachedAppOptimizer(mService);
        ...
    }

The parameters are AMS objects.

Let's take a look at  the CachedAppOptimizer structure :

frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java

    public CachedAppOptimizer(ActivityManagerService am) {
        this(am, null, new DefaultProcessDependencies());
    }

    @VisibleForTesting
    CachedAppOptimizer(ActivityManagerService am, PropertyChangedCallbackForTest callback,
            ProcessDependencies processDependencies) {
        mAm = am;
        mCachedAppOptimizerThread = new ServiceThread("CachedAppOptimizerThread",
            Process.THREAD_GROUP_SYSTEM, true);
        mProcStateThrottle = new HashSet<>();
        mProcessDependencies = processDependencies;
        mTestCallback = callback;
    }

A ServiceThread is created in the construction with the name CachedAppOptimizerThread and the priority THREAD_GROUP_SYSTEM .

mProcessDependencies is an object of type DefaultProcessDependencies, used for final compression processing.

2. init()

The init() function is triggered by calling the installSystemProviders() function in AMS when SystemServer.startOtherServices(). mOomAdjuster.initSettings() is called in installSystemProviders():

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java

    void initSettings() {
        mCachedAppOptimizer.init();
        ...
    }

Let’s take a look at the init() function:

frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java

    public void init() {
        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                ActivityThread.currentApplication().getMainExecutor(), mOnFlagsChangedListener);
        synchronized (mPhenotypeFlagLock) {
            updateUseCompaction();
            updateCompactionActions();
            updateCompactionThrottles();
            updateCompactStatsdSampleRate();
            updateFreezerStatsdSampleRate();
            updateFullRssThrottle();
            updateFullDeltaRssThrottle();
            updateProcStateThrottle();
            updateUseFreezer();
        }
    }

The function first sets a listening listener for the attribute variable of DeviceConfig. When the attribute whose namespace is activity_manager in DeviceConfig changes, the mOnFlagsChangedListener() function will be called back.

Then there are a bunch of update functions to initialize some member variables. Let’s pick a few for analysis.

2.1 updateUseCompaction()

    private void updateUseCompaction() {
        mUseCompaction = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                    KEY_USE_COMPACTION, DEFAULT_USE_COMPACTION);

        if (mUseCompaction && mCompactionHandler == null) {
            if (!mCachedAppOptimizerThread.isAlive()) {
                mCachedAppOptimizerThread.start();
            }

            mCompactionHandler = new MemCompactionHandler();

            Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(),
                    Process.THREAD_GROUP_SYSTEM);
        }
    }

First get the attribute use_compaction, the default value is DEFAULT_USE_COMPACTION ( false ):

    @VisibleForTesting static final Boolean DEFAULT_USE_COMPACTION = false;

If user_compation is enabled, then mCompactionHandler will be created for asynchronous message processing and the ServiceThread created in the constructor will be started.

Let’s take a look at the MemCompactionHandler class:

    private final class MemCompactionHandler extends Handler {
        private MemCompactionHandler() {
            super(mCachedAppOptimizerThread.getLooper());
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case COMPACT_PROCESS_MSG: {
                    ...
                    break;
                }
                case COMPACT_SYSTEM_MSG: {
                    ...
                    break;
                }
            }
        }

Looper uses ServiceThread's Looper, which is mainly used to process compressed process memory or system process memory.

2.2 updateCompactionActions()

    private void updateCompactionActions() {
        int compactAction1 = DeviceConfig.getInt(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                KEY_COMPACT_ACTION_1, DEFAULT_COMPACT_ACTION_1);

        int compactAction2 = DeviceConfig.getInt(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                KEY_COMPACT_ACTION_2, DEFAULT_COMPACT_ACTION_2);

        mCompactActionSome = compactActionIntToString(compactAction1);
        mCompactActionFull = compactActionIntToString(compactAction2);
    }

Mainly to confirm the actions executed during some and full compression. By default:

  • some takes DEFAULT_COMPACT_ACTION_1( file );
  • full takes DEFAULT_COMPACT_ACTION_2 ( all );
    @VisibleForTesting static final int DEFAULT_COMPACT_ACTION_1 = COMPACT_ACTION_FILE_FLAG;
    @VisibleForTesting static final int DEFAULT_COMPACT_ACTION_2 = COMPACT_ACTION_FULL_FLAG;

We will later rely on these two variables in the choice of compression. The final compression strategy is:

  • pendingAction is SOME, compressed using mCompactActionSome;
  • pendingAction is FULL / PERSISTENT  BFGS, compressed using mCompactActionFull;
  • system is compressed in all mode;

See Section 4 for detailed logic .

2.3 updateCompactionThrottles()

    private void updateCompactionThrottles() {
        boolean useThrottleDefaults = false;
        String throttleSomeSomeFlag =
                DeviceConfig.getProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                    KEY_COMPACT_THROTTLE_1);
        String throttleSomeFullFlag =
                DeviceConfig.getProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                    KEY_COMPACT_THROTTLE_2);
        String throttleFullSomeFlag =
                DeviceConfig.getProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                    KEY_COMPACT_THROTTLE_3);
        String throttleFullFullFlag =
                DeviceConfig.getProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                    KEY_COMPACT_THROTTLE_4);
        String throttleBFGSFlag =
                DeviceConfig.getProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                    KEY_COMPACT_THROTTLE_5);
        String throttlePersistentFlag =
                DeviceConfig.getProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                    KEY_COMPACT_THROTTLE_6);

        if (TextUtils.isEmpty(throttleSomeSomeFlag) || TextUtils.isEmpty(throttleSomeFullFlag)
                || TextUtils.isEmpty(throttleFullSomeFlag)
                || TextUtils.isEmpty(throttleFullFullFlag)
                || TextUtils.isEmpty(throttleBFGSFlag)
                || TextUtils.isEmpty(throttlePersistentFlag)) {
            // Set defaults for all if any are not set.
            useThrottleDefaults = true;
        } else {
            try {
                mCompactThrottleSomeSome = Integer.parseInt(throttleSomeSomeFlag);
                mCompactThrottleSomeFull = Integer.parseInt(throttleSomeFullFlag);
                mCompactThrottleFullSome = Integer.parseInt(throttleFullSomeFlag);
                mCompactThrottleFullFull = Integer.parseInt(throttleFullFullFlag);
                mCompactThrottleBFGS = Integer.parseInt(throttleBFGSFlag);
                mCompactThrottlePersistent = Integer.parseInt(throttlePersistentFlag);
            } catch (NumberFormatException e) {
                useThrottleDefaults = true;
            }
        }

        if (useThrottleDefaults) {
            mCompactThrottleSomeSome = DEFAULT_COMPACT_THROTTLE_1;
            mCompactThrottleSomeFull = DEFAULT_COMPACT_THROTTLE_2;
            mCompactThrottleFullSome = DEFAULT_COMPACT_THROTTLE_3;
            mCompactThrottleFullFull = DEFAULT_COMPACT_THROTTLE_4;
            mCompactThrottleBFGS = DEFAULT_COMPACT_THROTTLE_5;
            mCompactThrottlePersistent = DEFAULT_COMPACT_THROTTLE_6;
        }
    }

The code is relatively simple, get the DeviceConfig attribute KEY_COMPACT_THROTTLE_ 1 ~ 6 ;

If one of the properties is not set, all default values ​​are used:

    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_1 = 5_000;
    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_2 = 10_000;
    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_3 = 500;
    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_4 = 10_000;
    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_5 = 10 * 60 * 1000;
    @VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_6 = 10 * 60 * 1000;

It is used to compare the time difference between the last compression time and this compression. In order to prevent continuous compression requests , if continuous compression requests are made within the limited time, the system is not allowed, and the code returns directly.

2.4 updateFullRssThrottle()

    private void updateFullRssThrottle() {
        mFullAnonRssThrottleKb = DeviceConfig.getLong(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                KEY_COMPACT_FULL_RSS_THROTTLE_KB, DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB);

        // Don't allow negative values. 0 means don't apply the throttle.
        if (mFullAnonRssThrottleKb < 0) {
            mFullAnonRssThrottleKb = DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB;
        }
    }

When it is an anon request or an all request, you need to consider the remaining limit of the anonymous page . When it is lower than this limit, compression and recycling will not continue.

In addition, it can be seen logically that this limit value cannot be a negative number. If DeviceConfig sets KEY_COMPACT_FULL_RSS_THROTTLE_KB to a negative number, it means that the default value of 12M is used as the limit value.

2.5 updateFullDeltaRssThrottle()

    private void updateFullDeltaRssThrottle() {
        mFullDeltaRssThrottleKb = DeviceConfig.getLong(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB, DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB);

        if (mFullDeltaRssThrottleKb < 0) {
            mFullDeltaRssThrottleKb = DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB;
        }
    }

Same as 2.4, when it is an anon request or an all request, there will be another restriction. That is to compare the RSS after the last compression with the RSS requested this time. If there is only a small memory change between them, it means that the memory demand is not large or urgent, and the memory compression process will not continue.

Similarly, this value cannot be a negative number. If DeviceConfig sets KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB  to a negative number, it means using the default value of 8M as the limit value.

2.6 updateProcStateThrottle()

    private void updateProcStateThrottle() {
        String procStateThrottleString = DeviceConfig.getString(
                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, KEY_COMPACT_PROC_STATE_THROTTLE,
                DEFAULT_COMPACT_PROC_STATE_THROTTLE);
        if (!parseProcStateThrottle(procStateThrottleString)) {
            ...
            if (!parseProcStateThrottle(DEFAULT_COMPACT_PROC_STATE_THROTTLE)) {
                ...
            }
        }
    }

    private boolean parseProcStateThrottle(String procStateThrottleString) {
        String[] procStates = TextUtils.split(procStateThrottleString, ",");
        mProcStateThrottle.clear();
        for (String procState : procStates) {
            try {
                mProcStateThrottle.add(Integer.parseInt(procState));
            } catch (NumberFormatException e) {
                Slog.e(TAG_AM, "Failed to parse default app compaction proc state: "
                        + procState);
                return false;
            }
        }
        return true;
    }

Process state restrictions, when the process is in the state in mProcStateThrottle , no recycling is performed.

The collection of mProcStateThrottle comes from the KEY_COMPACT_PROC_STATE_THROTTLE attribute in DeviceConfig. The value of this attribute specifies the process limit state, which can be multiple, separated by commas. If the attribute value fails to be parsed, the default state value will be used, and the default process state limit value is  PROCESS_STATE_RECEIVER .

2.7 updateUseFreezer()

The process freeze state is initialized, and freeze optimization will be analyzed separately later.

3. Compression optimization initiator

Compression optimization is triggered in various situations. In CachedAppOptimizer, the compression optimization initiators mainly include:

  • compactAppSome()
  • compactAppFull()
  • compactAppPersistent()
  • compactAppBfgs()
  • compactAllSystem()

This section analyzes the use of each function and the triggering scenarios.

3.1 compactAppSome()

Triggered in OomAdjuster.ApplyOomAdjLocked () :

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java

    private final boolean applyOomAdjLocked(ProcessRecord app, boolean doingAll, long now,
            long nowElapsed) {
        ...
 
        // 在bootup 阶段不压缩
        // useCompaction() 是app compact是否使能
        if (mCachedAppOptimizer.useCompaction() && mService.mBooted) {
            // 如果跟上次的adj 不一样,有变化
            if (app.curAdj != app.setAdj) {
                // 当app从perceptible变为home/previous,执行some等级内存压缩
                // 当app变成cached,执行full等级内存压缩
                if (app.setAdj <= ProcessList.PERCEPTIBLE_APP_ADJ &&
                        (app.curAdj == ProcessList.PREVIOUS_APP_ADJ ||
                                app.curAdj == ProcessList.HOME_APP_ADJ)) {
                    mCachedAppOptimizer.compactAppSome(app);
                } else if ((app.setAdj < ProcessList.CACHED_APP_MIN_ADJ
                                || app.setAdj > ProcessList.CACHED_APP_MAX_ADJ)
                        && app.curAdj >= ProcessList.CACHED_APP_MIN_ADJ
                        && app.curAdj <= ProcessList.CACHED_APP_MAX_ADJ) {
                    mCachedAppOptimizer.compactAppFull(app);
                }
            } else if (mService.mWakefulness != PowerManagerInternal.WAKEFULNESS_AWAKE  //非唤醒状态
                    && app.setAdj < ProcessList.FOREGROUND_APP_ADJ
                    && mCachedAppOptimizer.shouldCompactPersistent(app, now)) {
                //处于非唤醒状态,且上一次重要性高于前台进程adj,
                //  且上一次压缩的时间已经尝过10min或者上次没压缩都返回true,
                //  进行persistent 级别压缩
                mCachedAppOptimizer.compactAppPersistent(app);
            } else if (mService.mWakefulness != PowerManagerInternal.WAKEFULNESS_AWAKE
                    && app.getCurProcState()
                        == ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
                    && mCachedAppOptimizer.shouldCompactBFGS(app, now)) {
                // 非唤醒状态,且当前进程状态为绑定一个前台service进程
                //   且上一次压缩的时间已经超过10min或上一次没压缩都返回true
                mCachedAppOptimizer.compactAppBfgs(app);
            }
        }

        ...
 
        return success;
    }

When the app compaction function is enabled and the system completes boot:

  • When oom adj changes:
    • When the last adj (setAdj) <= PERCEPTIBLE_APP_ADJ, it becomes  PREVIOUS_APP_ADJ or  HOME_APP_ADJ, use  compactAppSome() for compression optimization;
    • When the last adj (setAdj) is not cached adj, it becomes cached adj, and  compactAppFull() is used for compression optimization;
  • When in the non-awakening state, and when the last adj (setAdj) is more important than the foreground adj ( FOEGROUND_APP_ADJ ), and the interval from the last compression is more than 10 minutes or has never been compressed, use compactAppPersistent() compression optimization;
  • When in the non-awakened state, and the application process status is  PROCESS_STATE_BOUND_FOREGROUND_SERVICE , and the interval since the last compression is more than 10 minutes or has never been compressed, use  compactAppBfgs() compression optimization;

Let’s take a look at what compactAppSome() does:

    void compactAppSome(ProcessRecord app) {
        synchronized (this) {
            app.reqCompactAction = COMPACT_PROCESS_SOME;
            if (!app.mPendingCompact) {
                app.mPendingCompact = true;
                mPendingCompactionProcesses.add(app);
                mCompactionHandler.sendMessage(
                        mCompactionHandler.obtainMessage(
                        COMPACT_PROCESS_MSG, app.setAdj, app.setProcState));
            }
        }
    }
  • Specify the compression action requested by the process as  COMPACT_PROCESS_SOME ;
  • If the process is not in a compressed state, prepare to enter:
    • Specify mPendingCompact as true to mark the entry into the compression state;
    • Add the process to mPendingCompactionProcesses, and after subsequent messages are received, the processes that need to be compressed will be fetched from here;
    • Send message COMPACT_PROCESS_MSG to the thread where mCompactionHandler is located, with parameters app.setAdj and app.setProcState;

3.2 compactAppFull()

Same as Section 3.1 , it is also triggered in OomAdjuster.ApplyOomAdjLocked() .

    void compactAppFull(ProcessRecord app) {
        synchronized (this) {
            app.reqCompactAction = COMPACT_PROCESS_FULL;
            if (!app.mPendingCompact) {
                app.mPendingCompact = true;
                mPendingCompactionProcesses.add(app);
                mCompactionHandler.sendMessage(
                        mCompactionHandler.obtainMessage(
                        COMPACT_PROCESS_MSG, app.setAdj, app.setProcState));
            }
        }
    }

The code is similar to compactAppSome(), here the compact action is replaced by  COMPACT_PROCESS_FULL .

3.3 compactAppPersistent()

Same as Section 3.1 , it is also triggered in OomAdjuster.ApplyOomAdjLocked() .

    void compactAppPersistent(ProcessRecord app) {
        synchronized (this) {
            app.reqCompactAction = COMPACT_PROCESS_PERSISTENT;
            if (!app.mPendingCompact) {
                app.mPendingCompact = true;
                mPendingCompactionProcesses.add(app);
                mCompactionHandler.sendMessage(
                        mCompactionHandler.obtainMessage(
                        COMPACT_PROCESS_MSG, app.curAdj, app.setProcState));
            }
        }
    }

The code is similar to compactAppSome(), here the compact action is replaced by  COMPACT_PROCESS_PERSISTENT .

3.4 compactAppBfgs()

Same as Section 3.1 , it is also triggered in OomAdjuster.ApplyOomAdjLocked() .

    void compactAppBfgs(ProcessRecord app) {
        synchronized (this) {
            app.reqCompactAction = COMPACT_PROCESS_BFGS;
            if (!app.mPendingCompact) {
                app.mPendingCompact = true;
                mPendingCompactionProcesses.add(app);
                mCompactionHandler.sendMessage(
                        mCompactionHandler.obtainMessage(
                        COMPACT_PROCESS_MSG, app.curAdj, app.setProcState));
            }
        }
    }

The code is similar to compactAppSome(), here the compact action is replaced by  COMPACT_PROCESS_BFGS .

3.5 compactAllSystem()

The triggering of this function is different from the above four. The triggering is divided into two parts:

  • When finishBooting() is called in AMS, all processes in the system are fully compressed after the system startup is completed ;
  • MountServiceIdler calls AMS.performIdleMaintenance() every time startJob() to perform system compression;
    void compactAllSystem() {
        if (mUseCompaction) {
            mCompactionHandler.sendMessage(mCompactionHandler.obtainMessage(
                                              COMPACT_SYSTEM_MSG));
        }
    }

Send  the COMPACT_SYSTEM_MSG message to the thread where mCompactionHandler is located.

4. Compressed message processing

In the previous section, the triggering process of compression optimization was analyzed. In the end, the compression optimization instructions are completed by sending messages to  mCompactionHandler . This section looks at the processing flow of different messages by  mCompactionHandler .

        public void handleMessage(Message msg) {
            switch (msg.what) {
                case COMPACT_PROCESS_MSG: {
                    // 记录此次压缩处理的起始时间
                    long start = SystemClock.uptimeMillis();
                    ProcessRecord proc;
                    int pid;
                    String action;
                    final String name;
                    int pendingAction, lastCompactAction;
                    long lastCompactTime;
                    LastCompactionStats lastCompactionStats;
                    // 获取消息带过来的参数
                    int lastOomAdj = msg.arg1;
                    int procState = msg.arg2;
                    synchronized (CachedAppOptimizer.this) {
                        // 取出队列中的第一个进程
                        proc = mPendingCompactionProcesses.remove(0);

                        // 获取压缩 action
                        pendingAction = proc.reqCompactAction;
                        // 获取进程pid
                        pid = proc.mPidForCompact;
                        // 获取进程名
                        name = proc.processName;
                        // 进程进入压缩,该标记可以置回false,等待下一次压缩
                        proc.mPendingCompact = false;

                        // 对于 SOME 和 FULL 的action,如果进程的setAdj 小于可感知,不做处理
                        // 我们从applyOomAdjLocked() 得知,SOME 只针对HOME/PREVIOUS adj,
                        //   而FULL 只针对 cached adj 的进程
                        if ((pendingAction == COMPACT_PROCESS_SOME
                                || pendingAction == COMPACT_PROCESS_FULL)
                                && (proc.mSetAdjForCompact <= ProcessList.PERCEPTIBLE_APP_ADJ)) {
                            return;
                        }

                        // 获取上一次压缩的信息
                        lastCompactAction = proc.lastCompactAction;
                        lastCompactTime = proc.lastCompactTime;
                        lastCompactionStats = mLastCompactionStats.get(pid);
                    }

                    // 不接受pid 为0的进程
                    if (pid == 0) {
                        // not a real process, either one being launched or one being killed
                        return;
                    }

                    // basic throttling
                    // use the Phenotype flag knobs to determine whether current/prevous
                    // compaction combo should be throtted or not

                    // Note that we explicitly don't take mPhenotypeFlagLock here as the flags
                    // should very seldom change, and taking the risk of using the wrong action is
                    // preferable to taking the lock for every single compaction action.

                    // 开始一些限制条件的过滤
                    // 限制条件1,应用已经执行过 compaction,确认与上一次压缩的时间间隔是否在限制内
                    if (lastCompactTime != 0) {
                        if (pendingAction == COMPACT_PROCESS_SOME) {
                            if ((lastCompactAction == COMPACT_PROCESS_SOME
                                    && (start - lastCompactTime < mCompactThrottleSomeSome))
                                    || (lastCompactAction == COMPACT_PROCESS_FULL
                                        && (start - lastCompactTime
                                                < mCompactThrottleSomeFull))) {
                                return;
                            }
                        } else if (pendingAction == COMPACT_PROCESS_FULL) {
                            if ((lastCompactAction == COMPACT_PROCESS_SOME
                                    && (start - lastCompactTime < mCompactThrottleFullSome))
                                    || (lastCompactAction == COMPACT_PROCESS_FULL
                                        && (start - lastCompactTime
                                                < mCompactThrottleFullFull))) {
                                return;
                            }
                        } else if (pendingAction == COMPACT_PROCESS_PERSISTENT) {
                            if (start - lastCompactTime < mCompactThrottlePersistent) {
                                return;
                            }
                        } else if (pendingAction == COMPACT_PROCESS_BFGS) {
                            if (start - lastCompactTime < mCompactThrottleBFGS) {
                                return;
                            }
                        }
                    }

                    switch (pendingAction) {
                        case COMPACT_PROCESS_SOME:
                            action = mCompactActionSome;
                            break;
                        // For the time being, treat these as equivalent.
                        case COMPACT_PROCESS_FULL:
                        case COMPACT_PROCESS_PERSISTENT:
                        case COMPACT_PROCESS_BFGS:
                            action = mCompactActionFull;
                            break;
                        default:
                            action = COMPACT_ACTION_NONE;
                            break;
                    }

                    // 限制条件2,action 不能为 NONE
                    if (COMPACT_ACTION_NONE.equals(action)) {
                        return;
                    }

                    // 限制条件3,进程的procState 不能在限制的state 集合内
                    if (mProcStateThrottle.contains(procState)) {
                        return;
                    }

                    long[] rssBefore = mProcessDependencies.getRss(pid);
                    long anonRssBefore = rssBefore[2];

                    // 限制条件4,确定RSS是否不正常
                    if (rssBefore[0] == 0 && rssBefore[1] == 0 && rssBefore[2] == 0
                            && rssBefore[3] == 0) {
                        return;
                    }

                    // 限制条件5,对于FULL 压缩,或者匿名页压缩
                    // 对于RSS 使用小于 12MB 的进程,不做压缩处理;
                    // 对于上一次压缩时 RSS 与此次压缩时RSS,相差小于 8MB 的进程,不做压缩处理
                    if (action.equals(COMPACT_ACTION_FULL) || action.equals(COMPACT_ACTION_ANON)) {
                        if (mFullAnonRssThrottleKb > 0L
                                && anonRssBefore < mFullAnonRssThrottleKb) {
                            return;
                        }

                        if (lastCompactionStats != null && mFullDeltaRssThrottleKb > 0L) {
                            long[] lastRss = lastCompactionStats.getRssAfterCompaction();
                            long absDelta = Math.abs(rssBefore[1] - lastRss[1])
                                    + Math.abs(rssBefore[2] - lastRss[2])
                                    + Math.abs(rssBefore[3] - lastRss[3]);
                            if (absDelta <= mFullDeltaRssThrottleKb) {
                                return;
                            }
                        }
                    }

                    /*************限制条件全部通过,进入compact 流程**********************/

                    // 更新压缩计数
                    switch (pendingAction) {
                        case COMPACT_PROCESS_SOME:
                            mSomeCompactionCount++;
                            break;
                        case COMPACT_PROCESS_FULL:
                            mFullCompactionCount++;
                            break;
                        case COMPACT_PROCESS_PERSISTENT:
                            mPersistentCompactionCount++;
                            break;
                        case COMPACT_PROCESS_BFGS:
                            mBfgsCompactionCount++;
                            break;
                        default:
                            break;
                    }
                    try {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Compact "
                                + ((pendingAction == COMPACT_PROCESS_SOME) ? "some" : "full")
                                + ": " + name);
                        long zramFreeKbBefore = Debug.getZramFreeKb();

                        // 压缩处理的核心函数 performCompaction()
                        mProcessDependencies.performCompaction(action, pid);

                        // 压缩完成之后,进行一些数据的记录
                        long[] rssAfter = mProcessDependencies.getRss(pid);
                        long end = SystemClock.uptimeMillis();
                        long time = end - start;
                        long zramFreeKbAfter = Debug.getZramFreeKb();
                        EventLog.writeEvent(EventLogTags.AM_COMPACT, pid, name, action,
                                rssBefore[0], rssBefore[1], rssBefore[2], rssBefore[3],
                                rssAfter[0] - rssBefore[0], rssAfter[1] - rssBefore[1],
                                rssAfter[2] - rssBefore[2], rssAfter[3] - rssBefore[3], time,
                                lastCompactAction, lastCompactTime, lastOomAdj, procState,
                                zramFreeKbBefore, zramFreeKbAfter - zramFreeKbBefore);
                        // Note that as above not taking mPhenoTypeFlagLock here to avoid locking
                        // on every single compaction for a flag that will seldom change and the
                        // impact of reading the wrong value here is low.
                        if (mRandom.nextFloat() < mCompactStatsdSampleRate) {
                            FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPACTED, pid, name,
                                    pendingAction, rssBefore[0], rssBefore[1], rssBefore[2],
                                    rssBefore[3], rssAfter[0], rssAfter[1], rssAfter[2],
                                    rssAfter[3], time, lastCompactAction, lastCompactTime,
                                    lastOomAdj, ActivityManager.processStateAmToProto(procState),
                                    zramFreeKbBefore, zramFreeKbAfter);
                        }
                        synchronized (CachedAppOptimizer.this) {
                            proc.lastCompactTime = end;
                            proc.lastCompactAction = pendingAction;
                        }
                        if (action.equals(COMPACT_ACTION_FULL)
                                || action.equals(COMPACT_ACTION_ANON)) {
                            // Remove entry and insert again to update insertion order.
                            mLastCompactionStats.remove(pid);
                            mLastCompactionStats.put(pid, new LastCompactionStats(rssAfter));
                        }
                    } catch (Exception e) {
                        // nothing to do, presumably the process died
                    } finally {
                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    }
                    break;
                }
                case COMPACT_SYSTEM_MSG: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "compactSystem");
                    compactSystem();
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                }
            }
        }
    }

Although there is a lot of code, the logic is relatively clear. Please see the comments in the code for the detailed process. There are two main points to note here.

First point, the final compression strategy selection is:

  • pendingAction is SOME, compressed using mCompactActionSome;
  • pendingAction is FULL / PERSISTENT  BFGS, compressed using mCompactActionFull;
  • system is compressed in all mode;

Second point, there are two main ways to process final compression:

  • mProcessDependencies.performCompaction();
  • compactSystem();

4.1 performCompaction()

        public void performCompaction(String action, int pid) throws IOException {
            try (FileOutputStream fos = new FileOutputStream("/proc/" + pid + "/reclaim")) {
                fos.write(action.getBytes());
            }
        }

As you can see from here, the core of compression is the process_reclaim driver, which writes the corresponding action to the /proc/PID/reclaim node, and the driver performs the most important compression processing.

4.2 compactSystem()

This is a native function:

frameworks/base/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp

static void com_android_server_am_CachedAppOptimizer_compactSystem(JNIEnv *, jobject) {
    std::unique_ptr<DIR, decltype(&closedir)> proc(opendir("/proc"), closedir);
    struct dirent* current;
    while ((current = readdir(proc.get()))) {
        if (current->d_type != DT_DIR) {
            continue;
        }

        // don't compact system_server, rely on persistent compaction during screen off
        // in order to avoid mmap_sem-related stalls
        if (atoi(current->d_name) == getpid()) {
            continue;
        }

        std::string status_name = StringPrintf("/proc/%s/status", current->d_name);
        struct stat status_info;

        if (stat(status_name.c_str(), &status_info) != 0) {
            // must be some other directory that isn't a pid
            continue;
        }

        // android.os.Process.FIRST_APPLICATION_UID
        if (status_info.st_uid >= 10000) {
            continue;
        }

        std::string reclaim_path = StringPrintf("/proc/%s/reclaim", current->d_name);
        WriteStringToFile(std::string("all"), reclaim_path);
    }
}

There is a bunch of logic, and in the end, all is written to /proc/PID/reclaim for all nodes, and compressed according to the all method;

5. process_reclaim driver

fs/proc/internal.h

extern const struct file_operations proc_reclaim_operations;
fs/proc/base.c

#ifdef CONFIG_PROCESS_RECLAIM
	REG("reclaim", 0222, proc_reclaim_operations),
#endif

The proc file system adds one more node, and the corresponding operation is proc_reclaim_operations

fs/proc/task_mmu.c

const struct file_operations proc_reclaim_operations = {
	.write		= reclaim_write,
	.llseek		= noop_llseek,
};

write processing function reclaim_write():

fs/proc/task_mmu.c

num reclaim_type {
	RECLAIM_FILE,
	RECLAIM_ANON,
	RECLAIM_ALL,
	RECLAIM_RANGE,
};

static ssize_t reclaim_write(struct file *file, const char __user *buf,
				size_t count, loff_t *ppos)
{
	struct task_struct *task;
	char buffer[200];
	struct mm_struct *mm;
	struct vm_area_struct *vma;
	enum reclaim_type type;
	char *type_buf;
	unsigned long start = 0;
	unsigned long end = 0;
	const struct mm_walk_ops reclaim_walk_ops = {
		.pmd_entry = reclaim_pte_range,
	};

	memset(buffer, 0, sizeof(buffer));
	if (count > sizeof(buffer) - 1)
		count = sizeof(buffer) - 1;

	if (copy_from_user(buffer, buf, count))
		return -EFAULT;

	type_buf = strstrip(buffer);
	if (!strcmp(type_buf, "file"))
		type = RECLAIM_FILE;
	else if (!strcmp(type_buf, "anon"))
		type = RECLAIM_ANON;
	else if (!strcmp(type_buf, "all"))
		type = RECLAIM_ALL;
	else if (isdigit(*type_buf))
		type = RECLAIM_RANGE;
	else
		goto out_err;

	if (type == RECLAIM_RANGE) {
		char *token;
		unsigned long long len, len_in, tmp;

		token = strsep(&type_buf, " ");
		if (!token)
			goto out_err;
		tmp = memparse(token, &token);
		if (tmp & ~PAGE_MASK || tmp > ULONG_MAX)
			goto out_err;
		start = tmp;

		token = strsep(&type_buf, " ");
		if (!token)
			goto out_err;
		len_in = memparse(token, &token);
		len = (len_in + ~PAGE_MASK) & PAGE_MASK;
		if (len > ULONG_MAX)
			goto out_err;
		/*
		 * Check to see whether len was rounded up from small -ve
		 * to zero.
		 */
		if (len_in && !len)
			goto out_err;

		end = start + len;
		if (end < start)
			goto out_err;
	}

	task = get_proc_task(file->f_path.dentry->d_inode);
	if (!task)
		return -ESRCH;

	mm = get_task_mm(task);
	if (!mm)
		goto out;

	down_read(&mm->mmap_sem);
	if (type == RECLAIM_RANGE) {
		vma = find_vma(mm, start);
		while (vma) {
			if (vma->vm_start > end)
				break;
			if (is_vm_hugetlb_page(vma))
				continue;

			walk_page_range(mm, max(vma->vm_start, start),
					min(vma->vm_end, end),
					&reclaim_walk_ops, vma);
			vma = vma->vm_next;
		}
	} else {
        //遍历当前进程所占用的虚拟地址
		for (vma = mm->mmap; vma; vma = vma->vm_next) {
			if (is_vm_hugetlb_page(vma))
				continue;

            // anon 压缩的,只关心匿名页
			if (type == RECLAIM_ANON && vma->vm_file)
				continue;

            //file 压缩的,只关心文件页
			if (type == RECLAIM_FILE && !vma->vm_file)
				continue;

            // 遍历页表,并调用回调函数 reclaim_walk_ops() 处理
			walk_page_range(mm, vma->vm_start, vma->vm_end,
					&reclaim_walk_ops, vma);
		}
	}

	flush_tlb_mm(mm);
	up_read(&mm->mmap_sem);
	mmput(mm);
out:
	put_task_struct(task);
	return count;

out_err:
	return -EINVAL;
}

The logic is relatively simple. The value passed to the node from the upper layer may be:

  • file
  • anon
  • all
  • other

The corresponding reclaim_types in the driver are:

  • RECLAIM_FILE
  • RECLAIM_ANON
  • RECLAIM_ALL
  • RECLAIM_RANGE

Get the task_struct of the process through get_proc_task(), and then get the mm_struct of the process, and then get the vma of the process. By traversing the vma, process the file and anon of the process accordingly, and finally call walk_page_range(). The function parameters:

  • The process mm_struct is specified;
  • start and end of vma;
  • mm_walk_ops 为 reclaim_walk_ops;

walk_page_range() will eventually call back to reclaim_walk_ops.pmd_entry, that is, reclaim_pte_range() :

fs/proc/task_mmu.c

#ifdef CONFIG_PROCESS_RECLAIM
static int reclaim_pte_range(pmd_t *pmd, unsigned long addr,
				unsigned long end, struct mm_walk *walk)
{
	struct vm_area_struct *vma = walk->private;
	pte_t *pte, ptent;
	spinlock_t *ptl;
	struct page *page;
	LIST_HEAD(page_list);
	int isolated;

	split_huge_pmd(vma, addr, pmd);
	if (pmd_trans_unstable(pmd))
		return 0;
cont:
	isolated = 0;
	pte = pte_offset_map_lock(vma->vm_mm, pmd, addr, &ptl);
	for (; addr != end; pte++, addr += PAGE_SIZE) {
		ptent = *pte;
		if (!pte_present(ptent))
			continue;

		page = vm_normal_page(vma, addr, ptent);
		if (!page)
			continue;

		if (isolate_lru_page(compound_head(page)))
			continue;

		/* MADV_FREE clears pte dirty bit and then marks the page
		 * lazyfree (clear SwapBacked). Inbetween if this lazyfreed page
		 * is touched by user then it becomes dirty.  PPR in
		 * shrink_page_list in try_to_unmap finds the page dirty, marks
		 * it back as PageSwapBacked and skips reclaim. This can cause
		 * isolated count mismatch.
		 */
		if (PageAnon(page) && !PageSwapBacked(page)) {
			putback_lru_page(page);
			continue;
		}

		list_add(&page->lru, &page_list);
		inc_node_page_state(page, NR_ISOLATED_ANON +
				page_is_file_lru(page));
		isolated++;
		if (isolated >= SWAP_CLUSTER_MAX)
			break;
	}
	pte_unmap_unlock(pte - 1, ptl);
	reclaim_pages_from_list(&page_list, vma);
	if (addr != end)
		goto cont;

	cond_resched();
	return 0;
}

Finally call the reclaim_pages_from_list() function:

mm/vmscan.c

#ifdef CONFIG_PROCESS_RECLAIM
unsigned long reclaim_pages_from_list(struct list_head *page_list,
			struct vm_area_struct *vma)
{
	struct scan_control sc = {
		.gfp_mask = GFP_KERNEL,
		.priority = DEF_PRIORITY,
		.may_writepage = 1,
		.may_unmap = 1,
		.may_swap = 1,
		.target_vma = vma,
	};

	unsigned long nr_reclaimed;
	struct reclaim_stat stat;
	struct page *page;

	list_for_each_entry(page, page_list, lru)
		ClearPageActive(page);

	nr_reclaimed = shrink_page_list(page_list, NULL, &sc,
			TTU_IGNORE_ACCESS, &stat, true);

	while (!list_empty(page_list)) {
		page = lru_to_page(page_list);
		list_del(&page->lru);
		dec_node_page_state(page, NR_ISOLATED_ANON +
				page_is_file_lru(page));
		putback_lru_page(page);
	}

	return nr_reclaimed;
}
#endif

For detailed shrink_page_list(), please see Section 4 of the article "Shrink_list Detailed Explanation" .

Other process reclaim patches are not listed here. For details, you can click here to download.

 

At this point, all the analysis of app recycling optimization in the Android R version has been completed. Here is a summary:

  • Use CachedAppOptimizer to manage app compaction and trigger the compact interface in the following main ways:
    • When adj changes, if the last adj <= PERCEPTIBLE_APP_ADJ becomes PREVIOUS or HOME, use SOME compression optimization;
    • When adj changes, if it was not CACHED last time, it becomes CACHED, and FULL compression optimization is used;
    • When in the non-awakening state, and when the last adj (setAdj) is more important than the foreground adj ( FOEGROUND_APP_ADJ ), and the interval from the last compression is more than 10 minutes or has never been compressed, use  compactAppPersistent()  compression optimization;
    • When in the non-awakened state, and the application process status is  PROCESS_STATE_BOUND_FOREGROUND_SERVICE , and the interval since the last compression is more than 10 minutes or has never been compressed, use  compactAppBfgs()  compression optimization;
    • When finishBooting() is called in AMS, that is, after the system startup is completed, all processes in the system are fully compressed and compactAllSystem() is called ;
    • MountServiceIdler calls AMS . performIdleMaintenance() and compactAllSystem() every time startJob() performs system compression;
  • CachedAppOptimizer maintains a ServiceThread to process messages sent after compact is called;
  • Non-system compression requires many restrictions before it can officially enter the compression processing function;
  • Compact actions will be merged before compression. SOME uses the mCompactActionSome compression method, and FULL / PERSISTENT / BFGS all use the mCompactActionFull compression method;
  • Compression processing function points:
    • performCompaction(), writes /proc/PID/reclaim node, requires driver support;
    • compactSystem(), this is a native call, and ultimately writes the /proc/PID/reclaim node;
    • Android S began to implement compression processing functions in native;

Regarding the compression optimization of the Andoid S version, there are many changes, please check the next blog post .

Guess you like

Origin blog.csdn.net/jingerppp/article/details/131591931