diff options
| author | 2016-12-13 10:38:42 -0800 | |
|---|---|---|
| committer | 2016-12-20 13:32:51 -0800 | |
| commit | 090b2d9d6c73ad1b92fd6374aaaa26a384333239 (patch) | |
| tree | 7b1b6dded0f58bf7f291ab5eb27602a09d36c2be | |
| parent | be770dce086413618d05917ebb967e0414346dce (diff) | |
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
7 files changed, 809 insertions, 4 deletions
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 632bcfe1f9fe..dc09e64dce91 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3390,10 +3390,13 @@ android:permission="android.permission.BIND_JOB_SERVICE" > </service> - <service - android:name="com.android.server.pm.BackgroundDexOptService" - android:exported="true" - android:permission="android.permission.BIND_JOB_SERVICE"> + <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> diff --git a/services/core/java/com/android/server/DiskStatsService.java b/services/core/java/com/android/server/DiskStatsService.java index 8ca675a904c4..dd95f6718bea 100644 --- a/services/core/java/com/android/server/DiskStatsService.java +++ b/services/core/java/com/android/server/DiskStatsService.java @@ -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); + } + } + } diff --git a/services/core/java/com/android/server/storage/DiskStatsFileLogger.java b/services/core/java/com/android/server/storage/DiskStatsFileLogger.java new file mode 100644 index 000000000000..22299df93ef5 --- /dev/null +++ b/services/core/java/com/android/server/storage/DiskStatsFileLogger.java @@ -0,0 +1,166 @@ +/* + * 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 diff --git a/services/core/java/com/android/server/storage/DiskStatsLoggingService.java b/services/core/java/com/android/server/storage/DiskStatsLoggingService.java new file mode 100644 index 000000000000..4a86175be2df --- /dev/null +++ b/services/core/java/com/android/server/storage/DiskStatsLoggingService.java @@ -0,0 +1,170 @@ +/* + * 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 diff --git a/services/core/java/com/android/server/storage/FileCollector.java b/services/core/java/com/android/server/storage/FileCollector.java index b96eb694897b..90f9f1391679 100644 --- a/services/core/java/com/android/server/storage/FileCollector.java +++ b/services/core/java/com/android/server/storage/FileCollector.java @@ -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(); diff --git a/services/tests/servicestests/src/com/android/server/storage/DiskStatsFileLoggerTest.java b/services/tests/servicestests/src/com/android/server/storage/DiskStatsFileLoggerTest.java new file mode 100644 index 000000000000..2aca702b809c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/storage/DiskStatsFileLoggerTest.java @@ -0,0 +1,239 @@ +/* + * 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/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.storage; + +import android.content.pm.PackageStats; +import android.test.AndroidTestCase; +import android.util.ArraySet; +import libcore.io.IoUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.util.ArrayList; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(JUnit4.class) +public class DiskStatsFileLoggerTest extends AndroidTestCase { + @Rule public TemporaryFolder temporaryFolder; + public FileCollector.MeasurementResult mMainResult; + public FileCollector.MeasurementResult mDownloadsResult; + private ArrayList<PackageStats> mPackages; + private File mOutputFile; + + @Before + public void setUp() throws Exception { + super.setUp(); + temporaryFolder = new TemporaryFolder(); + temporaryFolder.create(); + mOutputFile = temporaryFolder.newFile(); + mMainResult = new FileCollector.MeasurementResult(); + mDownloadsResult = new FileCollector.MeasurementResult(); + mPackages = new ArrayList<>(); + } + + @Test + public void testEmptyStorage() throws Exception { + DiskStatsFileLogger logger = new DiskStatsFileLogger( + mMainResult, mDownloadsResult,mPackages, 0L); + + logger.dumpToFile(mOutputFile); + + JSONObject output = getOutputFileAsJson(); + assertThat(output.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(0L); + assertThat(output.getLong(DiskStatsFileLogger.VIDEOS_KEY)).isEqualTo(0L); + assertThat(output.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(0L); + assertThat(output.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(0L); + assertThat(output.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(0L); + assertThat(output.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(0L); + assertThat(output.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(0L); + assertThat(output.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(0L); + assertThat( + output.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY).length()).isEqualTo(0L); + assertThat(output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY).length()).isEqualTo(0L); + assertThat(output.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY).length()).isEqualTo(0L); + } + + @Test + public void testMeasurementResultsReported() throws Exception { + mMainResult.audioSize = 1; + mMainResult.imagesSize = 10; + mMainResult.miscSize = 100; + mDownloadsResult.miscSize = 1000; + DiskStatsFileLogger logger = new DiskStatsFileLogger( + mMainResult, mDownloadsResult,mPackages, 3L); + + logger.dumpToFile(mOutputFile); + + JSONObject output = getOutputFileAsJson(); + assertThat(output.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(1L); + assertThat(output.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(10L); + assertThat(output.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(100L); + assertThat(output.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(1000L); + assertThat(output.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(3L); + } + + @Test + public void testAppsReported() throws Exception { + PackageStats firstPackage = new PackageStats("com.test.app"); + firstPackage.codeSize = 100; + firstPackage.dataSize = 1000; + firstPackage.cacheSize = 20; + mPackages.add(firstPackage); + + PackageStats secondPackage = new PackageStats("com.test.app2"); + secondPackage.codeSize = 10; + secondPackage.dataSize = 1; + secondPackage.cacheSize = 2; + mPackages.add(secondPackage); + + DiskStatsFileLogger logger = new DiskStatsFileLogger( + mMainResult, mDownloadsResult, mPackages, 0L); + logger.dumpToFile(mOutputFile); + + JSONObject output = getOutputFileAsJson(); + assertThat(output.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(1111); + assertThat(output.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(22); + + JSONArray packageNames = output.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY); + assertThat(packageNames.length()).isEqualTo(2); + JSONArray appSizes = output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY); + assertThat(appSizes.length()).isEqualTo(2); + JSONArray cacheSizes = output.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY); + assertThat(cacheSizes.length()).isEqualTo(2); + + // We need to do this crazy Set over this because the DiskStatsFileLogger provides no + // guarantee of the ordering of the apps in its output. By using a set, we avoid any order + // problems. + ArraySet<AppSizeGrouping> apps = new ArraySet<>(); + for (int i = 0; i < packageNames.length(); i++) { + AppSizeGrouping app = new AppSizeGrouping(packageNames.getString(i), + appSizes.getLong(i), cacheSizes.getLong(i)); + apps.add(app); + } + assertThat(apps).containsAllOf(new AppSizeGrouping("com.test.app", 1100, 20), + new AppSizeGrouping("com.test.app2", 11, 2)); + } + + @Test + public void testEmulatedExternalStorageCounted() throws Exception { + PackageStats app = new PackageStats("com.test.app"); + app.dataSize = 1000; + app.externalDataSize = 1000; + app.cacheSize = 20; + mPackages.add(app); + + DiskStatsFileLogger logger = new DiskStatsFileLogger( + mMainResult, mDownloadsResult, mPackages, 0L); + logger.dumpToFile(mOutputFile); + + JSONObject output = getOutputFileAsJson(); + JSONArray appSizes = output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY); + assertThat(appSizes.length()).isEqualTo(1); + assertThat(appSizes.getLong(0)).isEqualTo(2000); + } + + @Test + public void testDuplicatePackageNameIsMergedAcrossMultipleUsers() throws Exception { + PackageStats app = new PackageStats("com.test.app"); + app.dataSize = 1000; + app.externalDataSize = 1000; + app.cacheSize = 20; + app.userHandle = 0; + mPackages.add(app); + + PackageStats secondApp = new PackageStats("com.test.app"); + secondApp.dataSize = 100; + secondApp.externalDataSize = 100; + secondApp.cacheSize = 2; + secondApp.userHandle = 1; + mPackages.add(secondApp); + + DiskStatsFileLogger logger = new DiskStatsFileLogger( + mMainResult, mDownloadsResult, mPackages, 0L); + logger.dumpToFile(mOutputFile); + + JSONObject output = getOutputFileAsJson(); + assertThat(output.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(2200); + assertThat(output.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(22); + JSONArray packageNames = output.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY); + assertThat(packageNames.length()).isEqualTo(1); + assertThat(packageNames.getString(0)).isEqualTo("com.test.app"); + + JSONArray appSizes = output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY); + assertThat(appSizes.length()).isEqualTo(1); + assertThat(appSizes.getLong(0)).isEqualTo(2200); + + JSONArray cacheSizes = output.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY); + assertThat(cacheSizes.length()).isEqualTo(1); + assertThat(cacheSizes.getLong(0)).isEqualTo(22); + } + + private JSONObject getOutputFileAsJson() throws Exception { + return new JSONObject(IoUtils.readFileAsString(mOutputFile.getAbsolutePath())); + } + + /** + * This class exists for putting zipped app size information arrays into a set for comparison + * purposes. + */ + private class AppSizeGrouping { + public String packageName; + public long appSize; + public long cacheSize; + + public AppSizeGrouping(String packageName, long appSize, long cacheSize) { + this.packageName = packageName; + this.appSize = appSize; + this.cacheSize = cacheSize; + } + + @Override + public int hashCode() { + int result = 17; + result = 37 * result + (int)(appSize ^ (appSize >>> 32)); + result = 37 * result + (int)(cacheSize ^ (cacheSize >>> 32)); + result = 37 * result + packageName.hashCode(); + return result; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AppSizeGrouping)) { + return false; + } + if (this == o) { + return true; + } + AppSizeGrouping grouping = (AppSizeGrouping) o; + return packageName.equals(grouping.packageName) && appSize == grouping.appSize && + cacheSize == grouping.cacheSize; + } + + @Override + public String toString() { + return packageName + " " + appSize + " " + cacheSize; + } + } +}
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/storage/DiskStatsLoggingServiceTest.java b/services/tests/servicestests/src/com/android/server/storage/DiskStatsLoggingServiceTest.java new file mode 100644 index 000000000000..357ce744c1e2 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/storage/DiskStatsLoggingServiceTest.java @@ -0,0 +1,142 @@ +/* + * 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/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.storage; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.when; + +import android.content.pm.PackageStats; +import android.test.AndroidTestCase; + +import com.android.server.storage.DiskStatsLoggingService.LogRunnable; + +import libcore.io.IoUtils; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.io.PrintStream; +import java.util.ArrayList; + +@RunWith(JUnit4.class) +public class DiskStatsLoggingServiceTest extends AndroidTestCase { + @Rule public TemporaryFolder mTemporaryFolder; + @Rule public TemporaryFolder mDownloads; + @Rule public TemporaryFolder mRootDirectory; + @Mock private AppCollector mCollector; + private File mInputFile; + + + @Before + public void setUp() throws Exception { + super.setUp(); + MockitoAnnotations.initMocks(this); + mTemporaryFolder = new TemporaryFolder(); + mTemporaryFolder.create(); + mInputFile = mTemporaryFolder.newFile(); + mDownloads = new TemporaryFolder(); + mDownloads.create(); + mRootDirectory = new TemporaryFolder(); + mRootDirectory.create(); + } + + @Test + public void testEmptyLog() throws Exception { + LogRunnable task = new LogRunnable(); + task.setAppCollector(mCollector); + task.setDownloadsDirectory(mDownloads.getRoot()); + task.setRootDirectory(mRootDirectory.getRoot()); + task.setLogOutputFile(mInputFile); + task.setSystemSize(0L); + task.run(); + + JSONObject json = getJsonOutput(); + assertThat(json.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(0L); + assertThat(json.getLong(DiskStatsFileLogger.VIDEOS_KEY)).isEqualTo(0L); + assertThat(json.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(0L); + assertThat(json.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(0L); + assertThat(json.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(0L); + assertThat(json.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(0L); + assertThat(json.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(0L); + assertThat(json.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(0L); + assertThat( + json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY).length()).isEqualTo(0L); + assertThat(json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY).length()).isEqualTo(0L); + assertThat(json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY).length()).isEqualTo(0L); + } + + @Test + public void testPopulatedLogTask() throws Exception { + // Write data to directories. + writeDataToFile(mDownloads.newFile(), "lol"); + writeDataToFile(mRootDirectory.newFile("test.jpg"), "1234"); + writeDataToFile(mRootDirectory.newFile("test.mp4"), "12345"); + writeDataToFile(mRootDirectory.newFile("test.mp3"), "123456"); + writeDataToFile(mRootDirectory.newFile("test.whatever"), "1234567"); + + // Write apps. + ArrayList<PackageStats> apps = new ArrayList<>(); + PackageStats testApp = new PackageStats("com.test.app"); + testApp.dataSize = 5L; + testApp.cacheSize = 55L; + testApp.codeSize = 10L; + apps.add(testApp); + when(mCollector.getPackageStats(anyInt())).thenReturn(apps); + + LogRunnable task = new LogRunnable(); + task.setAppCollector(mCollector); + task.setDownloadsDirectory(mDownloads.getRoot()); + task.setRootDirectory(mRootDirectory.getRoot()); + task.setLogOutputFile(mInputFile); + task.setSystemSize(10L); + task.run(); + + JSONObject json = getJsonOutput(); + assertThat(json.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(4L); + assertThat(json.getLong(DiskStatsFileLogger.VIDEOS_KEY)).isEqualTo(5L); + assertThat(json.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(6L); + assertThat(json.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(3L); + assertThat(json.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(10L); + assertThat(json.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(7L); + assertThat(json.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(15L); + assertThat(json.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(55L); + assertThat( + json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY).length()).isEqualTo(1L); + assertThat(json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY).length()).isEqualTo(1L); + assertThat(json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY).length()).isEqualTo(1L); + } + + private void writeDataToFile(File f, String data) throws Exception{ + PrintStream out = new PrintStream(f); + out.print(data); + out.close(); + } + + private JSONObject getJsonOutput() throws Exception { + return new JSONObject(IoUtils.readFileAsString(mInputFile.getAbsolutePath())); + } +} |