blob: 932ff572219f69cc73b9d07e4675734957b23689 [file] [log] [blame]
/*
* Copyright (C) 2018 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 android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ThreadLocalRandom;
/**
* Collects aggregated telemetry data about Looper message dispatching.
*
* @hide Only for use within the system server.
*/
public class LooperStats implements Looper.Observer {
public static final String DEBUG_ENTRY_PREFIX = "__DEBUG_";
private static final int SESSION_POOL_SIZE = 50;
private static final boolean DISABLED_SCREEN_STATE_TRACKING_VALUE = false;
@GuardedBy("mLock")
private final SparseArray<Entry> mEntries = new SparseArray<>(512);
private final Object mLock = new Object();
private final Entry mOverflowEntry = new Entry("OVERFLOW");
private final Entry mHashCollisionEntry = new Entry("HASH_COLLISION");
private final ConcurrentLinkedQueue<DispatchSession> mSessionPool =
new ConcurrentLinkedQueue<>();
private final int mEntriesSizeCap;
private int mSamplingInterval;
private CachedDeviceState.Readonly mDeviceState;
private CachedDeviceState.TimeInStateStopwatch mBatteryStopwatch;
private long mStartCurrentTime = System.currentTimeMillis();
private long mStartElapsedTime = SystemClock.elapsedRealtime();
private boolean mAddDebugEntries = false;
private boolean mTrackScreenInteractive = false;
public LooperStats(int samplingInterval, int entriesSizeCap) {
this.mSamplingInterval = samplingInterval;
this.mEntriesSizeCap = entriesSizeCap;
}
public void setDeviceState(@NonNull CachedDeviceState.Readonly deviceState) {
if (mBatteryStopwatch != null) {
mBatteryStopwatch.close();
}
mDeviceState = deviceState;
mBatteryStopwatch = deviceState.createTimeOnBatteryStopwatch();
}
public void setAddDebugEntries(boolean addDebugEntries) {
mAddDebugEntries = addDebugEntries;
}
@Override
public Object messageDispatchStarting() {
if (deviceStateAllowsCollection() && shouldCollectDetailedData()) {
DispatchSession session = mSessionPool.poll();
session = session == null ? new DispatchSession() : session;
session.startTimeMicro = getElapsedRealtimeMicro();
session.cpuStartMicro = getThreadTimeMicro();
session.systemUptimeMillis = getSystemUptimeMillis();
return session;
}
return DispatchSession.NOT_SAMPLED;
}
@Override
public void messageDispatched(Object token, Message msg) {
if (!deviceStateAllowsCollection()) {
return;
}
DispatchSession session = (DispatchSession) token;
Entry entry = findEntry(msg, /* allowCreateNew= */session != DispatchSession.NOT_SAMPLED);
if (entry != null) {
synchronized (entry) {
entry.messageCount++;
if (session != DispatchSession.NOT_SAMPLED) {
entry.recordedMessageCount++;
final long latency = getElapsedRealtimeMicro() - session.startTimeMicro;
final long cpuUsage = getThreadTimeMicro() - session.cpuStartMicro;
entry.totalLatencyMicro += latency;
entry.maxLatencyMicro = Math.max(entry.maxLatencyMicro, latency);
entry.cpuUsageMicro += cpuUsage;
entry.maxCpuUsageMicro = Math.max(entry.maxCpuUsageMicro, cpuUsage);
if (msg.getWhen() > 0) {
final long delay = Math.max(0L, session.systemUptimeMillis - msg.getWhen());
entry.delayMillis += delay;
entry.maxDelayMillis = Math.max(entry.maxDelayMillis, delay);
entry.recordedDelayMessageCount++;
}
}
}
}
recycleSession(session);
}
@Override
public void dispatchingThrewException(Object token, Message msg, Exception exception) {
if (!deviceStateAllowsCollection()) {
return;
}
DispatchSession session = (DispatchSession) token;
Entry entry = findEntry(msg, /* allowCreateNew= */session != DispatchSession.NOT_SAMPLED);
if (entry != null) {
synchronized (entry) {
entry.exceptionCount++;
}
}
recycleSession(session);
}
private boolean deviceStateAllowsCollection() {
// Do not collect data if on charger or the state is not set.
return mDeviceState != null && !mDeviceState.isCharging();
}
/** Returns an array of {@link ExportedEntry entries} with the aggregated statistics. */
public List<ExportedEntry> getEntries() {
final ArrayList<ExportedEntry> exportedEntries;
synchronized (mLock) {
final int size = mEntries.size();
exportedEntries = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
Entry entry = mEntries.valueAt(i);
synchronized (entry) {
exportedEntries.add(new ExportedEntry(entry));
}
}
}
// Add the overflow and collision entries only if they have any data.
maybeAddSpecialEntry(exportedEntries, mOverflowEntry);
maybeAddSpecialEntry(exportedEntries, mHashCollisionEntry);
// Debug entries added to help validate the data.
if (mAddDebugEntries && mBatteryStopwatch != null) {
exportedEntries.add(createDebugEntry("start_time_millis", mStartElapsedTime));
exportedEntries.add(createDebugEntry("end_time_millis", SystemClock.elapsedRealtime()));
exportedEntries.add(
createDebugEntry("battery_time_millis", mBatteryStopwatch.getMillis()));
exportedEntries.add(createDebugEntry("sampling_interval", mSamplingInterval));
}
return exportedEntries;
}
private ExportedEntry createDebugEntry(String variableName, long value) {
final Entry entry = new Entry(DEBUG_ENTRY_PREFIX + variableName);
entry.messageCount = 1;
entry.recordedMessageCount = 1;
entry.totalLatencyMicro = value;
return new ExportedEntry(entry);
}
/** Returns a timestamp indicating when the statistics were last reset. */
public long getStartTimeMillis() {
return mStartCurrentTime;
}
public long getStartElapsedTimeMillis() {
return mStartElapsedTime;
}
public long getBatteryTimeMillis() {
return mBatteryStopwatch != null ? mBatteryStopwatch.getMillis() : 0;
}
private void maybeAddSpecialEntry(List<ExportedEntry> exportedEntries, Entry specialEntry) {
synchronized (specialEntry) {
if (specialEntry.messageCount > 0 || specialEntry.exceptionCount > 0) {
exportedEntries.add(new ExportedEntry(specialEntry));
}
}
}
/** Removes all collected data. */
public void reset() {
synchronized (mLock) {
mEntries.clear();
}
synchronized (mHashCollisionEntry) {
mHashCollisionEntry.reset();
}
synchronized (mOverflowEntry) {
mOverflowEntry.reset();
}
mStartCurrentTime = System.currentTimeMillis();
mStartElapsedTime = SystemClock.elapsedRealtime();
if (mBatteryStopwatch != null) {
mBatteryStopwatch.reset();
}
}
public void setSamplingInterval(int samplingInterval) {
mSamplingInterval = samplingInterval;
}
public void setTrackScreenInteractive(boolean enabled) {
mTrackScreenInteractive = enabled;
}
@Nullable
private Entry findEntry(Message msg, boolean allowCreateNew) {
final boolean isInteractive = mTrackScreenInteractive
? mDeviceState.isScreenInteractive()
: DISABLED_SCREEN_STATE_TRACKING_VALUE;
final int id = Entry.idFor(msg, isInteractive);
Entry entry;
synchronized (mLock) {
entry = mEntries.get(id);
if (entry == null) {
if (!allowCreateNew) {
return null;
} else if (mEntries.size() >= mEntriesSizeCap) {
// If over the size cap track totals under OVERFLOW entry.
return mOverflowEntry;
} else {
entry = new Entry(msg, isInteractive);
mEntries.put(id, entry);
}
}
}
if (entry.workSourceUid != msg.workSourceUid
|| entry.handler.getClass() != msg.getTarget().getClass()
|| entry.handler.getLooper().getThread() != msg.getTarget().getLooper().getThread()
|| entry.isInteractive != isInteractive) {
// If a hash collision happened, track totals under a single entry.
return mHashCollisionEntry;
}
return entry;
}
private void recycleSession(DispatchSession session) {
if (session != DispatchSession.NOT_SAMPLED && mSessionPool.size() < SESSION_POOL_SIZE) {
mSessionPool.add(session);
}
}
protected long getThreadTimeMicro() {
return SystemClock.currentThreadTimeMicro();
}
protected long getElapsedRealtimeMicro() {
return SystemClock.elapsedRealtimeNanos() / 1000;
}
protected long getSystemUptimeMillis() {
return SystemClock.uptimeMillis();
}
protected boolean shouldCollectDetailedData() {
return ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
}
private static class DispatchSession {
static final DispatchSession NOT_SAMPLED = new DispatchSession();
public long startTimeMicro;
public long cpuStartMicro;
public long systemUptimeMillis;
}
private static class Entry {
public final int workSourceUid;
public final Handler handler;
public final String messageName;
public final boolean isInteractive;
public long messageCount;
public long recordedMessageCount;
public long exceptionCount;
public long totalLatencyMicro;
public long maxLatencyMicro;
public long cpuUsageMicro;
public long maxCpuUsageMicro;
public long recordedDelayMessageCount;
public long delayMillis;
public long maxDelayMillis;
Entry(Message msg, boolean isInteractive) {
this.workSourceUid = msg.workSourceUid;
this.handler = msg.getTarget();
this.messageName = handler.getMessageName(msg);
this.isInteractive = isInteractive;
}
Entry(String specialEntryName) {
this.workSourceUid = Message.UID_NONE;
this.messageName = specialEntryName;
this.handler = null;
this.isInteractive = false;
}
void reset() {
messageCount = 0;
recordedMessageCount = 0;
exceptionCount = 0;
totalLatencyMicro = 0;
maxLatencyMicro = 0;
cpuUsageMicro = 0;
maxCpuUsageMicro = 0;
delayMillis = 0;
maxDelayMillis = 0;
recordedDelayMessageCount = 0;
}
static int idFor(Message msg, boolean isInteractive) {
int result = 7;
result = 31 * result + msg.workSourceUid;
result = 31 * result + msg.getTarget().getLooper().getThread().hashCode();
result = 31 * result + msg.getTarget().getClass().hashCode();
result = 31 * result + (isInteractive ? 1231 : 1237);
if (msg.getCallback() != null) {
return 31 * result + msg.getCallback().getClass().hashCode();
} else {
return 31 * result + msg.what;
}
}
}
/** Aggregated data of Looper message dispatching in the in the current process. */
public static class ExportedEntry {
public final int workSourceUid;
public final String handlerClassName;
public final String threadName;
public final String messageName;
public final boolean isInteractive;
public final long messageCount;
public final long recordedMessageCount;
public final long exceptionCount;
public final long totalLatencyMicros;
public final long maxLatencyMicros;
public final long cpuUsageMicros;
public final long maxCpuUsageMicros;
public final long maxDelayMillis;
public final long delayMillis;
public final long recordedDelayMessageCount;
ExportedEntry(Entry entry) {
this.workSourceUid = entry.workSourceUid;
if (entry.handler != null) {
this.handlerClassName = entry.handler.getClass().getName();
this.threadName = entry.handler.getLooper().getThread().getName();
} else {
// Overflow/collision entries do not have a handler set.
this.handlerClassName = "";
this.threadName = "";
}
this.isInteractive = entry.isInteractive;
this.messageName = entry.messageName;
this.messageCount = entry.messageCount;
this.recordedMessageCount = entry.recordedMessageCount;
this.exceptionCount = entry.exceptionCount;
this.totalLatencyMicros = entry.totalLatencyMicro;
this.maxLatencyMicros = entry.maxLatencyMicro;
this.cpuUsageMicros = entry.cpuUsageMicro;
this.maxCpuUsageMicros = entry.maxCpuUsageMicro;
this.delayMillis = entry.delayMillis;
this.maxDelayMillis = entry.maxDelayMillis;
this.recordedDelayMessageCount = entry.recordedDelayMessageCount;
}
}
}