| /* |
| * 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.internal.os; |
| |
| import static com.android.internal.os.BinderLatencyProto.Dims.SYSTEM_SERVER; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.database.ContentObserver; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Process; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.text.format.DateFormat; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.IntArray; |
| import android.util.KeyValueListParser; |
| import android.util.Pair; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.os.BinderInternal.CallSession; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Queue; |
| import java.util.Random; |
| import java.util.concurrent.ConcurrentLinkedQueue; |
| import java.util.function.ToDoubleFunction; |
| |
| /** |
| * Collects statistics about CPU time spent per binder call across multiple dimensions, e.g. |
| * per thread, uid or call description. |
| */ |
| public class BinderCallsStats implements BinderInternal.Observer { |
| public static final boolean ENABLED_DEFAULT = true; |
| public static final boolean DETAILED_TRACKING_DEFAULT = true; |
| public static final int PERIODIC_SAMPLING_INTERVAL_DEFAULT = 1000; |
| public static final boolean DEFAULT_TRACK_SCREEN_INTERACTIVE = false; |
| public static final boolean DEFAULT_TRACK_DIRECT_CALLING_UID = true; |
| public static final boolean DEFAULT_IGNORE_BATTERY_STATUS = false; |
| public static final boolean DEFAULT_COLLECT_LATENCY_DATA = true; |
| public static final int MAX_BINDER_CALL_STATS_COUNT_DEFAULT = 1500; |
| public static final int SHARDING_MODULO_DEFAULT = 1; |
| private static final String DEBUG_ENTRY_PREFIX = "__DEBUG_"; |
| |
| private static class OverflowBinder extends Binder {} |
| |
| private static final String TAG = "BinderCallsStats"; |
| private static final int CALL_SESSIONS_POOL_SIZE = 100; |
| private static final int MAX_EXCEPTION_COUNT_SIZE = 50; |
| private static final String EXCEPTION_COUNT_OVERFLOW_NAME = "overflow"; |
| // Default values for overflow entry. The work source uid does not use a default value in order |
| // to have on overflow entry per work source uid. |
| private static final Class<? extends Binder> OVERFLOW_BINDER = OverflowBinder.class; |
| private static final boolean OVERFLOW_SCREEN_INTERACTIVE = false; |
| private static final int OVERFLOW_DIRECT_CALLING_UID = -1; |
| private static final int OVERFLOW_TRANSACTION_CODE = -1; |
| |
| // Whether to collect all the data: cpu + exceptions + reply/request sizes. |
| private boolean mDetailedTracking = DETAILED_TRACKING_DEFAULT; |
| // If set to true, indicates that all transactions for specific UIDs are being |
| // recorded, ignoring sampling. The UidEntry.recordAllTransactions flag is also set |
| // for the UIDs being tracked. |
| private boolean mRecordingAllTransactionsForUid; |
| // Sampling period to control how often to track CPU usage. 1 means all calls, 100 means ~1 out |
| // of 100 requests. |
| private int mPeriodicSamplingInterval = PERIODIC_SAMPLING_INTERVAL_DEFAULT; |
| private int mMaxBinderCallStatsCount = MAX_BINDER_CALL_STATS_COUNT_DEFAULT; |
| @GuardedBy("mLock") |
| private final SparseArray<UidEntry> mUidEntries = new SparseArray<>(); |
| @GuardedBy("mLock") |
| private final ArrayMap<String, Integer> mExceptionCounts = new ArrayMap<>(); |
| private final Queue<CallSession> mCallSessionsPool = new ConcurrentLinkedQueue<>(); |
| private final Object mLock = new Object(); |
| private final Random mRandom; |
| private long mStartCurrentTime = System.currentTimeMillis(); |
| private long mStartElapsedTime = SystemClock.elapsedRealtime(); |
| private long mCallStatsCount = 0; |
| private boolean mAddDebugEntries = false; |
| private boolean mTrackDirectCallingUid = DEFAULT_TRACK_DIRECT_CALLING_UID; |
| private boolean mTrackScreenInteractive = DEFAULT_TRACK_SCREEN_INTERACTIVE; |
| private boolean mIgnoreBatteryStatus = DEFAULT_IGNORE_BATTERY_STATUS; |
| private boolean mCollectLatencyData = DEFAULT_COLLECT_LATENCY_DATA; |
| |
| // Controls how many APIs will be collected per device. 1 means all APIs, 10 means every 10th |
| // API will be collected. |
| private int mShardingModulo = SHARDING_MODULO_DEFAULT; |
| // Controls which shards will be collected on this device. |
| private int mShardingOffset; |
| |
| private CachedDeviceState.Readonly mDeviceState; |
| private CachedDeviceState.TimeInStateStopwatch mBatteryStopwatch; |
| |
| private static final int CALL_STATS_OBSERVER_DEBOUNCE_MILLIS = 5000; |
| private BinderLatencyObserver mLatencyObserver; |
| private BinderInternal.CallStatsObserver mCallStatsObserver; |
| private ArraySet<Integer> mSendUidsToObserver = new ArraySet<>(32); |
| private final Handler mCallStatsObserverHandler; |
| private Runnable mCallStatsObserverRunnable = new Runnable() { |
| @Override |
| public void run() { |
| if (mCallStatsObserver == null) { |
| return; |
| } |
| |
| noteCallsStatsDelayed(); |
| |
| synchronized (mLock) { |
| int size = mSendUidsToObserver.size(); |
| for (int i = 0; i < size; i++) { |
| UidEntry uidEntry = mUidEntries.get(mSendUidsToObserver.valueAt(i)); |
| if (uidEntry != null) { |
| ArrayMap<CallStatKey, CallStat> callStats = uidEntry.mCallStats; |
| final int csize = callStats.size(); |
| final ArrayList<CallStat> tmpCallStats = new ArrayList<>(csize); |
| for (int j = 0; j < csize; j++) { |
| tmpCallStats.add(callStats.valueAt(j).clone()); |
| } |
| mCallStatsObserver.noteCallStats(uidEntry.workSourceUid, |
| uidEntry.incrementalCallCount, tmpCallStats |
| ); |
| uidEntry.incrementalCallCount = 0; |
| for (int j = callStats.size() - 1; j >= 0; j--) { |
| callStats.valueAt(j).incrementalCallCount = 0; |
| } |
| } |
| } |
| mSendUidsToObserver.clear(); |
| } |
| } |
| }; |
| |
| private final Object mNativeTidsLock = new Object(); |
| // @GuardedBy("mNativeTidsLock") // Cannot mark it as "GuardedBy" because it's read |
| // directly, as a volatile field. |
| private volatile IntArray mNativeTids = new IntArray(0); |
| |
| /** Injector for {@link BinderCallsStats}. */ |
| public static class Injector { |
| public Random getRandomGenerator() { |
| return new Random(); |
| } |
| |
| public Handler getHandler() { |
| return new Handler(Looper.getMainLooper()); |
| } |
| |
| /** Create a latency observer for the specified process. */ |
| public BinderLatencyObserver getLatencyObserver(int processSource) { |
| return new BinderLatencyObserver(new BinderLatencyObserver.Injector(), processSource); |
| } |
| } |
| |
| public BinderCallsStats(Injector injector) { |
| this(injector, SYSTEM_SERVER); |
| } |
| |
| public BinderCallsStats(Injector injector, int processSource) { |
| this.mRandom = injector.getRandomGenerator(); |
| this.mCallStatsObserverHandler = injector.getHandler(); |
| this.mLatencyObserver = injector.getLatencyObserver(processSource); |
| this.mShardingOffset = mRandom.nextInt(mShardingModulo); |
| } |
| |
| public void setDeviceState(@NonNull CachedDeviceState.Readonly deviceState) { |
| if (mBatteryStopwatch != null) { |
| mBatteryStopwatch.close(); |
| } |
| mDeviceState = deviceState; |
| mBatteryStopwatch = deviceState.createTimeOnBatteryStopwatch(); |
| } |
| |
| /** |
| * Registers an observer for call stats, which is invoked periodically with accumulated |
| * binder call stats. |
| */ |
| public void setCallStatsObserver( |
| BinderInternal.CallStatsObserver callStatsObserver) { |
| mCallStatsObserver = callStatsObserver; |
| noteBinderThreadNativeIds(); |
| noteCallsStatsDelayed(); |
| } |
| |
| private void noteCallsStatsDelayed() { |
| mCallStatsObserverHandler.removeCallbacks(mCallStatsObserverRunnable); |
| if (mCallStatsObserver != null) { |
| mCallStatsObserverHandler.postDelayed(mCallStatsObserverRunnable, |
| CALL_STATS_OBSERVER_DEBOUNCE_MILLIS); |
| } |
| } |
| |
| @Override |
| @Nullable |
| public CallSession callStarted(Binder binder, int code, int workSourceUid) { |
| noteNativeThreadId(); |
| |
| boolean collectCpu = canCollect(); |
| // We always want to collect data for latency if it's enabled, regardless of device state. |
| if (!mCollectLatencyData && !collectCpu) { |
| return null; |
| } |
| |
| final CallSession s = obtainCallSession(); |
| s.binderClass = binder.getClass(); |
| s.transactionCode = code; |
| s.exceptionThrown = false; |
| s.cpuTimeStarted = -1; |
| s.timeStarted = -1; |
| s.recordedCall = shouldRecordDetailedData(); |
| |
| if (collectCpu && (mRecordingAllTransactionsForUid || s.recordedCall)) { |
| s.cpuTimeStarted = getThreadTimeMicro(); |
| s.timeStarted = getElapsedRealtimeMicro(); |
| } else if (mCollectLatencyData) { |
| s.timeStarted = getElapsedRealtimeMicro(); |
| } |
| |
| return s; |
| } |
| |
| private CallSession obtainCallSession() { |
| CallSession s = mCallSessionsPool.poll(); |
| return s == null ? new CallSession() : s; |
| } |
| |
| @Override |
| public void callEnded(@Nullable CallSession s, int parcelRequestSize, |
| int parcelReplySize, int workSourceUid) { |
| if (s == null) { |
| return; |
| } |
| |
| processCallEnded(s, parcelRequestSize, parcelReplySize, workSourceUid); |
| |
| if (mCallSessionsPool.size() < CALL_SESSIONS_POOL_SIZE) { |
| mCallSessionsPool.add(s); |
| } |
| } |
| |
| private void processCallEnded(CallSession s, |
| int parcelRequestSize, int parcelReplySize, int workSourceUid) { |
| if (mCollectLatencyData) { |
| mLatencyObserver.callEnded(s); |
| } |
| |
| // Latency collection has already been processed so check if the rest should be processed. |
| if (!canCollect()) { |
| return; |
| } |
| |
| UidEntry uidEntry = null; |
| final boolean recordCall; |
| if (s.recordedCall) { |
| recordCall = true; |
| } else if (mRecordingAllTransactionsForUid) { |
| uidEntry = getUidEntry(workSourceUid); |
| recordCall = uidEntry.recordAllTransactions; |
| } else { |
| recordCall = false; |
| } |
| |
| final long duration; |
| final long latencyDuration; |
| if (recordCall) { |
| duration = getThreadTimeMicro() - s.cpuTimeStarted; |
| latencyDuration = getElapsedRealtimeMicro() - s.timeStarted; |
| } else { |
| duration = 0; |
| latencyDuration = 0; |
| } |
| final boolean screenInteractive = mTrackScreenInteractive |
| ? mDeviceState.isScreenInteractive() |
| : OVERFLOW_SCREEN_INTERACTIVE; |
| final int callingUid = mTrackDirectCallingUid |
| ? getCallingUid() |
| : OVERFLOW_DIRECT_CALLING_UID; |
| |
| synchronized (mLock) { |
| // This was already checked in #callStart but check again while synchronized. |
| if (!canCollect()) { |
| return; |
| } |
| |
| if (uidEntry == null) { |
| uidEntry = getUidEntry(workSourceUid); |
| } |
| |
| uidEntry.callCount++; |
| uidEntry.incrementalCallCount++; |
| if (recordCall) { |
| uidEntry.cpuTimeMicros += duration; |
| uidEntry.recordedCallCount++; |
| |
| final CallStat callStat = uidEntry.getOrCreate( |
| callingUid, s.binderClass, s.transactionCode, |
| screenInteractive, |
| mCallStatsCount >= mMaxBinderCallStatsCount); |
| final boolean isNewCallStat = callStat.callCount == 0; |
| if (isNewCallStat) { |
| mCallStatsCount++; |
| } |
| |
| callStat.callCount++; |
| callStat.incrementalCallCount++; |
| callStat.recordedCallCount++; |
| callStat.cpuTimeMicros += duration; |
| callStat.maxCpuTimeMicros = Math.max(callStat.maxCpuTimeMicros, duration); |
| callStat.latencyMicros += latencyDuration; |
| callStat.maxLatencyMicros = |
| Math.max(callStat.maxLatencyMicros, latencyDuration); |
| if (mDetailedTracking) { |
| callStat.exceptionCount += s.exceptionThrown ? 1 : 0; |
| callStat.maxRequestSizeBytes = |
| Math.max(callStat.maxRequestSizeBytes, parcelRequestSize); |
| callStat.maxReplySizeBytes = |
| Math.max(callStat.maxReplySizeBytes, parcelReplySize); |
| } |
| } else { |
| // Only record the total call count if we already track data for this key. |
| // It helps to keep the memory usage down when sampling is enabled. |
| final CallStat callStat = uidEntry.get( |
| callingUid, s.binderClass, s.transactionCode, |
| screenInteractive); |
| if (callStat != null) { |
| callStat.callCount++; |
| callStat.incrementalCallCount++; |
| } |
| } |
| if (mCallStatsObserver != null && !UserHandle.isCore(workSourceUid)) { |
| mSendUidsToObserver.add(workSourceUid); |
| } |
| } |
| } |
| |
| private boolean shouldExport(ExportedCallStat e, boolean applySharding) { |
| if (!applySharding) { |
| return true; |
| } |
| |
| int hash = e.binderClass.hashCode(); |
| hash = 31 * hash + e.transactionCode; |
| hash = 31 * hash + e.callingUid; |
| hash = 31 * hash + (e.screenInteractive ? 1231 : 1237); |
| |
| return (hash + mShardingOffset) % mShardingModulo == 0; |
| } |
| |
| private UidEntry getUidEntry(int uid) { |
| UidEntry uidEntry = mUidEntries.get(uid); |
| if (uidEntry == null) { |
| uidEntry = new UidEntry(uid); |
| mUidEntries.put(uid, uidEntry); |
| } |
| return uidEntry; |
| } |
| |
| @Override |
| public void callThrewException(@Nullable CallSession s, Exception exception) { |
| if (s == null) { |
| return; |
| } |
| s.exceptionThrown = true; |
| try { |
| String className = exception.getClass().getName(); |
| synchronized (mLock) { |
| if (mExceptionCounts.size() >= MAX_EXCEPTION_COUNT_SIZE) { |
| className = EXCEPTION_COUNT_OVERFLOW_NAME; |
| } |
| final Integer count = mExceptionCounts.get(className); |
| mExceptionCounts.put(className, count == null ? 1 : count + 1); |
| } |
| } catch (RuntimeException e) { |
| // Do not propagate the exception. We do not want to swallow original exception. |
| Slog.wtf(TAG, "Unexpected exception while updating mExceptionCounts"); |
| } |
| } |
| |
| private void noteNativeThreadId() { |
| final int tid = getNativeTid(); |
| int index = mNativeTids.binarySearch(tid); |
| if (index >= 0) { |
| return; |
| } |
| |
| // Use the copy-on-write approach. The changes occur exceedingly infrequently, so |
| // this code path is exercised just a few times per boot |
| synchronized (mNativeTidsLock) { |
| IntArray nativeTids = mNativeTids; |
| index = nativeTids.binarySearch(tid); |
| if (index < 0) { |
| IntArray copyOnWriteArray = new IntArray(nativeTids.size() + 1); |
| copyOnWriteArray.addAll(nativeTids); |
| copyOnWriteArray.add(-index - 1, tid); |
| mNativeTids = copyOnWriteArray; |
| } |
| } |
| |
| noteBinderThreadNativeIds(); |
| } |
| |
| private void noteBinderThreadNativeIds() { |
| if (mCallStatsObserver == null) { |
| return; |
| } |
| |
| mCallStatsObserver.noteBinderThreadNativeIds(getNativeTids()); |
| } |
| |
| private boolean canCollect() { |
| if (mRecordingAllTransactionsForUid) { |
| return true; |
| } |
| if (mIgnoreBatteryStatus) { |
| return true; |
| } |
| if (mDeviceState == null) { |
| return false; |
| } |
| if (mDeviceState.isCharging()) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * This method is expensive to call. |
| */ |
| public ArrayList<ExportedCallStat> getExportedCallStats() { |
| return getExportedCallStats(false); |
| } |
| |
| /** |
| * This method is expensive to call. |
| * Exports call stats and applies sharding if requested. |
| */ |
| @VisibleForTesting |
| public ArrayList<ExportedCallStat> getExportedCallStats(boolean applySharding) { |
| // We do not collect all the data if detailed tracking is off. |
| if (!mDetailedTracking) { |
| return new ArrayList<>(); |
| } |
| |
| ArrayList<ExportedCallStat> resultCallStats = new ArrayList<>(); |
| synchronized (mLock) { |
| final int uidEntriesSize = mUidEntries.size(); |
| for (int entryIdx = 0; entryIdx < uidEntriesSize; entryIdx++) { |
| final UidEntry entry = mUidEntries.valueAt(entryIdx); |
| for (CallStat stat : entry.getCallStatsList()) { |
| ExportedCallStat e = getExportedCallStat(entry.workSourceUid, stat); |
| if (shouldExport(e, applySharding)) { |
| resultCallStats.add(e); |
| } |
| } |
| } |
| } |
| |
| // Resolve codes outside of the lock since it can be slow. |
| resolveBinderMethodNames(resultCallStats); |
| |
| // Debug entries added to help validate the data. |
| if (mAddDebugEntries && mBatteryStopwatch != null) { |
| resultCallStats.add(createDebugEntry("start_time_millis", mStartElapsedTime)); |
| resultCallStats.add(createDebugEntry("end_time_millis", SystemClock.elapsedRealtime())); |
| resultCallStats.add( |
| createDebugEntry("battery_time_millis", mBatteryStopwatch.getMillis())); |
| resultCallStats.add(createDebugEntry("sampling_interval", mPeriodicSamplingInterval)); |
| resultCallStats.add(createDebugEntry("sharding_modulo", mShardingModulo)); |
| } |
| |
| return resultCallStats; |
| } |
| |
| /** |
| * This method is expensive to call. |
| */ |
| public ArrayList<ExportedCallStat> getExportedCallStats(int workSourceUid) { |
| return getExportedCallStats(workSourceUid, false); |
| } |
| |
| /** |
| * This method is expensive to call. |
| * Exports call stats and applies sharding if requested. |
| */ |
| @VisibleForTesting |
| public ArrayList<ExportedCallStat> getExportedCallStats( |
| int workSourceUid, boolean applySharding) { |
| ArrayList<ExportedCallStat> resultCallStats = new ArrayList<>(); |
| synchronized (mLock) { |
| final UidEntry entry = getUidEntry(workSourceUid); |
| for (CallStat stat : entry.getCallStatsList()) { |
| ExportedCallStat e = getExportedCallStat(workSourceUid, stat); |
| if (shouldExport(e, applySharding)) { |
| resultCallStats.add(e); |
| } |
| } |
| } |
| |
| // Resolve codes outside of the lock since it can be slow. |
| resolveBinderMethodNames(resultCallStats); |
| |
| return resultCallStats; |
| } |
| |
| private ExportedCallStat getExportedCallStat(int workSourceUid, CallStat stat) { |
| ExportedCallStat exported = new ExportedCallStat(); |
| exported.workSourceUid = workSourceUid; |
| exported.callingUid = stat.callingUid; |
| exported.className = stat.binderClass.getName(); |
| exported.binderClass = stat.binderClass; |
| exported.transactionCode = stat.transactionCode; |
| exported.screenInteractive = stat.screenInteractive; |
| exported.cpuTimeMicros = stat.cpuTimeMicros; |
| exported.maxCpuTimeMicros = stat.maxCpuTimeMicros; |
| exported.latencyMicros = stat.latencyMicros; |
| exported.maxLatencyMicros = stat.maxLatencyMicros; |
| exported.recordedCallCount = stat.recordedCallCount; |
| exported.callCount = stat.callCount; |
| exported.maxRequestSizeBytes = stat.maxRequestSizeBytes; |
| exported.maxReplySizeBytes = stat.maxReplySizeBytes; |
| exported.exceptionCount = stat.exceptionCount; |
| return exported; |
| } |
| |
| private void resolveBinderMethodNames( |
| ArrayList<ExportedCallStat> resultCallStats) { |
| // Resolve codes outside of the lock since it can be slow. |
| ExportedCallStat previous = null; |
| String previousMethodName = null; |
| resultCallStats.sort(BinderCallsStats::compareByBinderClassAndCode); |
| BinderTransactionNameResolver resolver = new BinderTransactionNameResolver(); |
| for (ExportedCallStat exported : resultCallStats) { |
| final boolean isClassDifferent = previous == null |
| || !previous.className.equals(exported.className); |
| final boolean isCodeDifferent = previous == null |
| || previous.transactionCode != exported.transactionCode; |
| final String methodName; |
| if (isClassDifferent || isCodeDifferent) { |
| methodName = resolver.getMethodName(exported.binderClass, exported.transactionCode); |
| } else { |
| methodName = previousMethodName; |
| } |
| previousMethodName = methodName; |
| exported.methodName = methodName; |
| previous = exported; |
| } |
| } |
| |
| private ExportedCallStat createDebugEntry(String variableName, long value) { |
| final int uid = Process.myUid(); |
| final ExportedCallStat callStat = new ExportedCallStat(); |
| callStat.className = ""; |
| callStat.workSourceUid = uid; |
| callStat.callingUid = uid; |
| callStat.recordedCallCount = 1; |
| callStat.callCount = 1; |
| callStat.methodName = DEBUG_ENTRY_PREFIX + variableName; |
| callStat.latencyMicros = value; |
| return callStat; |
| } |
| |
| /** @hide */ |
| public ArrayMap<String, Integer> getExportedExceptionStats() { |
| synchronized (mLock) { |
| return new ArrayMap(mExceptionCounts); |
| } |
| } |
| |
| /** Writes the collected statistics to the supplied {@link PrintWriter}.*/ |
| public void dump(PrintWriter pw, AppIdToPackageMap packageMap, int workSourceUid, |
| boolean verbose) { |
| synchronized (mLock) { |
| dumpLocked(pw, packageMap, workSourceUid, verbose); |
| } |
| } |
| |
| private void dumpLocked(PrintWriter pw, AppIdToPackageMap packageMap, int workSourceUid, |
| boolean verbose) { |
| if (workSourceUid != Process.INVALID_UID) { |
| verbose = true; |
| } |
| pw.print("Start time: "); |
| pw.println(DateFormat.format("yyyy-MM-dd HH:mm:ss", mStartCurrentTime)); |
| pw.print("On battery time (ms): "); |
| pw.println(mBatteryStopwatch != null ? mBatteryStopwatch.getMillis() : 0); |
| pw.println("Sampling interval period: " + mPeriodicSamplingInterval); |
| pw.println("Sharding modulo: " + mShardingModulo); |
| |
| final String datasetSizeDesc = verbose ? "" : "(top 90% by cpu time) "; |
| final StringBuilder sb = new StringBuilder(); |
| pw.println("Per-UID raw data " + datasetSizeDesc |
| + "(package/uid, worksource, call_desc, screen_interactive, " |
| + "cpu_time_micros, max_cpu_time_micros, " |
| + "latency_time_micros, max_latency_time_micros, exception_count, " |
| + "max_request_size_bytes, max_reply_size_bytes, recorded_call_count, " |
| + "call_count):"); |
| final List<ExportedCallStat> exportedCallStats; |
| if (workSourceUid != Process.INVALID_UID) { |
| exportedCallStats = getExportedCallStats(workSourceUid, true); |
| } else { |
| exportedCallStats = getExportedCallStats(true); |
| } |
| exportedCallStats.sort(BinderCallsStats::compareByCpuDesc); |
| for (ExportedCallStat e : exportedCallStats) { |
| if (e.methodName != null && e.methodName.startsWith(DEBUG_ENTRY_PREFIX)) { |
| // Do not dump debug entries. |
| continue; |
| } |
| sb.setLength(0); |
| sb.append(" ") |
| .append(packageMap.mapUid(e.callingUid)) |
| .append(',') |
| .append(packageMap.mapUid(e.workSourceUid)) |
| .append(',').append(e.className) |
| .append('#').append(e.methodName) |
| .append(',').append(e.screenInteractive) |
| .append(',').append(e.cpuTimeMicros) |
| .append(',').append(e.maxCpuTimeMicros) |
| .append(',').append(e.latencyMicros) |
| .append(',').append(e.maxLatencyMicros) |
| .append(',').append(mDetailedTracking ? e.exceptionCount : '_') |
| .append(',').append(mDetailedTracking ? e.maxRequestSizeBytes : '_') |
| .append(',').append(mDetailedTracking ? e.maxReplySizeBytes : '_') |
| .append(',').append(e.recordedCallCount) |
| .append(',').append(e.callCount); |
| pw.println(sb); |
| } |
| pw.println(); |
| final List<UidEntry> entries = new ArrayList<>(); |
| long totalCallsCount = 0; |
| long totalRecordedCallsCount = 0; |
| long totalCpuTime = 0; |
| |
| if (workSourceUid != Process.INVALID_UID) { |
| UidEntry e = getUidEntry(workSourceUid); |
| entries.add(e); |
| totalCpuTime += e.cpuTimeMicros; |
| totalRecordedCallsCount += e.recordedCallCount; |
| totalCallsCount += e.callCount; |
| } else { |
| final int uidEntriesSize = mUidEntries.size(); |
| for (int i = 0; i < uidEntriesSize; i++) { |
| UidEntry e = mUidEntries.valueAt(i); |
| entries.add(e); |
| totalCpuTime += e.cpuTimeMicros; |
| totalRecordedCallsCount += e.recordedCallCount; |
| totalCallsCount += e.callCount; |
| } |
| entries.sort( |
| Comparator.<UidEntry>comparingDouble(value -> value.cpuTimeMicros).reversed()); |
| } |
| |
| pw.println("Per-UID Summary " + datasetSizeDesc |
| + "(cpu_time, % of total cpu_time, recorded_call_count, call_count, package/uid):"); |
| final List<UidEntry> summaryEntries = verbose ? entries |
| : getHighestValues(entries, value -> value.cpuTimeMicros, 0.9); |
| for (UidEntry entry : summaryEntries) { |
| String uidStr = packageMap.mapUid(entry.workSourceUid); |
| pw.println(String.format(" %10d %3.0f%% %8d %8d %s", |
| entry.cpuTimeMicros, 100d * entry.cpuTimeMicros / totalCpuTime, |
| entry.recordedCallCount, entry.callCount, uidStr)); |
| } |
| pw.println(); |
| if (workSourceUid == Process.INVALID_UID) { |
| pw.println(String.format(" Summary: total_cpu_time=%d, " |
| + "calls_count=%d, avg_call_cpu_time=%.0f", |
| totalCpuTime, totalCallsCount, |
| (double) totalCpuTime / totalRecordedCallsCount)); |
| pw.println(); |
| } |
| |
| pw.println("Exceptions thrown (exception_count, class_name):"); |
| final List<Pair<String, Integer>> exceptionEntries = new ArrayList<>(); |
| // We cannot use new ArrayList(Collection) constructor because MapCollections does not |
| // implement toArray method. |
| mExceptionCounts.entrySet().iterator().forEachRemaining( |
| (e) -> exceptionEntries.add(Pair.create(e.getKey(), e.getValue()))); |
| exceptionEntries.sort((e1, e2) -> Integer.compare(e2.second, e1.second)); |
| for (Pair<String, Integer> entry : exceptionEntries) { |
| pw.println(String.format(" %6d %s", entry.second, entry.first)); |
| } |
| |
| if (mPeriodicSamplingInterval != 1) { |
| pw.println(""); |
| pw.println("/!\\ Displayed data is sampled. See sampling interval at the top."); |
| } |
| } |
| |
| protected long getThreadTimeMicro() { |
| return SystemClock.currentThreadTimeMicro(); |
| } |
| |
| protected int getCallingUid() { |
| return Binder.getCallingUid(); |
| } |
| |
| protected int getNativeTid() { |
| return Process.myTid(); |
| } |
| |
| /** |
| * Returns known Linux TIDs for threads taking incoming binder calls. |
| */ |
| public int[] getNativeTids() { |
| return mNativeTids.toArray(); |
| } |
| |
| protected long getElapsedRealtimeMicro() { |
| return SystemClock.elapsedRealtimeNanos() / 1000; |
| } |
| |
| protected boolean shouldRecordDetailedData() { |
| return mRandom.nextInt() % mPeriodicSamplingInterval == 0; |
| } |
| |
| /** |
| * Sets to true to collect all the data. |
| */ |
| public void setDetailedTracking(boolean enabled) { |
| synchronized (mLock) { |
| if (enabled != mDetailedTracking) { |
| mDetailedTracking = enabled; |
| reset(); |
| } |
| } |
| } |
| |
| /** |
| * Whether to track the screen state. |
| */ |
| public void setTrackScreenInteractive(boolean enabled) { |
| synchronized (mLock) { |
| if (enabled != mTrackScreenInteractive) { |
| mTrackScreenInteractive = enabled; |
| reset(); |
| } |
| } |
| } |
| |
| /** |
| * Whether to track direct caller uid. |
| */ |
| public void setTrackDirectCallerUid(boolean enabled) { |
| synchronized (mLock) { |
| if (enabled != mTrackDirectCallingUid) { |
| mTrackDirectCallingUid = enabled; |
| reset(); |
| } |
| } |
| } |
| |
| /** |
| * Whether to ignore battery status when collecting stats |
| */ |
| public void setIgnoreBatteryStatus(boolean ignored) { |
| synchronized (mLock) { |
| if (ignored != mIgnoreBatteryStatus) { |
| mIgnoreBatteryStatus = ignored; |
| reset(); |
| } |
| } |
| } |
| |
| /** |
| * Marks the specified work source UID for total binder call tracking: detailed information |
| * will be recorded for all calls from this source ID. |
| * |
| * This is expensive and can cause memory pressure, therefore this mode should only be used |
| * for debugging. |
| */ |
| public void recordAllCallsForWorkSourceUid(int workSourceUid) { |
| setDetailedTracking(true); |
| |
| Slog.i(TAG, "Recording all Binder calls for UID: " + workSourceUid); |
| UidEntry uidEntry = getUidEntry(workSourceUid); |
| uidEntry.recordAllTransactions = true; |
| mRecordingAllTransactionsForUid = true; |
| } |
| |
| public void setAddDebugEntries(boolean addDebugEntries) { |
| mAddDebugEntries = addDebugEntries; |
| } |
| |
| /** |
| * Sets the maximum number of items to track. |
| */ |
| public void setMaxBinderCallStats(int maxKeys) { |
| if (maxKeys <= 0) { |
| Slog.w(TAG, "Ignored invalid max value (value must be positive): " |
| + maxKeys); |
| return; |
| } |
| |
| synchronized (mLock) { |
| if (maxKeys != mMaxBinderCallStatsCount) { |
| mMaxBinderCallStatsCount = maxKeys; |
| reset(); |
| } |
| } |
| } |
| |
| public void setSamplingInterval(int samplingInterval) { |
| if (samplingInterval <= 0) { |
| Slog.w(TAG, "Ignored invalid sampling interval (value must be positive): " |
| + samplingInterval); |
| return; |
| } |
| |
| synchronized (mLock) { |
| if (samplingInterval != mPeriodicSamplingInterval) { |
| mPeriodicSamplingInterval = samplingInterval; |
| reset(); |
| } |
| } |
| } |
| |
| /** Updates the sharding modulo. */ |
| public void setShardingModulo(int shardingModulo) { |
| if (shardingModulo <= 0) { |
| Slog.w(TAG, "Ignored invalid sharding modulo (value must be positive): " |
| + shardingModulo); |
| return; |
| } |
| |
| synchronized (mLock) { |
| if (shardingModulo != mShardingModulo) { |
| mShardingModulo = shardingModulo; |
| mShardingOffset = mRandom.nextInt(shardingModulo); |
| reset(); |
| } |
| } |
| } |
| |
| /** Whether to collect latency histograms. */ |
| public void setCollectLatencyData(boolean collectLatencyData) { |
| mCollectLatencyData = collectLatencyData; |
| } |
| |
| /** Whether to collect latency histograms. */ |
| @VisibleForTesting |
| public boolean getCollectLatencyData() { |
| return mCollectLatencyData; |
| } |
| |
| public void reset() { |
| synchronized (mLock) { |
| mCallStatsCount = 0; |
| mUidEntries.clear(); |
| mExceptionCounts.clear(); |
| mStartCurrentTime = System.currentTimeMillis(); |
| mStartElapsedTime = SystemClock.elapsedRealtime(); |
| if (mBatteryStopwatch != null) { |
| mBatteryStopwatch.reset(); |
| } |
| mRecordingAllTransactionsForUid = false; |
| // Do not reset the latency observer as binder stats and latency will be pushed to WW |
| // at different intervals so the resets should not be coupled. |
| } |
| } |
| |
| /** |
| * Aggregated data by uid/class/method to be sent through statsd. |
| */ |
| public static class ExportedCallStat { |
| public int callingUid; |
| public int workSourceUid; |
| public String className; |
| public String methodName; |
| public boolean screenInteractive; |
| public long cpuTimeMicros; |
| public long maxCpuTimeMicros; |
| public long latencyMicros; |
| public long maxLatencyMicros; |
| public long callCount; |
| public long recordedCallCount; |
| public long maxRequestSizeBytes; |
| public long maxReplySizeBytes; |
| public long exceptionCount; |
| |
| // Used internally. |
| Class<? extends Binder> binderClass; |
| int transactionCode; |
| } |
| |
| @VisibleForTesting |
| public static class CallStat { |
| // The UID who executed the transaction (i.e. Binder#getCallingUid). |
| public final int callingUid; |
| public final Class<? extends Binder> binderClass; |
| public final int transactionCode; |
| // True if the screen was interactive when the call ended. |
| public final boolean screenInteractive; |
| // Number of calls for which we collected data for. We do not record data for all the calls |
| // when sampling is on. |
| public long recordedCallCount; |
| // Roughly the real number of total calls. We only track only track the API call count once |
| // at least one non-sampled count happened. |
| public long callCount; |
| // Total CPU of all for all the recorded calls. |
| // Approximate total CPU usage can be computed by |
| // cpuTimeMicros * callCount / recordedCallCount |
| public long cpuTimeMicros; |
| public long maxCpuTimeMicros; |
| // Total latency of all for all the recorded calls. |
| // Approximate average latency can be computed by |
| // latencyMicros * callCount / recordedCallCount |
| public long latencyMicros; |
| public long maxLatencyMicros; |
| // The following fields are only computed if mDetailedTracking is set. |
| public long maxRequestSizeBytes; |
| public long maxReplySizeBytes; |
| public long exceptionCount; |
| // Call count since reset |
| public long incrementalCallCount; |
| |
| public CallStat(int callingUid, Class<? extends Binder> binderClass, int transactionCode, |
| boolean screenInteractive) { |
| this.callingUid = callingUid; |
| this.binderClass = binderClass; |
| this.transactionCode = transactionCode; |
| this.screenInteractive = screenInteractive; |
| } |
| |
| @Override |
| public CallStat clone() { |
| CallStat clone = new CallStat(callingUid, binderClass, transactionCode, |
| screenInteractive); |
| clone.recordedCallCount = recordedCallCount; |
| clone.callCount = callCount; |
| clone.cpuTimeMicros = cpuTimeMicros; |
| clone.maxCpuTimeMicros = maxCpuTimeMicros; |
| clone.latencyMicros = latencyMicros; |
| clone.maxLatencyMicros = maxLatencyMicros; |
| clone.maxRequestSizeBytes = maxRequestSizeBytes; |
| clone.maxReplySizeBytes = maxReplySizeBytes; |
| clone.exceptionCount = exceptionCount; |
| clone.incrementalCallCount = incrementalCallCount; |
| return clone; |
| } |
| |
| @Override |
| public String toString() { |
| // This is expensive, but CallStat.toString() is only used for debugging. |
| String methodName = new BinderTransactionNameResolver().getMethodName(binderClass, |
| transactionCode); |
| return "CallStat{" |
| + "callingUid=" + callingUid |
| + ", transaction=" + binderClass.getSimpleName() + '.' + methodName |
| + ", callCount=" + callCount |
| + ", incrementalCallCount=" + incrementalCallCount |
| + ", recordedCallCount=" + recordedCallCount |
| + ", cpuTimeMicros=" + cpuTimeMicros |
| + ", latencyMicros=" + latencyMicros |
| + '}'; |
| } |
| } |
| |
| /** Key used to store CallStat object in a Map. */ |
| public static class CallStatKey { |
| public int callingUid; |
| public Class<? extends Binder> binderClass; |
| public int transactionCode; |
| private boolean screenInteractive; |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| |
| final CallStatKey key = (CallStatKey) o; |
| return callingUid == key.callingUid |
| && transactionCode == key.transactionCode |
| && screenInteractive == key.screenInteractive |
| && (binderClass.equals(key.binderClass)); |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = binderClass.hashCode(); |
| result = 31 * result + transactionCode; |
| result = 31 * result + callingUid; |
| result = 31 * result + (screenInteractive ? 1231 : 1237); |
| return result; |
| } |
| } |
| |
| |
| @VisibleForTesting |
| public static class UidEntry { |
| // The UID who is responsible for the binder transaction. If the bluetooth process execute a |
| // transaction on behalf of app foo, the workSourceUid will be the uid of app foo. |
| public int workSourceUid; |
| // Number of calls for which we collected data for. We do not record data for all the calls |
| // when sampling is on. |
| public long recordedCallCount; |
| // Real number of total calls. |
| public long callCount; |
| // Total CPU of all for all the recorded calls. |
| // Approximate total CPU usage can be computed by |
| // cpuTimeMicros * callCount / recordedCallCount |
| public long cpuTimeMicros; |
| // Call count that gets reset after delivery to BatteryStats |
| public long incrementalCallCount; |
| // Indicates that all transactions for the UID must be tracked |
| public boolean recordAllTransactions; |
| |
| UidEntry(int uid) { |
| this.workSourceUid = uid; |
| } |
| |
| // Aggregate time spent per each call name: call_desc -> cpu_time_micros |
| private ArrayMap<CallStatKey, CallStat> mCallStats = new ArrayMap<>(); |
| private CallStatKey mTempKey = new CallStatKey(); |
| |
| @Nullable |
| CallStat get(int callingUid, Class<? extends Binder> binderClass, int transactionCode, |
| boolean screenInteractive) { |
| // Use a global temporary key to avoid creating new objects for every lookup. |
| mTempKey.callingUid = callingUid; |
| mTempKey.binderClass = binderClass; |
| mTempKey.transactionCode = transactionCode; |
| mTempKey.screenInteractive = screenInteractive; |
| return mCallStats.get(mTempKey); |
| } |
| |
| CallStat getOrCreate(int callingUid, Class<? extends Binder> binderClass, |
| int transactionCode, boolean screenInteractive, boolean maxCallStatsReached) { |
| CallStat mapCallStat = get(callingUid, binderClass, transactionCode, screenInteractive); |
| // Only create CallStat if it's a new entry, otherwise update existing instance. |
| if (mapCallStat == null) { |
| if (maxCallStatsReached) { |
| mapCallStat = get(OVERFLOW_DIRECT_CALLING_UID, OVERFLOW_BINDER, |
| OVERFLOW_TRANSACTION_CODE, OVERFLOW_SCREEN_INTERACTIVE); |
| if (mapCallStat != null) { |
| return mapCallStat; |
| } |
| |
| callingUid = OVERFLOW_DIRECT_CALLING_UID; |
| binderClass = OVERFLOW_BINDER; |
| transactionCode = OVERFLOW_TRANSACTION_CODE; |
| screenInteractive = OVERFLOW_SCREEN_INTERACTIVE; |
| } |
| |
| mapCallStat = new CallStat(callingUid, binderClass, transactionCode, |
| screenInteractive); |
| CallStatKey key = new CallStatKey(); |
| key.callingUid = callingUid; |
| key.binderClass = binderClass; |
| key.transactionCode = transactionCode; |
| key.screenInteractive = screenInteractive; |
| mCallStats.put(key, mapCallStat); |
| } |
| return mapCallStat; |
| } |
| |
| /** |
| * Returns list of calls sorted by CPU time |
| */ |
| public Collection<CallStat> getCallStatsList() { |
| return mCallStats.values(); |
| } |
| |
| @Override |
| public String toString() { |
| return "UidEntry{" + |
| "cpuTimeMicros=" + cpuTimeMicros + |
| ", callCount=" + callCount + |
| ", mCallStats=" + mCallStats + |
| '}'; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| |
| UidEntry uidEntry = (UidEntry) o; |
| return workSourceUid == uidEntry.workSourceUid; |
| } |
| |
| @Override |
| public int hashCode() { |
| return workSourceUid; |
| } |
| } |
| |
| @VisibleForTesting |
| public SparseArray<UidEntry> getUidEntries() { |
| return mUidEntries; |
| } |
| |
| @VisibleForTesting |
| public ArrayMap<String, Integer> getExceptionCounts() { |
| return mExceptionCounts; |
| } |
| |
| public BinderLatencyObserver getLatencyObserver() { |
| return mLatencyObserver; |
| } |
| |
| @VisibleForTesting |
| public static <T> List<T> getHighestValues(List<T> list, ToDoubleFunction<T> toDouble, |
| double percentile) { |
| List<T> sortedList = new ArrayList<>(list); |
| sortedList.sort(Comparator.comparingDouble(toDouble).reversed()); |
| double total = 0; |
| for (T item : list) { |
| total += toDouble.applyAsDouble(item); |
| } |
| List<T> result = new ArrayList<>(); |
| double runningSum = 0; |
| for (T item : sortedList) { |
| if (runningSum > percentile * total) { |
| break; |
| } |
| result.add(item); |
| runningSum += toDouble.applyAsDouble(item); |
| } |
| return result; |
| } |
| |
| private static int compareByCpuDesc( |
| ExportedCallStat a, ExportedCallStat b) { |
| return Long.compare(b.cpuTimeMicros, a.cpuTimeMicros); |
| } |
| |
| private static int compareByBinderClassAndCode( |
| ExportedCallStat a, ExportedCallStat b) { |
| int result = a.className.compareTo(b.className); |
| return result != 0 |
| ? result |
| : Integer.compare(a.transactionCode, b.transactionCode); |
| } |
| |
| |
| /** |
| * Settings observer for other processes (not system_server). |
| * |
| * We do not want to collect cpu data from other processes so only latency collection should be |
| * possible to enable. |
| */ |
| public static class SettingsObserver extends ContentObserver { |
| // Settings for BinderCallsStats. |
| public static final String SETTINGS_ENABLED_KEY = "enabled"; |
| public static final String SETTINGS_DETAILED_TRACKING_KEY = "detailed_tracking"; |
| public static final String SETTINGS_UPLOAD_DATA_KEY = "upload_data"; |
| public static final String SETTINGS_SAMPLING_INTERVAL_KEY = "sampling_interval"; |
| public static final String SETTINGS_TRACK_SCREEN_INTERACTIVE_KEY = "track_screen_state"; |
| public static final String SETTINGS_TRACK_DIRECT_CALLING_UID_KEY = "track_calling_uid"; |
| public static final String SETTINGS_MAX_CALL_STATS_KEY = "max_call_stats_count"; |
| public static final String SETTINGS_IGNORE_BATTERY_STATUS_KEY = "ignore_battery_status"; |
| public static final String SETTINGS_SHARDING_MODULO_KEY = "sharding_modulo"; |
| // Settings for BinderLatencyObserver. |
| public static final String SETTINGS_COLLECT_LATENCY_DATA_KEY = "collect_latency_data"; |
| public static final String SETTINGS_LATENCY_OBSERVER_SAMPLING_INTERVAL_KEY = |
| "latency_observer_sampling_interval"; |
| public static final String SETTINGS_LATENCY_OBSERVER_SHARDING_MODULO_KEY = |
| "latency_observer_sharding_modulo"; |
| public static final String SETTINGS_LATENCY_OBSERVER_PUSH_INTERVAL_MINUTES_KEY = |
| "latency_observer_push_interval_minutes"; |
| public static final String SETTINGS_LATENCY_HISTOGRAM_BUCKET_COUNT_KEY = |
| "latency_histogram_bucket_count"; |
| public static final String SETTINGS_LATENCY_HISTOGRAM_FIRST_BUCKET_SIZE_KEY = |
| "latency_histogram_first_bucket_size"; |
| public static final String SETTINGS_LATENCY_HISTOGRAM_BUCKET_SCALE_FACTOR_KEY = |
| "latency_histogram_bucket_scale_factor"; |
| |
| private boolean mEnabled; |
| private final Uri mUri = Settings.Global.getUriFor(Settings.Global.BINDER_CALLS_STATS); |
| private final Context mContext; |
| private final KeyValueListParser mParser = new KeyValueListParser(','); |
| private final BinderCallsStats mBinderCallsStats; |
| |
| public SettingsObserver(Context context, BinderCallsStats binderCallsStats) { |
| super(BackgroundThread.getHandler()); |
| mContext = context; |
| context.getContentResolver().registerContentObserver(mUri, false, this); |
| mBinderCallsStats = binderCallsStats; |
| // Always kick once to ensure that we match current state |
| onChange(); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange, Uri uri, int userId) { |
| if (mUri.equals(uri)) { |
| onChange(); |
| } |
| } |
| |
| void onChange() { |
| try { |
| mParser.setString(Settings.Global.getString(mContext.getContentResolver(), |
| Settings.Global.BINDER_CALLS_STATS)); |
| } catch (IllegalArgumentException e) { |
| Slog.e(TAG, "Bad binder call stats settings", e); |
| } |
| |
| // Cpu data collection should always be disabled for other processes. |
| mBinderCallsStats.setDetailedTracking(false); |
| mBinderCallsStats.setTrackScreenInteractive(false); |
| mBinderCallsStats.setTrackDirectCallerUid(false); |
| |
| mBinderCallsStats.setIgnoreBatteryStatus( |
| mParser.getBoolean(SETTINGS_IGNORE_BATTERY_STATUS_KEY, |
| BinderCallsStats.DEFAULT_IGNORE_BATTERY_STATUS)); |
| mBinderCallsStats.setCollectLatencyData( |
| mParser.getBoolean(SETTINGS_COLLECT_LATENCY_DATA_KEY, |
| BinderCallsStats.DEFAULT_COLLECT_LATENCY_DATA)); |
| |
| // Binder latency observer settings. |
| configureLatencyObserver(mParser, mBinderCallsStats.getLatencyObserver()); |
| |
| final boolean enabled = |
| mParser.getBoolean(SETTINGS_ENABLED_KEY, BinderCallsStats.ENABLED_DEFAULT); |
| if (mEnabled != enabled) { |
| if (enabled) { |
| Binder.setObserver(mBinderCallsStats); |
| } else { |
| Binder.setObserver(null); |
| } |
| mEnabled = enabled; |
| mBinderCallsStats.reset(); |
| mBinderCallsStats.setAddDebugEntries(enabled); |
| mBinderCallsStats.getLatencyObserver().reset(); |
| } |
| } |
| |
| /** Configures the binder latency observer related settings. */ |
| public static void configureLatencyObserver( |
| KeyValueListParser mParser, BinderLatencyObserver binderLatencyObserver) { |
| binderLatencyObserver.setSamplingInterval(mParser.getInt( |
| SETTINGS_LATENCY_OBSERVER_SAMPLING_INTERVAL_KEY, |
| BinderLatencyObserver.PERIODIC_SAMPLING_INTERVAL_DEFAULT)); |
| binderLatencyObserver.setShardingModulo(mParser.getInt( |
| SETTINGS_LATENCY_OBSERVER_SHARDING_MODULO_KEY, |
| BinderLatencyObserver.SHARDING_MODULO_DEFAULT)); |
| binderLatencyObserver.setHistogramBucketsParams( |
| mParser.getInt( |
| SETTINGS_LATENCY_HISTOGRAM_BUCKET_COUNT_KEY, |
| BinderLatencyObserver.BUCKET_COUNT_DEFAULT), |
| mParser.getInt( |
| SETTINGS_LATENCY_HISTOGRAM_FIRST_BUCKET_SIZE_KEY, |
| BinderLatencyObserver.FIRST_BUCKET_SIZE_DEFAULT), |
| mParser.getFloat( |
| SETTINGS_LATENCY_HISTOGRAM_BUCKET_SCALE_FACTOR_KEY, |
| BinderLatencyObserver.BUCKET_SCALE_FACTOR_DEFAULT)); |
| binderLatencyObserver.setPushInterval(mParser.getInt( |
| SETTINGS_LATENCY_OBSERVER_PUSH_INTERVAL_MINUTES_KEY, |
| BinderLatencyObserver.STATSD_PUSH_INTERVAL_MINUTES_DEFAULT)); |
| } |
| } |
| } |