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

Commit 77a78c6f authored by Daniel Nishi's avatar Daniel Nishi
Browse files

Augment diskstats dumpsys to have categorization and apps.

This adds a new service which opportunistically saves the
file system categorization information and the app sizes. This
information is fetched during a diskstats dumpsys call from a file
stored on the disk. This allows us to keep the dumpsys running quickly
while adding information which is costly to calculate.

Bug: 32207207
Test: System server instrumentation tests
Change-Id: Id59e84b9ad38a9debf3e46e5133ef06f7353829d
parent 3c7febdb
Loading
Loading
Loading
Loading
+7 −4
Original line number Diff line number Diff line
@@ -3436,12 +3436,15 @@
                 android:permission="android.permission.BIND_JOB_SERVICE" >
        </service>

        <service
            android:name="com.android.server.pm.BackgroundDexOptService"
        <service android:name="com.android.server.pm.BackgroundDexOptService"
                 android:exported="true"
                 android:permission="android.permission.BIND_JOB_SERVICE">
        </service>

        <service android:name="com.android.server.storage.DiskStatsLoggingService"
                 android:permission="android.permission.BIND_JOB_SERVICE" >
        </service>

    </application>

</manifest>
+56 −0
Original line number Diff line number Diff line
@@ -22,6 +22,15 @@ import android.os.Environment;
import android.os.StatFs;
import android.os.SystemClock;
import android.os.storage.StorageManager;
import android.util.Log;

import com.android.server.storage.DiskStatsFileLogger;
import com.android.server.storage.DiskStatsLoggingService;

import libcore.io.IoUtils;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileDescriptor;
@@ -35,11 +44,13 @@ import java.io.PrintWriter;
 */
public class DiskStatsService extends Binder {
    private static final String TAG = "DiskStatsService";
    private static final String DISKSTATS_DUMP_FILE = "/data/system/diskstats_cache.json";

    private final Context mContext;

    public DiskStatsService(Context context) {
        mContext = context;
        DiskStatsLoggingService.schedule(context);
    }

    @Override
@@ -84,6 +95,10 @@ public class DiskStatsService extends Binder {
            pw.println("File-based Encryption: true");
        }

        if (isCheckin(args)) {
            reportCachedValues(pw);
        }

        // TODO: Read /proc/yaffs and report interesting values;
        // add configurable (through args) performance test parameters.
    }
@@ -114,4 +129,45 @@ public class DiskStatsService extends Binder {
            return;
        }
    }

    private boolean isCheckin(String[] args) {
        for (String opt : args) {
            if ("--checkin".equals(opt)) {
                return true;
            }
        }
        return false;
    }

    private void reportCachedValues(PrintWriter pw) {
        try {
            String jsonString = IoUtils.readFileAsString(DISKSTATS_DUMP_FILE);
            JSONObject json = new JSONObject(jsonString);
            pw.print("App Size: ");
            pw.println(json.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY));
            pw.print("App Cache Size: ");
            pw.println(json.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY));
            pw.print("Photos Size: ");
            pw.println(json.getLong(DiskStatsFileLogger.PHOTOS_KEY));
            pw.print("Videos Size: ");
            pw.println(json.getLong(DiskStatsFileLogger.VIDEOS_KEY));
            pw.print("Audio Size: ");
            pw.println(json.getLong(DiskStatsFileLogger.AUDIO_KEY));
            pw.print("Downloads Size: ");
            pw.println(json.getLong(DiskStatsFileLogger.DOWNLOADS_KEY));
            pw.print("System Size: ");
            pw.println(json.getLong(DiskStatsFileLogger.SYSTEM_KEY));
            pw.print("Other Size: ");
            pw.println(json.getLong(DiskStatsFileLogger.MISC_KEY));
            pw.print("Package Names: ");
            pw.println(json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY));
            pw.print("App Sizes: ");
            pw.println(json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY));
            pw.print("Cache Sizes: ");
            pw.println(json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY));
        } catch (IOException | JSONException e) {
            Log.w(TAG, "exception reading diskstats cache file", e);
        }
    }

}
+166 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE2.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.storage;

import android.content.pm.PackageStats;
import android.os.Environment;
import android.util.ArrayMap;
import android.util.Log;

import com.android.server.storage.FileCollector.MeasurementResult;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;

/**
 * DiskStatsFileLogger logs collected storage information to a file in a JSON format.
 *
 * The following information is cached in the file:
 * 1. Size of images on disk.
 * 2. Size of videos on disk.
 * 3. Size of audio on disk.
 * 4. Size of the downloads folder.
 * 5. System size.
 * 6. Aggregate and individual app and app cache sizes.
 * 7. How much storage couldn't be categorized in one of the above categories.
 */
public class DiskStatsFileLogger {
    private static final String TAG = "DiskStatsLogger";

