content: /*
   * 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;
          }
      }
  }
  