From 8fb49c78633aa7b01ec6e32e2431f83eb599cdff Mon Sep 17 00:00:00 2001 From: Daeho Jeong Date: Tue, 4 Jan 2022 11:49:08 -0800 Subject: [PATCH] Add smart idle maintenance service Added smart idle maintenance service in StorageManagerService, whose goal is to determine when to trigger filesystem defragmentation while keeping the best user experience as long as possible, and avoiding hurting UFS lifetime. Test: check smart idle maintenance log every hour Bug: 202283480 Bug: 181079477 Signed-off-by: Daeho Jeong Change-Id: Ie7331aec45d62a59b28ca2806e34d6d45b6ffaf4 --- core/res/AndroidManifest.xml | 5 + .../server/SmartStorageMaintIdler.java | 89 +++++++ .../android/server/StorageManagerService.java | 220 +++++++++++++++++- 3 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 services/core/java/com/android/server/SmartStorageMaintIdler.java diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 6ecfcec0e804..8d2d603997b7 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6234,6 +6234,11 @@ android:permission="android.permission.BIND_JOB_SERVICE" > + + + diff --git a/services/core/java/com/android/server/SmartStorageMaintIdler.java b/services/core/java/com/android/server/SmartStorageMaintIdler.java new file mode 100644 index 000000000000..2dff72fdc344 --- /dev/null +++ b/services/core/java/com/android/server/SmartStorageMaintIdler.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2014 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; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.util.Slog; + +import java.util.concurrent.TimeUnit; + +public class SmartStorageMaintIdler extends JobService { + private static final String TAG = "SmartStorageMaintIdler"; + + private static final ComponentName SMART_STORAGE_MAINT_SERVICE = + new ComponentName("android", SmartStorageMaintIdler.class.getName()); + + private static final int SMART_MAINT_JOB_ID = 2808; + + private boolean mStarted; + private JobParameters mJobParams; + private final Runnable mFinishCallback = new Runnable() { + @Override + public void run() { + Slog.i(TAG, "Got smart storage maintenance service completion callback"); + if (mStarted) { + jobFinished(mJobParams, false); + mStarted = false; + } + // ... and try again in a next period + scheduleSmartIdlePass(SmartStorageMaintIdler.this, + StorageManagerService.SMART_IDLE_MAINT_PERIOD); + } + }; + + @Override + public boolean onStartJob(JobParameters params) { + mJobParams = params; + StorageManagerService ms = StorageManagerService.sSelf; + if (ms != null) { + mStarted = true; + ms.runSmartIdleMaint(mFinishCallback); + } + return ms != null; + } + + @Override + public boolean onStopJob(JobParameters params) { + mStarted = false; + return false; + } + + /** + * Schedule the smart storage idle maintenance job + */ + public static void scheduleSmartIdlePass(Context context, int nHours) { + StorageManagerService ms = StorageManagerService.sSelf; + if ((ms == null) || ms.isPassedLifetimeThresh()) { + return; + } + + JobScheduler tm = context.getSystemService(JobScheduler.class); + + long nextScheduleTime = TimeUnit.HOURS.toMillis(nHours); + + JobInfo.Builder builder = new JobInfo.Builder(SMART_MAINT_JOB_ID, + SMART_STORAGE_MAINT_SERVICE); + + builder.setMinimumLatency(nextScheduleTime); + tm.schedule(builder.build()); + } +} diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 2d6170b4cbea..53c8635c4e0a 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -79,6 +79,7 @@ import android.content.res.Configuration; import android.content.res.ObbInfo; import android.database.ContentObserver; import android.net.Uri; +import android.os.BatteryManager; import android.os.Binder; import android.os.DropBoxManager; import android.os.Environment; @@ -158,6 +159,8 @@ import com.android.server.storage.StorageSessionController.ExternalStorageServic import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal.ScreenObserver; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import libcore.io.IoUtils; import libcore.util.EmptyArray; @@ -339,7 +342,44 @@ class StorageManagerService extends IStorageManager.Stub @Nullable public static String sMediaStoreAuthorityProcessName; + // Run period in hour for smart idle maintenance + static final int SMART_IDLE_MAINT_PERIOD = 1; + private final AtomicFile mSettingsFile; + private final AtomicFile mHourlyWriteFile; + + private static final int MAX_HOURLY_WRITE_RECORDS = 72; + + /** + * Default config values for smart idle maintenance + * Actual values will be controlled by DeviceConfig + */ + // Decide whether smart idle maintenance is enabled or not + private static final boolean DEFAULT_SMART_IDLE_MAINT_ENABLED = false; + // Storage lifetime percentage threshold to decide to turn off the feature + private static final int DEFAULT_LIFETIME_PERCENT_THRESHOLD = 70; + // Minimum required number of dirty + free segments to trigger GC + private static final int DEFAULT_MIN_SEGMENTS_THRESHOLD = 512; + // Determine how much portion of current dirty segments will be GCed + private static final float DEFAULT_DIRTY_RECLAIM_RATE = 0.5F; + // Multiplier to amplify the target segment number for GC + private static final float DEFAULT_SEGMENT_RECLAIM_WEIGHT = 1.0F; + // Low battery level threshold to decide to turn off the feature + private static final float DEFAULT_LOW_BATTERY_LEVEL = 20F; + // Decide whether charging is required to turn on the feature + private static final boolean DEFAULT_CHARGING_REQUIRED = true; + + private volatile int mLifetimePercentThreshold; + private volatile int mMinSegmentsThreshold; + private volatile float mDirtyReclaimRate; + private volatile float mSegmentReclaimWeight; + private volatile float mLowBatteryLevel; + private volatile boolean mChargingRequired; + private volatile boolean mNeedGC; + + private volatile boolean mPassedLifetimeThresh; + // Tracking storage hourly write amounts + private volatile int[] mStorageHourlyWrites; /** * Never hold the lock while performing downcalls into vold, since @@ -901,6 +941,10 @@ class StorageManagerService extends IStorageManager.Stub } private void handleSystemReady() { + if (prepareSmartIdleMaint()) { + SmartStorageMaintIdler.scheduleSmartIdlePass(mContext, SMART_IDLE_MAINT_PERIOD); + } + // Start scheduling nominally-daily fstrim operations MountServiceIdler.scheduleIdlePass(mContext); @@ -1907,6 +1951,10 @@ class StorageManagerService extends IStorageManager.Stub mSettingsFile = new AtomicFile( new File(Environment.getDataSystemDirectory(), "storage.xml"), "storage-settings"); + mHourlyWriteFile = new AtomicFile( + new File(Environment.getDataSystemDirectory(), "storage-hourly-writes")); + + mStorageHourlyWrites = new int[MAX_HOURLY_WRITE_RECORDS]; synchronized (mLock) { readSettingsLocked(); @@ -2572,7 +2620,7 @@ class StorageManagerService extends IStorageManager.Stub // fstrim time is still updated. If file based checkpoints are used, we run // idle maintenance (GC + fstrim) regardless of checkpoint status. if (!needsCheckpoint() || !supportsBlockCheckpoint()) { - mVold.runIdleMaint(new IVoldTaskListener.Stub() { + mVold.runIdleMaint(mNeedGC, new IVoldTaskListener.Stub() { @Override public void onStatus(int status, PersistableBundle extras) { // Not currently used @@ -2623,6 +2671,176 @@ class StorageManagerService extends IStorageManager.Stub abortIdleMaint(null); } + private boolean prepareSmartIdleMaint() { + /** + * We can choose whether going with a new storage smart idle maintenance job + * or falling back to the traditional way using DeviceConfig + */ + boolean smartIdleMaintEnabled = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, + "smart_idle_maint_enabled", + DEFAULT_SMART_IDLE_MAINT_ENABLED); + if (smartIdleMaintEnabled) { + mLifetimePercentThreshold = DeviceConfig.getInt( + DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, + "lifetime_threshold", DEFAULT_LIFETIME_PERCENT_THRESHOLD); + mMinSegmentsThreshold = DeviceConfig.getInt(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, + "min_segments_threshold", DEFAULT_MIN_SEGMENTS_THRESHOLD); + mDirtyReclaimRate = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, + "dirty_reclaim_rate", DEFAULT_DIRTY_RECLAIM_RATE); + mSegmentReclaimWeight = DeviceConfig.getFloat( + DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, + "segment_reclaim_weight", DEFAULT_SEGMENT_RECLAIM_WEIGHT); + mLowBatteryLevel = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, + "low_battery_level", DEFAULT_LOW_BATTERY_LEVEL); + mChargingRequired = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, + "charging_required", DEFAULT_CHARGING_REQUIRED); + + // If we use the smart idle maintenance, we need to turn off GC in the traditional idle + // maintenance to avoid the conflict + mNeedGC = false; + + loadStorageHourlyWrites(); + try { + mVold.refreshLatestWrite(); + } catch (Exception e) { + Slog.wtf(TAG, e); + } + refreshLifetimeConstraint(); + } + return smartIdleMaintEnabled; + } + + // Return whether storage lifetime exceeds the threshold + public boolean isPassedLifetimeThresh() { + return mPassedLifetimeThresh; + } + + private void loadStorageHourlyWrites() { + FileInputStream fis = null; + + try { + fis = mHourlyWriteFile.openRead(); + ObjectInputStream ois = new ObjectInputStream(fis); + mStorageHourlyWrites = (int[])ois.readObject(); + } catch (FileNotFoundException e) { + // Missing data is okay, probably first boot + } catch (Exception e) { + Slog.wtf(TAG, "Failed reading write records", e); + } finally { + IoUtils.closeQuietly(fis); + } + } + + private int getAverageHourlyWrite() { + return Arrays.stream(mStorageHourlyWrites).sum() / MAX_HOURLY_WRITE_RECORDS; + } + + private void updateStorageHourlyWrites(int latestWrite) { + FileOutputStream fos = null; + + System.arraycopy(mStorageHourlyWrites,0, mStorageHourlyWrites, 1, + MAX_HOURLY_WRITE_RECORDS - 1); + mStorageHourlyWrites[0] = latestWrite; + try { + fos = mHourlyWriteFile.startWrite(); + ObjectOutputStream oos = new ObjectOutputStream(fos); + oos.writeObject(mStorageHourlyWrites); + mHourlyWriteFile.finishWrite(fos); + } catch (IOException e) { + if (fos != null) { + mHourlyWriteFile.failWrite(fos); + } + } + } + + private boolean checkChargeStatus() { + IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + Intent batteryStatus = mContext.registerReceiver(null, ifilter); + + if (mChargingRequired) { + int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + if (status != BatteryManager.BATTERY_STATUS_CHARGING && + status != BatteryManager.BATTERY_STATUS_FULL) { + Slog.w(TAG, "Battery is not being charged"); + return false; + } + } + + int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + float chargePercent = level * 100f / (float)scale; + + if (chargePercent < mLowBatteryLevel) { + Slog.w(TAG, "Battery level is " + chargePercent + ", which is lower than threshold: " + + mLowBatteryLevel); + return false; + } + return true; + } + + private boolean refreshLifetimeConstraint() { + int storageLifeTime = 0; + + try { + storageLifeTime = mVold.getStorageLifeTime(); + } catch (Exception e) { + Slog.wtf(TAG, e); + return false; + } + + if (storageLifeTime == -1) { + Slog.w(TAG, "Failed to get storage lifetime"); + return false; + } else if (storageLifeTime > mLifetimePercentThreshold) { + Slog.w(TAG, "Ended smart idle maintenance, because of lifetime(" + storageLifeTime + + ")" + ", lifetime threshold(" + mLifetimePercentThreshold + ")"); + mPassedLifetimeThresh = true; + return false; + } + Slog.i(TAG, "Storage lifetime: " + storageLifeTime); + return true; + } + + void runSmartIdleMaint(Runnable callback) { + enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS); + + try { + // Block based checkpoint process runs fstrim. So, if checkpoint is in progress + // (first boot after OTA), We skip the smart idle maintenance + if (!needsCheckpoint() || !supportsBlockCheckpoint()) { + if (!refreshLifetimeConstraint() || !checkChargeStatus()) { + return; + } + + int latestHourlyWrite = mVold.getWriteAmount(); + if (latestHourlyWrite == -1) { + Slog.w(TAG, "Failed to get storage hourly write"); + return; + } + + updateStorageHourlyWrites(latestHourlyWrite); + int avgHourlyWrite = getAverageHourlyWrite(); + + Slog.i(TAG, "Set smart idle maintenance: " + "latest hourly write: " + + latestHourlyWrite + ", average hourly write: " + avgHourlyWrite + + ", min segment threshold: " + mMinSegmentsThreshold + + ", dirty reclaim rate: " + mDirtyReclaimRate + + ", segment reclaim weight:" + mSegmentReclaimWeight); + mVold.setGCUrgentPace(avgHourlyWrite, mMinSegmentsThreshold, mDirtyReclaimRate, + mSegmentReclaimWeight); + } else { + Slog.i(TAG, "Skipping smart idle maintenance - block based checkpoint in progress"); + } + } catch (Exception e) { + Slog.wtf(TAG, e); + } finally { + if (callback != null) { + callback.run(); + } + } + } + @Override public void setDebugFlags(int flags, int mask) { enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); -- GitLab