    public static final String PHOTOS_KEY = "photosSize";
    public static final String VIDEOS_KEY = "videosSize";
    public static final String AUDIO_KEY = "audioSize";
    public static final String DOWNLOADS_KEY = "downloadsSize";
    public static final String SYSTEM_KEY = "systemSize";
    public static final String MISC_KEY = "otherSize";
    public static final String APP_SIZE_AGG_KEY = "appSize";
    public static final String APP_CACHE_AGG_KEY = "cacheSize";
    public static final String PACKAGE_NAMES_KEY = "packageNames";
    public static final String APP_SIZES_KEY = "appSizes";
    public static final String APP_CACHES_KEY = "cacheSizes";
    public static final String LAST_QUERY_TIMESTAMP_KEY = "queryTime";

    private MeasurementResult mResult;
    private long mDownloadsSize;
    private long mSystemSize;
    private List<PackageStats> mPackageStats;

    /**
     * Constructs a DiskStatsFileLogger with calculated measurement results.
     */
    public DiskStatsFileLogger(MeasurementResult result, MeasurementResult downloadsResult,
            List<PackageStats> stats, long systemSize) {
        mResult = result;
        mDownloadsSize = downloadsResult.totalAccountedSize();
        mSystemSize = systemSize;
        mPackageStats = stats;
    }

    /**
     * Dumps the storage collection output to a file.
     * @param file File to write the output into.
     * @throws FileNotFoundException
     */
    public void dumpToFile(File file) throws FileNotFoundException {
        PrintWriter pw = new PrintWriter(file);
        JSONObject representation = getJsonRepresentation();
        if (representation != null) {
            pw.println(representation);
        }
        pw.close();
    }

    private JSONObject getJsonRepresentation() {
        JSONObject json = new JSONObject();
        try {
            json.put(LAST_QUERY_TIMESTAMP_KEY, System.currentTimeMillis());
            json.put(PHOTOS_KEY, mResult.imagesSize);
            json.put(VIDEOS_KEY, mResult.videosSize);
            json.put(AUDIO_KEY, mResult.audioSize);
            json.put(DOWNLOADS_KEY, mDownloadsSize);
            json.put(SYSTEM_KEY, mSystemSize);
            json.put(MISC_KEY, mResult.miscSize);
            addAppsToJson(json);
        } catch (JSONException e) {
            Log.e(TAG, e.toString());
            return null;
        }

        return json;
    }

    private void addAppsToJson(JSONObject json) throws JSONException {
        JSONArray names = new JSONArray();
        JSONArray appSizeList = new JSONArray();
        JSONArray cacheSizeList = new JSONArray();

        long appSizeSum = 0L;
        long cacheSizeSum = 0L;
        boolean isExternal = Environment.isExternalStorageEmulated();
        for (Map.Entry<String, PackageStats> entry : mergePackagesAcrossUsers().entrySet()) {
            PackageStats stat = entry.getValue();
            long appSize = stat.codeSize + stat.dataSize;
            long cacheSize = stat.cacheSize;
            if (isExternal) {
                appSize += stat.externalCodeSize + stat.externalDataSize;
                cacheSize += stat.externalCacheSize;
            }
            appSizeSum += appSize;
            cacheSizeSum += cacheSize;

            names.put(stat.packageName);
            appSizeList.put(appSize);
            cacheSizeList.put(cacheSize);
        }
        json.put(PACKAGE_NAMES_KEY, names);
        json.put(APP_SIZES_KEY, appSizeList);
        json.put(APP_CACHES_KEY, cacheSizeList);
        json.put(APP_SIZE_AGG_KEY, appSizeSum);
        json.put(APP_CACHE_AGG_KEY, cacheSizeSum);
    }

    /**
     * A given package may exist for multiple users with distinct sizes. This function merges
     * the duplicated packages together and sums up their sizes to get the actual totals for the
     * package.
     * @return A mapping of package name to merged package stats.
     */
    private ArrayMap<String, PackageStats> mergePackagesAcrossUsers() {
        ArrayMap<String, PackageStats> packageMap = new ArrayMap<>();
        for (PackageStats stat : mPackageStats) {
            PackageStats existingStats = packageMap.get(stat.packageName);
            if (existingStats != null) {
                existingStats.cacheSize += stat.cacheSize;
                existingStats.codeSize += stat.codeSize;
                existingStats.dataSize += stat.dataSize;
                existingStats.externalCacheSize += stat.externalCacheSize;
                existingStats.externalCodeSize += stat.externalCodeSize;
                existingStats.externalDataSize += stat.externalDataSize;
            } else {
                packageMap.put(stat.packageName, new PackageStats(stat));
            }
        }
        return packageMap;
    }
}
 No newline at end of file
+170 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE2.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.storage;

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.content.pm.PackageStats;
import android.os.AsyncTask;
import android.os.BatteryManager;
import android.os.Environment;
import android.os.Environment.UserEnvironment;
import android.os.UserHandle;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.storage.FileCollector.MeasurementResult;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * DiskStatsLoggingService is a JobService which collects storage categorization information and
 * app size information on a roughly daily cadence.
 */
