blob: d34f73359f48ad38f7fa7875f07c9cb3260ec72f [file] [log] [blame]
/*
* Copyright (C) 2017 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.car;
import android.car.Car;
import android.car.storagemonitoring.CarStorageMonitoringManager;
import android.car.storagemonitoring.ICarStorageMonitoring;
import android.car.storagemonitoring.IIoStatsListener;
import android.car.storagemonitoring.IoStats;
import android.car.storagemonitoring.IoStatsEntry;
import android.car.storagemonitoring.IoStatsEntry.Metrics;
import android.car.storagemonitoring.LifetimeWriteInfo;
import android.car.storagemonitoring.UidIoRecord;
import android.car.storagemonitoring.WearEstimate;
import android.car.storagemonitoring.WearEstimateChange;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.IndentingPrintWriter;
import android.util.JsonWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import com.android.car.internal.CarPermission;
import com.android.car.storagemonitoring.IoStatsTracker;
import com.android.car.storagemonitoring.UidIoStatsProvider;
import com.android.car.storagemonitoring.WearEstimateRecord;
import com.android.car.storagemonitoring.WearHistory;
import com.android.car.storagemonitoring.WearInformation;
import com.android.car.storagemonitoring.WearInformationProvider;
import com.android.car.systeminterface.SystemInterface;
import com.android.internal.annotations.GuardedBy;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* A service to provide storage monitoring data like I/O statistics. In order to receive such data,
* users need to implement {@link IIoStatsListener} and register themselves against this service.
*/
public class CarStorageMonitoringService extends ICarStorageMonitoring.Stub
implements CarServiceBase {
public static final String INTENT_EXCESSIVE_IO =
CarStorageMonitoringManager.INTENT_EXCESSIVE_IO;
public static final long SHUTDOWN_COST_INFO_MISSING =
CarStorageMonitoringManager.SHUTDOWN_COST_INFO_MISSING;
private static final boolean DBG = false;
private static final String TAG = CarLog.tagFor(CarStorageMonitoringService.class);
private static final int MIN_WEAR_ESTIMATE_OF_CONCERN = 80;
static final String UPTIME_TRACKER_FILENAME = "service_uptime";
static final String WEAR_INFO_FILENAME = "wear_info";
static final String LIFETIME_WRITES_FILENAME = "lifetime_write";
private final WearInformationProvider[] mWearInformationProviders;
private final Context mContext;
private final File mUptimeTrackerFile;
private final File mWearInfoFile;
private final File mLifetimeWriteFile;
private final OnShutdownReboot mOnShutdownReboot;
private final SystemInterface mSystemInterface;
private final UidIoStatsProvider mUidIoStatsProvider;
private final Object mLock = new Object();
@GuardedBy("mLock")
private final SlidingWindow<IoStats> mIoStatsSamples;
private final RemoteCallbackList<IIoStatsListener> mListeners;
private final Configuration mConfiguration;
private final CarPermission mStorageMonitoringPermission;
@GuardedBy("mLock")
private UptimeTracker mUptimeTracker = null;
@GuardedBy("mLock")
private Optional<WearInformation> mWearInformation = Optional.empty();
@GuardedBy("mLock")
private List<WearEstimateChange> mWearEstimateChanges;
@GuardedBy("mLock")
private List<IoStatsEntry> mBootIoStats = Collections.emptyList();
@GuardedBy("mLock")
private IoStatsTracker mIoStatsTracker = null;
@GuardedBy("mLock")
private boolean mInitialized = false;
@GuardedBy("mLock")
private long mShutdownCostInfo = SHUTDOWN_COST_INFO_MISSING;
@GuardedBy("mLock")
private String mShutdownCostMissingReason;
public CarStorageMonitoringService(Context context, SystemInterface systemInterface) {
mContext = context;
Resources resources = mContext.getResources();
mConfiguration = new Configuration(resources);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Slog.d(TAG, "service configuration: " + mConfiguration);
}
mUidIoStatsProvider = systemInterface.getUidIoStatsProvider();
mUptimeTrackerFile = new File(systemInterface.getSystemCarDir(), UPTIME_TRACKER_FILENAME);
mWearInfoFile = new File(systemInterface.getSystemCarDir(), WEAR_INFO_FILENAME);
mLifetimeWriteFile = new File(systemInterface.getSystemCarDir(), LIFETIME_WRITES_FILENAME);
mOnShutdownReboot = new OnShutdownReboot(mContext);
mSystemInterface = systemInterface;
mWearInformationProviders = systemInterface.getFlashWearInformationProviders();
mStorageMonitoringPermission =
new CarPermission(mContext, Car.PERMISSION_STORAGE_MONITORING);
mWearEstimateChanges = Collections.emptyList();
mIoStatsSamples = new SlidingWindow<>(mConfiguration.ioStatsNumSamplesToStore);
mListeners = new RemoteCallbackList<>();
systemInterface.scheduleActionForBootCompleted(() -> {
synchronized (mLock) {
doInitServiceIfNeededLocked();
}}, Duration.ofSeconds(10));
}
private Optional<WearInformation> loadWearInformation() {
for (WearInformationProvider provider : mWearInformationProviders) {
WearInformation wearInfo = provider.load();
if (wearInfo != null) {
Slog.d(TAG, "retrieved wear info " + wearInfo + " via provider " + provider);
return Optional.of(wearInfo);
}
}
Slog.d(TAG, "no wear info available");
return Optional.empty();
}
private WearHistory loadWearHistory() {
if (mWearInfoFile.exists()) {
try {
WearHistory wearHistory = WearHistory.fromJson(mWearInfoFile);
Slog.d(TAG, "retrieved wear history " + wearHistory);
return wearHistory;
} catch (IOException | JSONException e) {
Slog.e(TAG, "unable to read wear info file " + mWearInfoFile, e);
}
}
Slog.d(TAG, "no wear history available");
return new WearHistory();
}
// returns true iff a new event was added (and hence the history needs to be saved)
@GuardedBy("mLock")
private boolean addEventIfNeededLocked(WearHistory wearHistory) {
if (!mWearInformation.isPresent()) return false;
WearInformation wearInformation = mWearInformation.get();
WearEstimate lastWearEstimate;
WearEstimate currentWearEstimate = wearInformation.toWearEstimate();
if (wearHistory.size() == 0) {
lastWearEstimate = WearEstimate.UNKNOWN_ESTIMATE;
} else {
lastWearEstimate = wearHistory.getLast().getNewWearEstimate();
}
if (currentWearEstimate.equals(lastWearEstimate)) return false;
WearEstimateRecord newRecord = new WearEstimateRecord(lastWearEstimate,
currentWearEstimate,
mUptimeTracker.getTotalUptime(),
Instant.now());
Slog.d(TAG, "new wear record generated " + newRecord);
wearHistory.add(newRecord);
return true;
}
private void storeWearHistory(WearHistory wearHistory) {
try (JsonWriter jsonWriter = new JsonWriter(new FileWriter(mWearInfoFile))) {
wearHistory.writeToJson(jsonWriter);
} catch (IOException e) {
Slog.e(TAG, "unable to write wear info file" + mWearInfoFile, e);
}
}
@Override
public void init() {
Slog.d(TAG, "CarStorageMonitoringService init()");
synchronized (mLock) {
mUptimeTracker = new UptimeTracker(mUptimeTrackerFile,
mConfiguration.uptimeIntervalBetweenUptimeDataWriteMs,
mSystemInterface);
}
}
private void launchWearChangeActivity() {
final String activityPath = mConfiguration.activityHandlerForFlashWearChanges;
if (activityPath.isEmpty()) return;
try {
final ComponentName activityComponent =
Objects.requireNonNull(ComponentName.unflattenFromString(activityPath));
Intent intent = new Intent();
intent.setComponent(activityComponent);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
} catch (ActivityNotFoundException | NullPointerException e) {
Slog.e(TAG, "value of activityHandlerForFlashWearChanges invalid non-empty string "
+ activityPath, e);
}
}
private static void logOnAdverseWearLevel(WearInformation wearInformation) {
if (wearInformation.preEolInfo > WearInformation.PRE_EOL_INFO_NORMAL ||
Math.max(wearInformation.lifetimeEstimateA,
wearInformation.lifetimeEstimateB) >= MIN_WEAR_ESTIMATE_OF_CONCERN) {
Slog.w(TAG, "flash storage reached wear a level that requires attention: "
+ wearInformation);
}
}
private SparseArray<UidIoRecord> loadNewIoStats() {
SparseArray<UidIoRecord> ioRecords = mUidIoStatsProvider.load();
return (ioRecords == null ? new SparseArray<>() : ioRecords);
}
private void collectNewIoMetrics() {
SparseArray<IoStatsEntry> currentSample;
boolean needsExcessiveIoBroadcast;
IoStats ioStats;
synchronized (mLock) {
mIoStatsTracker.update(loadNewIoStats());
currentSample = mIoStatsTracker.getCurrentSample();
ioStats = new IoStats(
SparseArrayStream.valueStream(currentSample).collect(Collectors.toList()),
mSystemInterface.getUptime());
mIoStatsSamples.add(ioStats);
needsExcessiveIoBroadcast = needsExcessiveIoBroadcastLocked();
}
dispatchNewIoEvent(ioStats);
if (DBG) {
if (currentSample.size() == 0) {
Slog.d(TAG, "no new I/O stat data");
} else {
SparseArrayStream.valueStream(currentSample).forEach(
uidIoStats -> Slog.d(TAG, "updated I/O stat data: " + uidIoStats));
}
}
if (needsExcessiveIoBroadcast) {
Slog.d(TAG, "about to send " + INTENT_EXCESSIVE_IO);
sendExcessiveIoBroadcast();
}
}
private void sendExcessiveIoBroadcast() {
Slog.w(TAG, "sending " + INTENT_EXCESSIVE_IO);
final String receiverPath = mConfiguration.intentReceiverForUnacceptableIoMetrics;
if (receiverPath.isEmpty()) return;
final ComponentName receiverComponent;
try {
receiverComponent = Objects.requireNonNull(
ComponentName.unflattenFromString(receiverPath));
} catch (NullPointerException e) {
Slog.e(TAG, "value of intentReceiverForUnacceptableIoMetrics non-null but invalid:"
+ receiverPath, e);
return;
}
Intent intent = new Intent(INTENT_EXCESSIVE_IO);
intent.setComponent(receiverComponent);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
mContext.sendBroadcast(intent, mStorageMonitoringPermission.toString());
}
@GuardedBy("mLock")
private boolean needsExcessiveIoBroadcastLocked() {
return mIoStatsSamples.count((IoStats delta) -> {
Metrics total = delta.getTotals();
final boolean tooManyBytesWritten =
(total.bytesWrittenToStorage > mConfiguration.acceptableBytesWrittenPerSample);
final boolean tooManyFsyncCalls =
(total.fsyncCalls > mConfiguration.acceptableFsyncCallsPerSample);
return tooManyBytesWritten || tooManyFsyncCalls;
}) > mConfiguration.maxExcessiveIoSamplesInWindow;
}
private void dispatchNewIoEvent(IoStats delta) {
final int listenersCount = mListeners.beginBroadcast();
IntStream.range(0, listenersCount).forEach(
i -> {
try {
mListeners.getBroadcastItem(i).onSnapshot(delta);
} catch (RemoteException e) {
Slog.w(TAG, "failed to dispatch snapshot", e);
}
});
mListeners.finishBroadcast();
}
@GuardedBy("mLock")
private void doInitServiceIfNeededLocked() {
if (mInitialized) return;
Slog.d(TAG, "initializing CarStorageMonitoringService");
mWearInformation = loadWearInformation();
// TODO(egranata): can this be done lazily?
final WearHistory wearHistory = loadWearHistory();
final boolean didWearChangeHappen = addEventIfNeededLocked(wearHistory);
if (didWearChangeHappen) {
storeWearHistory(wearHistory);
}
Slog.d(TAG, "wear history being tracked is " + wearHistory);
mWearEstimateChanges = wearHistory.toWearEstimateChanges(
mConfiguration.acceptableHoursPerOnePercentFlashWear);
mOnShutdownReboot.addAction((c, i) -> logLifetimeWrites())
.addAction((c, i) -> release());
mWearInformation.ifPresent(CarStorageMonitoringService::logOnAdverseWearLevel);
if (didWearChangeHappen) {
launchWearChangeActivity();
}
long bootUptime = mSystemInterface.getUptime();
mBootIoStats = SparseArrayStream.valueStream(loadNewIoStats())
.map(record -> {
// at boot, assume all UIDs have been running for as long as the system has
// been up, since we don't really know any better
IoStatsEntry stats = new IoStatsEntry(record, bootUptime);
if (DBG) {
Slog.d(TAG, "loaded boot I/O stat data: " + stats);
}
return stats;
}).collect(Collectors.toList());
mIoStatsTracker = new IoStatsTracker(mBootIoStats,
mConfiguration.ioStatsRefreshRateMs,
mSystemInterface.getSystemStateInterface());
if (mConfiguration.ioStatsNumSamplesToStore > 0) {
mSystemInterface.scheduleAction(this::collectNewIoMetrics,
mConfiguration.ioStatsRefreshRateMs);
} else {
Slog.i(TAG, "service configuration disabled I/O sample window. not collecting samples");
}
mShutdownCostInfo = computeShutdownCostLocked();
Slog.d(TAG, "calculated data written in last shutdown was " + mShutdownCostInfo + " bytes");
mLifetimeWriteFile.delete();
Slog.i(TAG, "CarStorageMonitoringService is up");
mInitialized = true;
}
@GuardedBy("mLock")
private long computeShutdownCostLocked() {
List<LifetimeWriteInfo> shutdownWrites = loadLifetimeWrites();
if (shutdownWrites.isEmpty()) {
Slog.d(TAG, "lifetime write data from last shutdown missing");
mShutdownCostMissingReason = "no historical writes stored at last shutdown";
return SHUTDOWN_COST_INFO_MISSING;
}
List<LifetimeWriteInfo> currentWrites =
Arrays.asList(mSystemInterface.getLifetimeWriteInfoProvider().load());
if (currentWrites.isEmpty()) {
Slog.d(TAG, "current lifetime write data missing");
mShutdownCostMissingReason = "current write data cannot be obtained";
return SHUTDOWN_COST_INFO_MISSING;
}
long shutdownCost = 0;
Map<String, Long> shutdownLifetimeWrites = new HashMap<>();
shutdownWrites.forEach(li ->
shutdownLifetimeWrites.put(li.partition, li.writtenBytes));
// for every partition currently available, look for it in the shutdown data
for (int i = 0; i < currentWrites.size(); ++i) {
LifetimeWriteInfo li = currentWrites.get(i);
// if this partition was not available when we last shutdown the system, then
// just pretend we had written the same amount of data then as we have now
final long writtenAtShutdown =
shutdownLifetimeWrites.getOrDefault(li.partition, li.writtenBytes);
final long costDelta = li.writtenBytes - writtenAtShutdown;
if (costDelta >= 0) {
Slog.d(TAG, "partition " + li.partition + " had " + costDelta
+ " bytes written to it during shutdown");
shutdownCost += costDelta;
} else {
// the counter of written bytes should be monotonic; a decrease might mean
// corrupt data, improper shutdown or that the kernel in use does not
// have proper monotonic guarantees on the lifetime write data. If any of these
// occur, it's probably safer to just bail out and say we don't know
mShutdownCostMissingReason = li.partition + " has a negative write amount ("
+ costDelta + " bytes)";
Slog.e(TAG, "partition " + li.partition + " reported " + costDelta
+ " bytes written to it during shutdown. assuming we can't"
+ " determine proper shutdown information.");
return SHUTDOWN_COST_INFO_MISSING;
}
}
return shutdownCost;
}
private List<LifetimeWriteInfo> loadLifetimeWrites() {
if (!mLifetimeWriteFile.exists() || !mLifetimeWriteFile.isFile()) {
Slog.d(TAG, "lifetime write file missing or inaccessible " + mLifetimeWriteFile);
return Collections.emptyList();
}
try {
JSONObject jsonObject = new JSONObject(
new String(Files.readAllBytes(mLifetimeWriteFile.toPath())));
JSONArray jsonArray = jsonObject.getJSONArray("lifetimeWriteInfo");
List<LifetimeWriteInfo> result = new ArrayList<>();
for (int i = 0; i < jsonArray.length(); ++i) {
result.add(new LifetimeWriteInfo(jsonArray.getJSONObject(i)));
}
return result;
} catch (JSONException | IOException e) {
Slog.e(TAG, "lifetime write file does not contain valid JSON", e);
return Collections.emptyList();
}
}
private void logLifetimeWrites() {
try {
LifetimeWriteInfo[] lifetimeWriteInfos =
mSystemInterface.getLifetimeWriteInfoProvider().load();
JsonWriter jsonWriter = new JsonWriter(new FileWriter(mLifetimeWriteFile));
jsonWriter.beginObject();
jsonWriter.name("lifetimeWriteInfo").beginArray();
for (LifetimeWriteInfo writeInfo : lifetimeWriteInfos) {
Slog.d(TAG, "storing lifetime write info " + writeInfo);
writeInfo.writeToJson(jsonWriter);
}
jsonWriter.endArray().endObject();
jsonWriter.close();
} catch (IOException e) {
Slog.e(TAG, "unable to save lifetime write info on shutdown", e);
}
}
@Override
public void release() {
Slog.i(TAG, "tearing down CarStorageMonitoringService");
synchronized (mLock) {
if (mUptimeTracker != null) {
mUptimeTracker.onDestroy();
}
}
mOnShutdownReboot.clearActions();
mListeners.kill();
}
@Override
public void dump(IndentingPrintWriter writer) {
writer.println("*CarStorageMonitoringService*");
synchronized (mLock) {
doInitServiceIfNeededLocked();
writer.println("last wear information retrieved: "
+ mWearInformation.map(WearInformation::toString).orElse("missing"));
writer.println("wear change history: "
+ mWearEstimateChanges.stream()
.map(WearEstimateChange::toString)
.collect(Collectors.joining("\n")));
writer.println("boot I/O stats: "
+ mBootIoStats.stream()
.map(IoStatsEntry::toString)
.collect(Collectors.joining("\n")));
writer.println("aggregate I/O stats: "
+ SparseArrayStream.valueStream(mIoStatsTracker.getTotal())
.map(IoStatsEntry::toString)
.collect(Collectors.joining("\n")));
writer.println("I/O stats snapshots: ");
writer.println(
mIoStatsSamples.stream().map(
sample -> sample.getStats().stream()
.map(IoStatsEntry::toString)
.collect(Collectors.joining("\n")))
.collect(Collectors.joining("\n------\n")));
if (mShutdownCostInfo < 0) {
writer.print("last shutdown cost: missing. ");
if (mShutdownCostMissingReason != null && !mShutdownCostMissingReason.isEmpty()) {
writer.println("reason: " + mShutdownCostMissingReason);
}
} else {
writer.println("last shutdown cost: " + mShutdownCostInfo + " bytes, estimated");
}
}
}
// ICarStorageMonitoring implementation
@Override
public int getPreEolIndicatorStatus() {
mStorageMonitoringPermission.assertGranted();
synchronized (mLock) {
doInitServiceIfNeededLocked();
return mWearInformation.map(wi -> wi.preEolInfo)
.orElse(WearInformation.UNKNOWN_PRE_EOL_INFO);
}
}
@Override
public WearEstimate getWearEstimate() {
mStorageMonitoringPermission.assertGranted();
synchronized (mLock) {
doInitServiceIfNeededLocked();
return mWearInformation.map(wi ->
new WearEstimate(wi.lifetimeEstimateA, wi.lifetimeEstimateB)).orElse(
WearEstimate.UNKNOWN_ESTIMATE);
}
}
@Override
public List<WearEstimateChange> getWearEstimateHistory() {
mStorageMonitoringPermission.assertGranted();
synchronized (mLock) {
doInitServiceIfNeededLocked();
return Collections.unmodifiableList(mWearEstimateChanges);
}
}
@Override
public List<IoStatsEntry> getBootIoStats() {
mStorageMonitoringPermission.assertGranted();
doInitServiceIfNeededLocked();
return Collections.unmodifiableList(mBootIoStats);
}
@Override
public List<IoStatsEntry> getAggregateIoStats() {
mStorageMonitoringPermission.assertGranted();
synchronized (mLock) {
doInitServiceIfNeededLocked();
return Collections.unmodifiableList(SparseArrayStream.valueStream(
mIoStatsTracker.getTotal()).collect(Collectors.toList()));
}
}
@Override
public long getShutdownDiskWriteAmount() {
mStorageMonitoringPermission.assertGranted();
synchronized (mLock) {
doInitServiceIfNeededLocked();
return mShutdownCostInfo;
}
}
@Override
public List<IoStats> getIoStatsDeltas() {
mStorageMonitoringPermission.assertGranted();
synchronized (mLock) {
doInitServiceIfNeededLocked();
return Collections.unmodifiableList(
mIoStatsSamples.stream().collect(Collectors.toList()));
}
}
@Override
public void registerListener(IIoStatsListener listener) {
mStorageMonitoringPermission.assertGranted();
synchronized (mLock) {
doInitServiceIfNeededLocked();
}
mListeners.register(listener);
}
@Override
public void unregisterListener(IIoStatsListener listener) {
mStorageMonitoringPermission.assertGranted();
// no need to initialize service if unregistering
mListeners.unregister(listener);
}
private static final class Configuration {
final long acceptableBytesWrittenPerSample;
final int acceptableFsyncCallsPerSample;
final int acceptableHoursPerOnePercentFlashWear;
final String activityHandlerForFlashWearChanges;
final String intentReceiverForUnacceptableIoMetrics;
final int ioStatsNumSamplesToStore;
final int ioStatsRefreshRateMs;
final int maxExcessiveIoSamplesInWindow;
final long uptimeIntervalBetweenUptimeDataWriteMs;
Configuration(Resources resources) throws Resources.NotFoundException {
ioStatsNumSamplesToStore = resources.getInteger(R.integer.ioStatsNumSamplesToStore);
acceptableBytesWrittenPerSample =
1024 * resources.getInteger(R.integer.acceptableWrittenKBytesPerSample);
acceptableFsyncCallsPerSample =
resources.getInteger(R.integer.acceptableFsyncCallsPerSample);
maxExcessiveIoSamplesInWindow =
resources.getInteger(R.integer.maxExcessiveIoSamplesInWindow);
uptimeIntervalBetweenUptimeDataWriteMs = 60 * 60 * 1000
* resources.getInteger(R.integer.uptimeHoursIntervalBetweenUptimeDataWrite);
acceptableHoursPerOnePercentFlashWear =
resources.getInteger(R.integer.acceptableHoursPerOnePercentFlashWear);
ioStatsRefreshRateMs = 1000 * resources.getInteger(R.integer.ioStatsRefreshRateSeconds);
activityHandlerForFlashWearChanges =
resources.getString(R.string.activityHandlerForFlashWearChanges);
intentReceiverForUnacceptableIoMetrics =
resources.getString(R.string.intentReceiverForUnacceptableIoMetrics);
}
@Override
public String toString() {
return String.format("acceptableBytesWrittenPerSample = %d, "
+ "acceptableFsyncCallsPerSample = %d, "
+ "acceptableHoursPerOnePercentFlashWear = %d, "
+ "activityHandlerForFlashWearChanges = %s, "
+ "intentReceiverForUnacceptableIoMetrics = %s, "
+ "ioStatsNumSamplesToStore = %d, "
+ "ioStatsRefreshRateMs = %d, "
+ "maxExcessiveIoSamplesInWindow = %d, "
+ "uptimeIntervalBetweenUptimeDataWriteMs = %d",
acceptableBytesWrittenPerSample,
acceptableFsyncCallsPerSample,
acceptableHoursPerOnePercentFlashWear,
activityHandlerForFlashWearChanges,
intentReceiverForUnacceptableIoMetrics,
ioStatsNumSamplesToStore,
ioStatsRefreshRateMs,
maxExcessiveIoSamplesInWindow,
uptimeIntervalBetweenUptimeDataWriteMs);
}
}
}