• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.internal.os;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.Handler;
22 import android.os.Looper;
23 import android.os.Message;
24 import android.os.SystemClock;
25 import android.util.SparseArray;
26 
27 import com.android.internal.annotations.GuardedBy;
28 
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.concurrent.ConcurrentLinkedQueue;
32 import java.util.concurrent.ThreadLocalRandom;
33 
34 /**
35  * Collects aggregated telemetry data about Looper message dispatching.
36  *
37  * @hide Only for use within the system server.
38  */
39 @android.ravenwood.annotation.RavenwoodKeepWholeClass
40 public class LooperStats implements Looper.Observer {
41     public static final String DEBUG_ENTRY_PREFIX = "__DEBUG_";
42     private static final int SESSION_POOL_SIZE = 50;
43     private static final boolean DISABLED_SCREEN_STATE_TRACKING_VALUE = false;
44     public static final boolean DEFAULT_IGNORE_BATTERY_STATUS = false;
45 
46     @GuardedBy("mLock")
47     private final SparseArray<Entry> mEntries = new SparseArray<>(512);
48     private final Object mLock = new Object();
49     private final Entry mOverflowEntry = new Entry("OVERFLOW");
50     private final Entry mHashCollisionEntry = new Entry("HASH_COLLISION");
51     private final ConcurrentLinkedQueue<DispatchSession> mSessionPool =
52             new ConcurrentLinkedQueue<>();
53     private final int mEntriesSizeCap;
54     private int mSamplingInterval;
55     private CachedDeviceState.Readonly mDeviceState;
56     private CachedDeviceState.TimeInStateStopwatch mBatteryStopwatch;
57     private long mStartCurrentTime = System.currentTimeMillis();
58     private long mStartElapsedTime = SystemClock.elapsedRealtime();
59     private boolean mAddDebugEntries = false;
60     private boolean mTrackScreenInteractive = false;
61     private boolean mIgnoreBatteryStatus = DEFAULT_IGNORE_BATTERY_STATUS;
62 
LooperStats(int samplingInterval, int entriesSizeCap)63     public LooperStats(int samplingInterval, int entriesSizeCap) {
64         this.mSamplingInterval = samplingInterval;
65         this.mEntriesSizeCap = entriesSizeCap;
66     }
67 
setDeviceState(@onNull CachedDeviceState.Readonly deviceState)68     public void setDeviceState(@NonNull CachedDeviceState.Readonly deviceState) {
69         if (mBatteryStopwatch != null) {
70             mBatteryStopwatch.close();
71         }
72 
73         mDeviceState = deviceState;
74         mBatteryStopwatch = deviceState.createTimeOnBatteryStopwatch();
75     }
76 
setAddDebugEntries(boolean addDebugEntries)77     public void setAddDebugEntries(boolean addDebugEntries) {
78         mAddDebugEntries = addDebugEntries;
79     }
80 
81     @Override
messageDispatchStarting()82     public Object messageDispatchStarting() {
83         if (deviceStateAllowsCollection() && shouldCollectDetailedData()) {
84             DispatchSession session = mSessionPool.poll();
85             session = session == null ? new DispatchSession() : session;
86             session.startTimeMicro = getElapsedRealtimeMicro();
87             session.cpuStartMicro = getThreadTimeMicro();
88             session.systemUptimeMillis = getSystemUptimeMillis();
89             return session;
90         }
91 
92         return DispatchSession.NOT_SAMPLED;
93     }
94 
95     @Override
messageDispatched(Object token, Message msg)96     public void messageDispatched(Object token, Message msg) {
97         if (!deviceStateAllowsCollection()) {
98             return;
99         }
100 
101         DispatchSession session = (DispatchSession) token;
102         Entry entry = findEntry(msg, /* allowCreateNew= */session != DispatchSession.NOT_SAMPLED);
103         if (entry != null) {
104             synchronized (entry) {
105                 entry.messageCount++;
106                 if (session != DispatchSession.NOT_SAMPLED) {
107                     entry.recordedMessageCount++;
108                     final long latency = getElapsedRealtimeMicro() - session.startTimeMicro;
109                     final long cpuUsage = getThreadTimeMicro() - session.cpuStartMicro;
110                     entry.totalLatencyMicro += latency;
111                     entry.maxLatencyMicro = Math.max(entry.maxLatencyMicro, latency);
112                     entry.cpuUsageMicro += cpuUsage;
113                     entry.maxCpuUsageMicro = Math.max(entry.maxCpuUsageMicro, cpuUsage);
114                     if (msg.getWhen() > 0) {
115                         final long delay = Math.max(0L, session.systemUptimeMillis - msg.getWhen());
116                         entry.delayMillis += delay;
117                         entry.maxDelayMillis = Math.max(entry.maxDelayMillis, delay);
118                         entry.recordedDelayMessageCount++;
119                     }
120                 }
121             }
122         }
123 
124         recycleSession(session);
125     }
126 
127     @Override
dispatchingThrewException(Object token, Message msg, Exception exception)128     public void dispatchingThrewException(Object token, Message msg, Exception exception) {
129         if (!deviceStateAllowsCollection()) {
130             return;
131         }
132 
133         DispatchSession session = (DispatchSession) token;
134         Entry entry = findEntry(msg, /* allowCreateNew= */session != DispatchSession.NOT_SAMPLED);
135         if (entry != null) {
136             synchronized (entry) {
137                 entry.exceptionCount++;
138             }
139         }
140 
141         recycleSession(session);
142     }
143 
deviceStateAllowsCollection()144     private boolean deviceStateAllowsCollection() {
145         if (mIgnoreBatteryStatus) {
146             return true;
147         }
148         if (mDeviceState == null) {
149             return false;
150         }
151         if (mDeviceState.isCharging()) {
152             return false;
153         }
154         return true;
155     }
156 
157     /** Returns an array of {@link ExportedEntry entries} with the aggregated statistics. */
getEntries()158     public List<ExportedEntry> getEntries() {
159         final ArrayList<ExportedEntry> exportedEntries;
160         synchronized (mLock) {
161             final int size = mEntries.size();
162             exportedEntries = new ArrayList<>(size);
163             for (int i = 0; i < size; i++) {
164                 Entry entry = mEntries.valueAt(i);
165                 synchronized (entry) {
166                     exportedEntries.add(new ExportedEntry(entry));
167                 }
168             }
169         }
170         // Add the overflow and collision entries only if they have any data.
171         maybeAddSpecialEntry(exportedEntries, mOverflowEntry);
172         maybeAddSpecialEntry(exportedEntries, mHashCollisionEntry);
173         // Debug entries added to help validate the data.
174         if (mAddDebugEntries && mBatteryStopwatch != null) {
175             exportedEntries.add(createDebugEntry("start_time_millis", mStartElapsedTime));
176             exportedEntries.add(createDebugEntry("end_time_millis", SystemClock.elapsedRealtime()));
177             exportedEntries.add(
178                     createDebugEntry("battery_time_millis", mBatteryStopwatch.getMillis()));
179             exportedEntries.add(createDebugEntry("sampling_interval", mSamplingInterval));
180         }
181         return exportedEntries;
182     }
183 
createDebugEntry(String variableName, long value)184     private ExportedEntry createDebugEntry(String variableName, long value) {
185         final Entry entry = new Entry(DEBUG_ENTRY_PREFIX + variableName);
186         entry.messageCount = 1;
187         entry.recordedMessageCount = 1;
188         entry.totalLatencyMicro = value;
189         return new ExportedEntry(entry);
190     }
191 
192     /** Returns a timestamp indicating when the statistics were last reset. */
getStartTimeMillis()193     public long getStartTimeMillis() {
194         return mStartCurrentTime;
195     }
196 
getStartElapsedTimeMillis()197     public long getStartElapsedTimeMillis() {
198         return mStartElapsedTime;
199     }
200 
getBatteryTimeMillis()201     public long getBatteryTimeMillis() {
202         return mBatteryStopwatch != null ? mBatteryStopwatch.getMillis() : 0;
203     }
204 
maybeAddSpecialEntry(List<ExportedEntry> exportedEntries, Entry specialEntry)205     private void maybeAddSpecialEntry(List<ExportedEntry> exportedEntries, Entry specialEntry) {
206         synchronized (specialEntry) {
207             if (specialEntry.messageCount > 0 || specialEntry.exceptionCount > 0) {
208                 exportedEntries.add(new ExportedEntry(specialEntry));
209             }
210         }
211     }
212 
213     /** Removes all collected data. */
reset()214     public void reset() {
215         synchronized (mLock) {
216             mEntries.clear();
217         }
218         synchronized (mHashCollisionEntry) {
219             mHashCollisionEntry.reset();
220         }
221         synchronized (mOverflowEntry) {
222             mOverflowEntry.reset();
223         }
224         mStartCurrentTime = System.currentTimeMillis();
225         mStartElapsedTime = SystemClock.elapsedRealtime();
226         if (mBatteryStopwatch != null) {
227             mBatteryStopwatch.reset();
228         }
229     }
230 
setSamplingInterval(int samplingInterval)231     public void setSamplingInterval(int samplingInterval) {
232         mSamplingInterval = samplingInterval;
233     }
234 
setTrackScreenInteractive(boolean enabled)235     public void setTrackScreenInteractive(boolean enabled) {
236         mTrackScreenInteractive = enabled;
237     }
238 
setIgnoreBatteryStatus(boolean ignore)239     public void setIgnoreBatteryStatus(boolean ignore) {
240         mIgnoreBatteryStatus = ignore;
241     }
242 
243     @Nullable
findEntry(Message msg, boolean allowCreateNew)244     private Entry findEntry(Message msg, boolean allowCreateNew) {
245         final boolean isInteractive = mTrackScreenInteractive
246                 ? mDeviceState.isScreenInteractive()
247                 : DISABLED_SCREEN_STATE_TRACKING_VALUE;
248         final int id = Entry.idFor(msg, isInteractive);
249         Entry entry;
250         synchronized (mLock) {
251             entry = mEntries.get(id);
252             if (entry == null) {
253                 if (!allowCreateNew) {
254                     return null;
255                 } else if (mEntries.size() >= mEntriesSizeCap) {
256                     // If over the size cap track totals under OVERFLOW entry.
257                     return mOverflowEntry;
258                 } else {
259                     entry = new Entry(msg, isInteractive);
260                     mEntries.put(id, entry);
261                 }
262             }
263         }
264 
265         if (entry.workSourceUid != msg.workSourceUid
266                 || entry.handler.getClass() != msg.getTarget().getClass()
267                 || entry.handler.getLooper().getThread() != msg.getTarget().getLooper().getThread()
268                 || entry.isInteractive != isInteractive) {
269             // If a hash collision happened, track totals under a single entry.
270             return mHashCollisionEntry;
271         }
272         return entry;
273     }
274 
recycleSession(DispatchSession session)275     private void recycleSession(DispatchSession session) {
276         if (session != DispatchSession.NOT_SAMPLED && mSessionPool.size() < SESSION_POOL_SIZE) {
277             mSessionPool.add(session);
278         }
279     }
280 
getThreadTimeMicro()281     protected long getThreadTimeMicro() {
282         return SystemClock.currentThreadTimeMicro();
283     }
284 
getElapsedRealtimeMicro()285     protected long getElapsedRealtimeMicro() {
286         return SystemClock.elapsedRealtimeNanos() / 1000;
287     }
288 
getSystemUptimeMillis()289     protected long getSystemUptimeMillis() {
290         return SystemClock.uptimeMillis();
291     }
292 
shouldCollectDetailedData()293     protected boolean shouldCollectDetailedData() {
294         return ThreadLocalRandom.current().nextInt(mSamplingInterval) == 0;
295     }
296 
297     private static class DispatchSession {
298         static final DispatchSession NOT_SAMPLED = new DispatchSession();
299         public long startTimeMicro;
300         public long cpuStartMicro;
301         public long systemUptimeMillis;
302     }
303 
304     private static class Entry {
305         public final int workSourceUid;
306         public final Handler handler;
307         public final String messageName;
308         public final boolean isInteractive;
309         public long messageCount;
310         public long recordedMessageCount;
311         public long exceptionCount;
312         public long totalLatencyMicro;
313         public long maxLatencyMicro;
314         public long cpuUsageMicro;
315         public long maxCpuUsageMicro;
316         public long recordedDelayMessageCount;
317         public long delayMillis;
318         public long maxDelayMillis;
319 
Entry(Message msg, boolean isInteractive)320         Entry(Message msg, boolean isInteractive) {
321             this.workSourceUid = msg.workSourceUid;
322             this.handler = msg.getTarget();
323             this.messageName = handler.getMessageName(msg);
324             this.isInteractive = isInteractive;
325         }
326 
Entry(String specialEntryName)327         Entry(String specialEntryName) {
328             this.workSourceUid = Message.UID_NONE;
329             this.messageName = specialEntryName;
330             this.handler = null;
331             this.isInteractive = false;
332         }
333 
reset()334         void reset() {
335             messageCount = 0;
336             recordedMessageCount = 0;
337             exceptionCount = 0;
338             totalLatencyMicro = 0;
339             maxLatencyMicro = 0;
340             cpuUsageMicro = 0;
341             maxCpuUsageMicro = 0;
342             delayMillis = 0;
343             maxDelayMillis = 0;
344             recordedDelayMessageCount = 0;
345         }
346 
idFor(Message msg, boolean isInteractive)347         static int idFor(Message msg, boolean isInteractive) {
348             int result = 7;
349             result = 31 * result + msg.workSourceUid;
350             result = 31 * result + msg.getTarget().getLooper().getThread().hashCode();
351             result = 31 * result + msg.getTarget().getClass().hashCode();
352             result = 31 * result + (isInteractive ? 1231 : 1237);
353             if (msg.getCallback() != null) {
354                 return 31 * result + msg.getCallback().getClass().hashCode();
355             } else {
356                 return 31 * result + msg.what;
357             }
358         }
359     }
360 
361     /** Aggregated data of Looper message dispatching in the in the current process. */
362     public static class ExportedEntry {
363         public final int workSourceUid;
364         public final String handlerClassName;
365         public final String threadName;
366         public final String messageName;
367         public final boolean isInteractive;
368         public final long messageCount;
369         public final long recordedMessageCount;
370         public final long exceptionCount;
371         public final long totalLatencyMicros;
372         public final long maxLatencyMicros;
373         public final long cpuUsageMicros;
374         public final long maxCpuUsageMicros;
375         public final long maxDelayMillis;
376         public final long delayMillis;
377         public final long recordedDelayMessageCount;
378 
ExportedEntry(Entry entry)379         ExportedEntry(Entry entry) {
380             this.workSourceUid = entry.workSourceUid;
381             if (entry.handler != null) {
382                 this.handlerClassName = entry.handler.getClass().getName();
383                 this.threadName = entry.handler.getLooper().getThread().getName();
384             } else {
385                 // Overflow/collision entries do not have a handler set.
386                 this.handlerClassName = "";
387                 this.threadName = "";
388             }
389             this.isInteractive = entry.isInteractive;
390             this.messageName = entry.messageName;
391             this.messageCount = entry.messageCount;
392             this.recordedMessageCount = entry.recordedMessageCount;
393             this.exceptionCount = entry.exceptionCount;
394             this.totalLatencyMicros = entry.totalLatencyMicro;
395             this.maxLatencyMicros = entry.maxLatencyMicro;
396             this.cpuUsageMicros = entry.cpuUsageMicro;
397             this.maxCpuUsageMicros = entry.maxCpuUsageMicro;
398             this.delayMillis = entry.delayMillis;
399             this.maxDelayMillis = entry.maxDelayMillis;
400             this.recordedDelayMessageCount = entry.recordedDelayMessageCount;
401         }
402     }
403 }
404