public class DiskStatsLoggingService extends JobService {
    private static final String TAG = "DiskStatsLogService";
    public static final String DUMPSYS_CACHE_PATH = "/data/system/diskstats_cache.json";
    private static final int JOB_DISKSTATS_LOGGING = 0x4449534b; // DISK
    private static ComponentName sDiskStatsLoggingService = new ComponentName(
            "android",
            DiskStatsLoggingService.class.getName());

    @Override
    public boolean onStartJob(JobParameters params) {
        // We need to check the preconditions again because they may not be enforced for
        // subsequent runs.
        if (!isCharging(this)) {
            jobFinished(params, true);
            return false;
        }

        final int userId = UserHandle.myUserId();
        UserEnvironment environment = new UserEnvironment(userId);
        AppCollector collector = new AppCollector(this,
                getPackageManager().getPrimaryStorageCurrentVolume());
        LogRunnable task = new LogRunnable();
        task.setRootDirectory(environment.getExternalStorageDirectory());
        task.setDownloadsDirectory(
                environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
        task.setSystemSize(FileCollector.getSystemSize(this));
        task.setLogOutputFile(new File(DUMPSYS_CACHE_PATH));
        task.setAppCollector(collector);
        task.setJobService(this, params);
        AsyncTask.execute(task);
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        // TODO: Try to stop being handled.
        return false;
    }

    /**
     * Schedules a DiskStats collection task. This task only runs on device idle while charging
     * once every 24 hours.
     * @param context Context to use to get a job scheduler.
     */
    public static void schedule(Context context) {
        JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);

        js.schedule(new JobInfo.Builder(JOB_DISKSTATS_LOGGING, sDiskStatsLoggingService)
                .setRequiresDeviceIdle(true)
                .setRequiresCharging(true)
                .setPeriodic(TimeUnit.DAYS.toMillis(1))
                .build());
    }

    private static boolean isCharging(Context context) {
        BatteryManager batteryManager = context.getSystemService(BatteryManager.class);
        if (batteryManager != null) {
            return batteryManager.isCharging();
        }
        return false;
    }

    @VisibleForTesting
    static class LogRunnable implements Runnable {
        private static final long TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);

        private JobService mJobService;
        private JobParameters mParams;
        private AppCollector mCollector;
        private File mOutputFile;
        private File mRootDirectory;
        private File mDownloadsDirectory;
        private long mSystemSize;

        public void setRootDirectory(File file) {
            mRootDirectory = file;
        }

        public void setDownloadsDirectory(File file) {
            mDownloadsDirectory = file;
        }

        public void setAppCollector(AppCollector collector) {
            mCollector = collector;
        }

        public void setLogOutputFile(File file) {
            mOutputFile = file;
        }

        public void setSystemSize(long size) {
            mSystemSize = size;
        }

        public void setJobService(JobService jobService, JobParameters params) {
            mJobService = jobService;
            mParams = params;
        }

        public void run() {
            FileCollector.MeasurementResult mainCategories =
                    FileCollector.getMeasurementResult(mRootDirectory);
            FileCollector.MeasurementResult downloads =
                    FileCollector.getMeasurementResult(mDownloadsDirectory);

            logToFile(mainCategories, downloads, mCollector.getPackageStats(TIMEOUT_MILLIS),
                    mSystemSize);

            if (mJobService != null) {
                mJobService.jobFinished(mParams, false);
            }
        }

        private void logToFile(MeasurementResult mainCategories, MeasurementResult downloads,
                List<PackageStats> stats, long systemSize) {
            DiskStatsFileLogger logger = new DiskStatsFileLogger(mainCategories, downloads, stats,
                    systemSize);
            try {
                mOutputFile.createNewFile();
                logger.dumpToFile(mOutputFile);
            } catch (IOException e) {
                Log.e(TAG, "Exception while writing opportunistic disk file cache.", e);
            }
        }
    }
}
 No newline at end of file
+29 −0
Original line number Diff line number Diff line
@@ -17,7 +17,10 @@
package com.android.server.storage;

import android.annotation.IntDef;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.util.ArrayMap;

import java.io.File;
@@ -150,6 +153,32 @@ public class FileCollector {
                new MeasurementResult());
    }

    /**
     * Returns the size of a system for a given context. This is done by finding the difference
     * between the shared data and the total primary storage size.
     * @param context Context to use to get storage information.
     */
    public static long getSystemSize(Context context) {
        PackageManager pm = context.getPackageManager();
        VolumeInfo primaryVolume = pm.getPrimaryStorageCurrentVolume();

        StorageManager sm = context.getSystemService(StorageManager.class);
        VolumeInfo shared = sm.findEmulatedForPrivate(primaryVolume);
        if (shared == null) {
            return 0;
        }

        final long sharedDataSize = shared.getPath().getTotalSpace();
        long systemSize = sm.getPrimaryStorageSize() - sharedDataSize;

        // This case is not exceptional -- we just fallback to the shared data volume in this case.
        if (systemSize <= 0) {
            return 0;
        }

        return systemSize;
    }

    private static MeasurementResult collectFiles(File file, MeasurementResult result) {
        File[] files = file.listFiles();

Loading