Home | History | Annotate | Download | only in car
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.car;
     18 
     19 import android.car.Car;
     20 import android.car.storagemonitoring.CarStorageMonitoringManager;
     21 import android.car.storagemonitoring.ICarStorageMonitoring;
     22 import android.car.storagemonitoring.IIoStatsListener;
     23 import android.car.storagemonitoring.IoStats;
     24 import android.car.storagemonitoring.IoStatsEntry;
     25 import android.car.storagemonitoring.IoStatsEntry.Metrics;
     26 import android.car.storagemonitoring.LifetimeWriteInfo;
     27 import android.car.storagemonitoring.UidIoRecord;
     28 import android.car.storagemonitoring.WearEstimate;
     29 import android.car.storagemonitoring.WearEstimateChange;
     30 import android.content.ActivityNotFoundException;
     31 import android.content.ComponentName;
     32 import android.content.Context;
     33 import android.content.Intent;
     34 import android.content.res.Resources;
     35 import android.os.RemoteCallbackList;
     36 import android.os.RemoteException;
     37 import android.util.JsonWriter;
     38 import android.util.Log;
     39 import android.util.SparseArray;
     40 
     41 import com.android.car.internal.CarPermission;
     42 import com.android.car.storagemonitoring.IoStatsTracker;
     43 import com.android.car.storagemonitoring.UidIoStatsProvider;
     44 import com.android.car.storagemonitoring.WearEstimateRecord;
     45 import com.android.car.storagemonitoring.WearHistory;
     46 import com.android.car.storagemonitoring.WearInformation;
     47 import com.android.car.storagemonitoring.WearInformationProvider;
     48 import com.android.car.systeminterface.SystemInterface;
     49 
     50 import org.json.JSONArray;
     51 import org.json.JSONException;
     52 import org.json.JSONObject;
     53 
     54 import java.io.File;
     55 import java.io.FileWriter;
     56 import java.io.IOException;
     57 import java.io.PrintWriter;
     58 import java.nio.file.Files;
     59 import java.time.Duration;
     60 import java.time.Instant;
     61 import java.util.ArrayList;
     62 import java.util.Arrays;
     63 import java.util.Collections;
     64 import java.util.HashMap;
     65 import java.util.List;
     66 import java.util.Map;
     67 import java.util.Objects;
     68 import java.util.Optional;
     69 import java.util.stream.Collectors;
     70 import java.util.stream.IntStream;
     71 
     72 public class CarStorageMonitoringService extends ICarStorageMonitoring.Stub
     73         implements CarServiceBase {
     74     public static final String INTENT_EXCESSIVE_IO =
     75             CarStorageMonitoringManager.INTENT_EXCESSIVE_IO;
     76 
     77     public static final long SHUTDOWN_COST_INFO_MISSING =
     78             CarStorageMonitoringManager.SHUTDOWN_COST_INFO_MISSING;
     79 
     80     private static final boolean DBG = false;
     81     private static final String TAG = CarLog.TAG_STORAGE;
     82     private static final int MIN_WEAR_ESTIMATE_OF_CONCERN = 80;
     83 
     84     static final String UPTIME_TRACKER_FILENAME = "service_uptime";
     85     static final String WEAR_INFO_FILENAME = "wear_info";
     86     static final String LIFETIME_WRITES_FILENAME = "lifetime_write";
     87 
     88     private final WearInformationProvider[] mWearInformationProviders;
     89     private final Context mContext;
     90     private final File mUptimeTrackerFile;
     91     private final File mWearInfoFile;
     92     private final File mLifetimeWriteFile;
     93     private final OnShutdownReboot mOnShutdownReboot;
     94     private final SystemInterface mSystemInterface;
     95     private final UidIoStatsProvider mUidIoStatsProvider;
     96     private final SlidingWindow<IoStats> mIoStatsSamples;
     97     private final RemoteCallbackList<IIoStatsListener> mListeners;
     98     private final Object mIoStatsSamplesLock = new Object();
     99     private final Configuration mConfiguration;
    100 
    101     private final CarPermission mStorageMonitoringPermission;
    102 
    103     private UptimeTracker mUptimeTracker = null;
    104     private Optional<WearInformation> mWearInformation = Optional.empty();
    105     private List<WearEstimateChange> mWearEstimateChanges = Collections.emptyList();
    106     private List<IoStatsEntry> mBootIoStats = Collections.emptyList();
    107     private IoStatsTracker mIoStatsTracker = null;
    108     private boolean mInitialized = false;
    109 
    110     private long mShutdownCostInfo = SHUTDOWN_COST_INFO_MISSING;
    111     private String mShutdownCostMissingReason;
    112 
    113     public CarStorageMonitoringService(Context context, SystemInterface systemInterface) {
    114         mContext = context;
    115         Resources resources = mContext.getResources();
    116         mConfiguration = new Configuration(resources);
    117 
    118         if (Log.isLoggable(TAG, Log.DEBUG)) {
    119             Log.d(TAG, "service configuration: " + mConfiguration);
    120         }
    121 
    122         mUidIoStatsProvider = systemInterface.getUidIoStatsProvider();
    123         mUptimeTrackerFile = new File(systemInterface.getFilesDir(), UPTIME_TRACKER_FILENAME);
    124         mWearInfoFile = new File(systemInterface.getFilesDir(), WEAR_INFO_FILENAME);
    125         mLifetimeWriteFile = new File(systemInterface.getFilesDir(), LIFETIME_WRITES_FILENAME);
    126         mOnShutdownReboot = new OnShutdownReboot(mContext);
    127         mSystemInterface = systemInterface;
    128         mWearInformationProviders = systemInterface.getFlashWearInformationProviders();
    129         mStorageMonitoringPermission =
    130                 new CarPermission(mContext, Car.PERMISSION_STORAGE_MONITORING);
    131         mWearEstimateChanges = Collections.emptyList();
    132         mIoStatsSamples = new SlidingWindow<>(mConfiguration.ioStatsNumSamplesToStore);
    133         mListeners = new RemoteCallbackList<>();
    134         systemInterface.scheduleActionForBootCompleted(this::doInitServiceIfNeeded,
    135             Duration.ofSeconds(10));
    136     }
    137 
    138     private Optional<WearInformation> loadWearInformation() {
    139         for (WearInformationProvider provider : mWearInformationProviders) {
    140             WearInformation wearInfo = provider.load();
    141             if (wearInfo != null) {
    142                 Log.d(TAG, "retrieved wear info " + wearInfo + " via provider " + provider);
    143                 return Optional.of(wearInfo);
    144             }
    145         }
    146 
    147         Log.d(TAG, "no wear info available");
    148         return Optional.empty();
    149     }
    150 
    151     private WearHistory loadWearHistory() {
    152         if (mWearInfoFile.exists()) {
    153             try {
    154                 WearHistory wearHistory = WearHistory.fromJson(mWearInfoFile);
    155                 Log.d(TAG, "retrieved wear history " + wearHistory);
    156                 return wearHistory;
    157             } catch (IOException | JSONException e) {
    158                 Log.e(TAG, "unable to read wear info file " + mWearInfoFile, e);
    159             }
    160         }
    161 
    162         Log.d(TAG, "no wear history available");
    163         return new WearHistory();
    164     }
    165 
    166     // returns true iff a new event was added (and hence the history needs to be saved)
    167     private boolean addEventIfNeeded(WearHistory wearHistory) {
    168         if (!mWearInformation.isPresent()) return false;
    169 
    170         WearInformation wearInformation = mWearInformation.get();
    171         WearEstimate lastWearEstimate;
    172         WearEstimate currentWearEstimate = wearInformation.toWearEstimate();
    173 
    174         if (wearHistory.size() == 0) {
    175             lastWearEstimate = WearEstimate.UNKNOWN_ESTIMATE;
    176         } else {
    177             lastWearEstimate = wearHistory.getLast().getNewWearEstimate();
    178         }
    179 
    180         if (currentWearEstimate.equals(lastWearEstimate)) return false;
    181 
    182         WearEstimateRecord newRecord = new WearEstimateRecord(lastWearEstimate,
    183             currentWearEstimate,
    184             mUptimeTracker.getTotalUptime(),
    185             Instant.now());
    186         Log.d(TAG, "new wear record generated " + newRecord);
    187         wearHistory.add(newRecord);
    188         return true;
    189     }
    190 
    191     private void storeWearHistory(WearHistory wearHistory) {
    192         try (JsonWriter jsonWriter = new JsonWriter(new FileWriter(mWearInfoFile))) {
    193             wearHistory.writeToJson(jsonWriter);
    194         } catch (IOException e) {
    195             Log.e(TAG, "unable to write wear info file" + mWearInfoFile, e);
    196         }
    197     }
    198 
    199     @Override
    200     public void init() {
    201         Log.d(TAG, "CarStorageMonitoringService init()");
    202 
    203         mUptimeTracker = new UptimeTracker(mUptimeTrackerFile,
    204             mConfiguration.uptimeIntervalBetweenUptimeDataWriteMs,
    205             mSystemInterface);
    206     }
    207 
    208     private void launchWearChangeActivity() {
    209         final String activityPath = mConfiguration.activityHandlerForFlashWearChanges;
    210         if (activityPath.isEmpty()) return;
    211         try {
    212             final ComponentName activityComponent =
    213                 Objects.requireNonNull(ComponentName.unflattenFromString(activityPath));
    214             Intent intent = new Intent();
    215             intent.setComponent(activityComponent);
    216             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    217             mContext.startActivity(intent);
    218         } catch (ActivityNotFoundException | NullPointerException e) {
    219             Log.e(TAG,
    220                 "value of activityHandlerForFlashWearChanges invalid non-empty string " +
    221                     activityPath, e);
    222         }
    223     }
    224 
    225     private static void logOnAdverseWearLevel(WearInformation wearInformation) {
    226         if (wearInformation.preEolInfo > WearInformation.PRE_EOL_INFO_NORMAL ||
    227             Math.max(wearInformation.lifetimeEstimateA,
    228                 wearInformation.lifetimeEstimateB) >= MIN_WEAR_ESTIMATE_OF_CONCERN) {
    229             Log.w(TAG, "flash storage reached wear a level that requires attention: "
    230                     + wearInformation);
    231         }
    232     }
    233 
    234     private SparseArray<UidIoRecord> loadNewIoStats() {
    235         SparseArray<UidIoRecord> ioRecords = mUidIoStatsProvider.load();
    236         return (ioRecords == null ? new SparseArray<>() : ioRecords);
    237     }
    238 
    239     private void collectNewIoMetrics() {
    240         IoStats ioStats;
    241 
    242         mIoStatsTracker.update(loadNewIoStats());
    243         synchronized (mIoStatsSamplesLock) {
    244             ioStats = new IoStats(
    245                 SparseArrayStream.valueStream(mIoStatsTracker.getCurrentSample())
    246                     .collect(Collectors.toList()),
    247                 mSystemInterface.getUptime());
    248             mIoStatsSamples.add(ioStats);
    249         }
    250 
    251         if (DBG) {
    252             SparseArray<IoStatsEntry> currentSample = mIoStatsTracker.getCurrentSample();
    253             if (currentSample.size() == 0) {
    254                 Log.d(TAG, "no new I/O stat data");
    255             } else {
    256                 SparseArrayStream.valueStream(currentSample).forEach(
    257                     uidIoStats -> Log.d(TAG, "updated I/O stat data: " + uidIoStats));
    258             }
    259         }
    260 
    261         dispatchNewIoEvent(ioStats);
    262         if (needsExcessiveIoBroadcast()) {
    263             Log.d(TAG, "about to send " + INTENT_EXCESSIVE_IO);
    264             sendExcessiveIoBroadcast();
    265         }
    266     }
    267 
    268     private void sendExcessiveIoBroadcast() {
    269         Log.w(TAG, "sending " + INTENT_EXCESSIVE_IO);
    270 
    271         final String receiverPath = mConfiguration.intentReceiverForUnacceptableIoMetrics;
    272         if (receiverPath.isEmpty()) return;
    273 
    274         final ComponentName receiverComponent;
    275         try {
    276             receiverComponent = Objects.requireNonNull(
    277                     ComponentName.unflattenFromString(receiverPath));
    278         } catch (NullPointerException e) {
    279             Log.e(TAG, "value of intentReceiverForUnacceptableIoMetrics non-null but invalid:"
    280                     + receiverPath, e);
    281             return;
    282         }
    283 
    284         Intent intent = new Intent(INTENT_EXCESSIVE_IO);
    285         intent.setComponent(receiverComponent);
    286         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    287         mContext.sendBroadcast(intent, mStorageMonitoringPermission.toString());
    288     }
    289 
    290     private boolean needsExcessiveIoBroadcast() {
    291         synchronized (mIoStatsSamplesLock) {
    292             return mIoStatsSamples.count((IoStats delta) -> {
    293                 Metrics total = delta.getTotals();
    294                 final boolean tooManyBytesWritten =
    295                     (total.bytesWrittenToStorage > mConfiguration.acceptableBytesWrittenPerSample);
    296                 final boolean tooManyFsyncCalls =
    297                     (total.fsyncCalls > mConfiguration.acceptableFsyncCallsPerSample);
    298                 return tooManyBytesWritten || tooManyFsyncCalls;
    299             }) > mConfiguration.maxExcessiveIoSamplesInWindow;
    300         }
    301     }
    302 
    303     private void dispatchNewIoEvent(IoStats delta) {
    304         final int listenersCount = mListeners.beginBroadcast();
    305         IntStream.range(0, listenersCount).forEach(
    306             i -> {
    307                 try {
    308                     mListeners.getBroadcastItem(i).onSnapshot(delta);
    309                 } catch (RemoteException e) {
    310                     Log.w(TAG, "failed to dispatch snapshot", e);
    311                 }
    312             });
    313         mListeners.finishBroadcast();
    314     }
    315 
    316     private synchronized void doInitServiceIfNeeded() {
    317         if (mInitialized) return;
    318 
    319         Log.d(TAG, "initializing CarStorageMonitoringService");
    320 
    321         mWearInformation = loadWearInformation();
    322 
    323         // TODO(egranata): can this be done lazily?
    324         final WearHistory wearHistory = loadWearHistory();
    325         final boolean didWearChangeHappen = addEventIfNeeded(wearHistory);
    326         if (didWearChangeHappen) {
    327             storeWearHistory(wearHistory);
    328         }
    329         Log.d(TAG, "wear history being tracked is " + wearHistory);
    330         mWearEstimateChanges = wearHistory.toWearEstimateChanges(
    331                 mConfiguration.acceptableHoursPerOnePercentFlashWear);
    332 
    333         mOnShutdownReboot.addAction((c, i) -> logLifetimeWrites())
    334                 .addAction((c, i) -> release());
    335 
    336         mWearInformation.ifPresent(CarStorageMonitoringService::logOnAdverseWearLevel);
    337 
    338         if (didWearChangeHappen) {
    339             launchWearChangeActivity();
    340         }
    341 
    342         long bootUptime = mSystemInterface.getUptime();
    343         mBootIoStats = SparseArrayStream.valueStream(loadNewIoStats())
    344             .map(record -> {
    345                 // at boot, assume all UIDs have been running for as long as the system has
    346                 // been up, since we don't really know any better
    347                 IoStatsEntry stats = new IoStatsEntry(record, bootUptime);
    348                 if (DBG) {
    349                     Log.d(TAG, "loaded boot I/O stat data: " + stats);
    350                 }
    351                 return stats;
    352             }).collect(Collectors.toList());
    353 
    354         mIoStatsTracker = new IoStatsTracker(mBootIoStats,
    355                 mConfiguration.ioStatsRefreshRateMs,
    356                 mSystemInterface.getSystemStateInterface());
    357 
    358         if (mConfiguration.ioStatsNumSamplesToStore > 0) {
    359             mSystemInterface.scheduleAction(this::collectNewIoMetrics,
    360                 mConfiguration.ioStatsRefreshRateMs);
    361         } else {
    362             Log.i(TAG, "service configuration disabled I/O sample window. not collecting samples");
    363         }
    364 
    365         mShutdownCostInfo = computeShutdownCost();
    366         Log.d(TAG, "calculated data written in last shutdown was " +
    367                 mShutdownCostInfo + " bytes");
    368         mLifetimeWriteFile.delete();
    369 
    370         Log.i(TAG, "CarStorageMonitoringService is up");
    371 
    372         mInitialized = true;
    373     }
    374 
    375     private long computeShutdownCost() {
    376         List<LifetimeWriteInfo> shutdownWrites = loadLifetimeWrites();
    377         if (shutdownWrites.isEmpty()) {
    378             Log.d(TAG, "lifetime write data from last shutdown missing");
    379             mShutdownCostMissingReason = "no historical writes stored at last shutdown";
    380             return SHUTDOWN_COST_INFO_MISSING;
    381         }
    382         List<LifetimeWriteInfo> currentWrites =
    383                 Arrays.asList(mSystemInterface.getLifetimeWriteInfoProvider().load());
    384         if (currentWrites.isEmpty()) {
    385             Log.d(TAG, "current lifetime write data missing");
    386             mShutdownCostMissingReason = "current write data cannot be obtained";
    387             return SHUTDOWN_COST_INFO_MISSING;
    388         }
    389 
    390         long shutdownCost = 0;
    391 
    392         Map<String, Long> shutdownLifetimeWrites = new HashMap<>();
    393         shutdownWrites.forEach(li ->
    394             shutdownLifetimeWrites.put(li.partition, li.writtenBytes));
    395 
    396         // for every partition currently available, look for it in the shutdown data
    397         for(int i = 0; i < currentWrites.size(); ++i) {
    398             LifetimeWriteInfo li = currentWrites.get(i);
    399             // if this partition was not available when we last shutdown the system, then
    400             // just pretend we had written the same amount of data then as we have now
    401             final long writtenAtShutdown =
    402                     shutdownLifetimeWrites.getOrDefault(li.partition, li.writtenBytes);
    403             final long costDelta = li.writtenBytes - writtenAtShutdown;
    404             if (costDelta >= 0) {
    405                 Log.d(TAG, "partition " + li.partition + " had " + costDelta +
    406                     " bytes written to it during shutdown");
    407                 shutdownCost += costDelta;
    408             } else {
    409                 // the counter of written bytes should be monotonic; a decrease might mean
    410                 // corrupt data, improper shutdown or that the kernel in use does not
    411                 // have proper monotonic guarantees on the lifetime write data. If any of these
    412                 // occur, it's probably safer to just bail out and say we don't know
    413                 mShutdownCostMissingReason = li.partition + " has a negative write amount (" +
    414                         costDelta + " bytes)";
    415                 Log.e(TAG, "partition " + li.partition + " reported " + costDelta +
    416                     " bytes written to it during shutdown. assuming we can't" +
    417                     " determine proper shutdown information.");
    418                 return SHUTDOWN_COST_INFO_MISSING;
    419             }
    420         }
    421 
    422         return shutdownCost;
    423     }
    424 
    425     private List<LifetimeWriteInfo> loadLifetimeWrites() {
    426         if (!mLifetimeWriteFile.exists() || !mLifetimeWriteFile.isFile()) {
    427             Log.d(TAG, "lifetime write file missing or inaccessible " + mLifetimeWriteFile);
    428             return Collections.emptyList();
    429         }
    430         try {
    431             JSONObject jsonObject = new JSONObject(
    432                 new String(Files.readAllBytes(mLifetimeWriteFile.toPath())));
    433 
    434             JSONArray jsonArray = jsonObject.getJSONArray("lifetimeWriteInfo");
    435 
    436             List<LifetimeWriteInfo> result = new ArrayList<>();
    437             for (int i = 0; i < jsonArray.length(); ++i) {
    438                 result.add(new LifetimeWriteInfo(jsonArray.getJSONObject(i)));
    439             }
    440             return result;
    441         } catch (JSONException | IOException e) {
    442             Log.e(TAG, "lifetime write file does not contain valid JSON", e);
    443             return Collections.emptyList();
    444         }
    445     }
    446 
    447     private void logLifetimeWrites() {
    448         try {
    449             LifetimeWriteInfo[] lifetimeWriteInfos =
    450                 mSystemInterface.getLifetimeWriteInfoProvider().load();
    451             JsonWriter jsonWriter = new JsonWriter(new FileWriter(mLifetimeWriteFile));
    452             jsonWriter.beginObject();
    453             jsonWriter.name("lifetimeWriteInfo").beginArray();
    454             for (LifetimeWriteInfo writeInfo : lifetimeWriteInfos) {
    455                 Log.d(TAG, "storing lifetime write info " + writeInfo);
    456                 writeInfo.writeToJson(jsonWriter);
    457             }
    458             jsonWriter.endArray().endObject();
    459             jsonWriter.close();
    460         } catch (IOException e) {
    461             Log.e(TAG, "unable to save lifetime write info on shutdown", e);
    462         }
    463     }
    464 
    465     @Override
    466     public void release() {
    467         Log.i(TAG, "tearing down CarStorageMonitoringService");
    468         if (mUptimeTracker != null) {
    469             mUptimeTracker.onDestroy();
    470         }
    471         mOnShutdownReboot.clearActions();
    472         mListeners.kill();
    473     }
    474 
    475     @Override
    476     public void dump(PrintWriter writer) {
    477         doInitServiceIfNeeded();
    478 
    479         writer.println("*CarStorageMonitoringService*");
    480         writer.println("last wear information retrieved: " +
    481             mWearInformation.map(WearInformation::toString).orElse("missing"));
    482         writer.println("wear change history: " +
    483             mWearEstimateChanges.stream()
    484                 .map(WearEstimateChange::toString)
    485                 .collect(Collectors.joining("\n")));
    486         writer.println("boot I/O stats: " +
    487             mBootIoStats.stream()
    488                 .map(IoStatsEntry::toString)
    489                 .collect(Collectors.joining("\n")));
    490         writer.println("aggregate I/O stats: " +
    491             SparseArrayStream.valueStream(mIoStatsTracker.getTotal())
    492                 .map(IoStatsEntry::toString)
    493                 .collect(Collectors.joining("\n")));
    494         writer.println("I/O stats snapshots: ");
    495         synchronized (mIoStatsSamplesLock) {
    496             writer.println(
    497                 mIoStatsSamples.stream().map(
    498                     sample -> sample.getStats().stream()
    499                         .map(IoStatsEntry::toString)
    500                         .collect(Collectors.joining("\n")))
    501                     .collect(Collectors.joining("\n------\n")));
    502         }
    503         if (mShutdownCostInfo < 0) {
    504             writer.print("last shutdown cost: missing. ");
    505             if (mShutdownCostMissingReason != null && !mShutdownCostMissingReason.isEmpty()) {
    506                 writer.println("reason: " + mShutdownCostMissingReason);
    507             }
    508         } else {
    509             writer.println("last shutdown cost: " + mShutdownCostInfo + " bytes, estimated");
    510         }
    511     }
    512 
    513     // ICarStorageMonitoring implementation
    514 
    515     @Override
    516     public int getPreEolIndicatorStatus() {
    517         mStorageMonitoringPermission.assertGranted();
    518         doInitServiceIfNeeded();
    519 
    520         return mWearInformation.map(wi -> wi.preEolInfo)
    521                 .orElse(WearInformation.UNKNOWN_PRE_EOL_INFO);
    522     }
    523 
    524     @Override
    525     public WearEstimate getWearEstimate() {
    526         mStorageMonitoringPermission.assertGranted();
    527         doInitServiceIfNeeded();
    528 
    529         return mWearInformation.map(wi ->
    530                 new WearEstimate(wi.lifetimeEstimateA,wi.lifetimeEstimateB)).orElse(
    531                     WearEstimate.UNKNOWN_ESTIMATE);
    532     }
    533 
    534     @Override
    535     public List<WearEstimateChange> getWearEstimateHistory() {
    536         mStorageMonitoringPermission.assertGranted();
    537         doInitServiceIfNeeded();
    538 
    539         return mWearEstimateChanges;
    540     }
    541 
    542     @Override
    543     public List<IoStatsEntry> getBootIoStats() {
    544         mStorageMonitoringPermission.assertGranted();
    545         doInitServiceIfNeeded();
    546 
    547         return mBootIoStats;
    548     }
    549 
    550     @Override
    551     public List<IoStatsEntry> getAggregateIoStats() {
    552         mStorageMonitoringPermission.assertGranted();
    553         doInitServiceIfNeeded();
    554 
    555         return SparseArrayStream.valueStream(mIoStatsTracker.getTotal())
    556                 .collect(Collectors.toList());
    557     }
    558 
    559     @Override
    560     public long getShutdownDiskWriteAmount() {
    561         mStorageMonitoringPermission.assertGranted();
    562         doInitServiceIfNeeded();
    563 
    564         return mShutdownCostInfo;
    565     }
    566 
    567     @Override
    568     public List<IoStats> getIoStatsDeltas() {
    569         mStorageMonitoringPermission.assertGranted();
    570         doInitServiceIfNeeded();
    571 
    572         synchronized (mIoStatsSamplesLock) {
    573             return mIoStatsSamples.stream().collect(Collectors.toList());
    574         }
    575     }
    576 
    577     @Override
    578     public void registerListener(IIoStatsListener listener) {
    579         mStorageMonitoringPermission.assertGranted();
    580         doInitServiceIfNeeded();
    581 
    582         mListeners.register(listener);
    583     }
    584 
    585     @Override
    586     public void unregisterListener(IIoStatsListener listener) {
    587         mStorageMonitoringPermission.assertGranted();
    588         // no need to initialize service if unregistering
    589 
    590         mListeners.unregister(listener);
    591     }
    592 
    593     private static final class Configuration {
    594         final long acceptableBytesWrittenPerSample;
    595         final int acceptableFsyncCallsPerSample;
    596         final int acceptableHoursPerOnePercentFlashWear;
    597         final String activityHandlerForFlashWearChanges;
    598         final String intentReceiverForUnacceptableIoMetrics;
    599         final int ioStatsNumSamplesToStore;
    600         final int ioStatsRefreshRateMs;
    601         final int maxExcessiveIoSamplesInWindow;
    602         final long uptimeIntervalBetweenUptimeDataWriteMs;
    603 
    604         Configuration(Resources resources) throws Resources.NotFoundException {
    605             ioStatsNumSamplesToStore = resources.getInteger(R.integer.ioStatsNumSamplesToStore);
    606             acceptableBytesWrittenPerSample =
    607                     1024 * resources.getInteger(R.integer.acceptableWrittenKBytesPerSample);
    608             acceptableFsyncCallsPerSample =
    609                     resources.getInteger(R.integer.acceptableFsyncCallsPerSample);
    610             maxExcessiveIoSamplesInWindow =
    611                     resources.getInteger(R.integer.maxExcessiveIoSamplesInWindow);
    612             uptimeIntervalBetweenUptimeDataWriteMs =
    613                         60 * 60 * 1000 *
    614                         resources.getInteger(R.integer.uptimeHoursIntervalBetweenUptimeDataWrite);
    615             acceptableHoursPerOnePercentFlashWear =
    616                     resources.getInteger(R.integer.acceptableHoursPerOnePercentFlashWear);
    617             ioStatsRefreshRateMs =
    618                     1000 * resources.getInteger(R.integer.ioStatsRefreshRateSeconds);
    619             activityHandlerForFlashWearChanges =
    620                     resources.getString(R.string.activityHandlerForFlashWearChanges);
    621             intentReceiverForUnacceptableIoMetrics =
    622                     resources.getString(R.string.intentReceiverForUnacceptableIoMetrics);
    623         }
    624 
    625         @Override
    626         public String toString() {
    627             return String.format(
    628                 "acceptableBytesWrittenPerSample = %d, " +
    629                 "acceptableFsyncCallsPerSample = %d, " +
    630                 "acceptableHoursPerOnePercentFlashWear = %d, " +
    631                 "activityHandlerForFlashWearChanges = %s, " +
    632                 "intentReceiverForUnacceptableIoMetrics = %s, " +
    633                 "ioStatsNumSamplesToStore = %d, " +
    634                 "ioStatsRefreshRateMs = %d, " +
    635                 "maxExcessiveIoSamplesInWindow = %d, " +
    636                 "uptimeIntervalBetweenUptimeDataWriteMs = %d",
    637                 acceptableBytesWrittenPerSample,
    638                 acceptableFsyncCallsPerSample,
    639                 acceptableHoursPerOnePercentFlashWear,
    640                 activityHandlerForFlashWearChanges,
    641                 intentReceiverForUnacceptableIoMetrics,
    642                 ioStatsNumSamplesToStore,
    643                 ioStatsRefreshRateMs,
    644                 maxExcessiveIoSamplesInWindow,
    645                 uptimeIntervalBetweenUptimeDataWriteMs);
    646         }
    647     }
    648 }
    649