content: /*
   * Copyright (C) 2016 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.server.job;
  
  import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
  import static com.android.server.job.JobSchedulerService.sSystemClock;
  import static com.android.server.job.JobSchedulerService.sUptimeMillisClock;
  
  import android.app.job.JobInfo;
  import android.app.job.JobParameters;
  import android.os.UserHandle;
  import android.text.format.DateFormat;
  import android.util.ArrayMap;
  import android.util.SparseArray;
  import android.util.SparseIntArray;
  import android.util.TimeUtils;
  import android.util.proto.ProtoOutputStream;
  
  import com.android.internal.util.RingBufferIndices;
  import com.android.server.job.controllers.JobStatus;
  
  import java.io.PrintWriter;
  
  public final class JobPackageTracker {
      // We batch every 30 minutes.
      static final long BATCHING_TIME = 30*60*1000;
      // Number of historical data sets we keep.
      static final int NUM_HISTORY = 5;
  
      private static final int EVENT_BUFFER_SIZE = 100;
  
      public static final int EVENT_CMD_MASK = 0xff;
      public static final int EVENT_STOP_REASON_SHIFT = 8;
      public static final int EVENT_STOP_REASON_MASK = 0xff << EVENT_STOP_REASON_SHIFT;
      public static final int EVENT_NULL = 0;
      public static final int EVENT_START_JOB = 1;
      public static final int EVENT_STOP_JOB = 2;
      public static final int EVENT_START_PERIODIC_JOB = 3;
      public static final int EVENT_STOP_PERIODIC_JOB = 4;
  
      private final RingBufferIndices mEventIndices = new RingBufferIndices(EVENT_BUFFER_SIZE);
      private final int[] mEventCmds = new int[EVENT_BUFFER_SIZE];
      private final long[] mEventTimes = new long[EVENT_BUFFER_SIZE];
      private final int[] mEventUids = new int[EVENT_BUFFER_SIZE];
      private final String[] mEventTags = new String[EVENT_BUFFER_SIZE];
      private final int[] mEventJobIds = new int[EVENT_BUFFER_SIZE];
      private final String[] mEventReasons = new String[EVENT_BUFFER_SIZE];
  
      public void addEvent(int cmd, int uid, String tag, int jobId, int stopReason,
              String debugReason) {
          int index = mEventIndices.add();
          mEventCmds[index] = cmd | ((stopReason<<EVENT_STOP_REASON_SHIFT) & EVENT_STOP_REASON_MASK);
          mEventTimes[index] = sElapsedRealtimeClock.millis();
          mEventUids[index] = uid;
          mEventTags[index] = tag;
          mEventJobIds[index] = jobId;
          mEventReasons[index] = debugReason;
      }
  
      DataSet mCurDataSet = new DataSet();
      DataSet[] mLastDataSets = new DataSet[NUM_HISTORY];
  
      final static class PackageEntry {
          long pastActiveTime;
          long activeStartTime;
          int activeNesting;
          int activeCount;
          boolean hadActive;
          long pastActiveTopTime;
          long activeTopStartTime;
          int activeTopNesting;
          int activeTopCount;
          boolean hadActiveTop;
          long pastPendingTime;
          long pendingStartTime;
          int pendingNesting;
          int pendingCount;
          boolean hadPending;
          final SparseIntArray stopReasons = new SparseIntArray();
  
          public long getActiveTime(long now) {
              long time = pastActiveTime;
              if (activeNesting > 0) {
                  time += now - activeStartTime;
              }
              return time;
          }
  
          public long getActiveTopTime(long now) {
              long time = pastActiveTopTime;
              if (activeTopNesting > 0) {
                  time += now - activeTopStartTime;
              }
              return time;
          }
  
          public long getPendingTime(long now) {
              long time = pastPendingTime;
              if (pendingNesting > 0) {
                  time += now - pendingStartTime;
              }
              return time;
          }
      }
  
      final static class DataSet {
          final SparseArray<ArrayMap<String, PackageEntry>> mEntries = new SparseArray<>();
          final long mStartUptimeTime;
          final long mStartElapsedTime;
          final long mStartClockTime;
          long mSummedTime;
          int mMaxTotalActive;
          int mMaxFgActive;
  
          public DataSet(DataSet otherTimes) {
              mStartUptimeTime = otherTimes.mStartUptimeTime;
              mStartElapsedTime = otherTimes.mStartElapsedTime;
              mStartClockTime = otherTimes.mStartClockTime;
          }
  
          public DataSet() {
              mStartUptimeTime = sUptimeMillisClock.millis();
              mStartElapsedTime = sElapsedRealtimeClock.millis();
              mStartClockTime = sSystemClock.millis();
          }
  
          private PackageEntry getOrCreateEntry(int uid, String pkg) {
              ArrayMap<String, PackageEntry> uidMap = mEntries.get(uid);
              if (uidMap == null) {
                  uidMap = new ArrayMap<>();
                  mEntries.put(uid, uidMap);
              }
              PackageEntry entry = uidMap.get(pkg);
              if (entry == null) {
                  entry = new PackageEntry();
                  uidMap.put(pkg, entry);
              }
              return entry;
          }
  
          public PackageEntry getEntry(int uid, String pkg) {
              ArrayMap<String, PackageEntry> uidMap = mEntries.get(uid);
              if (uidMap == null) {
                  return null;
              }
              return uidMap.get(pkg);
          }
  
          long getTotalTime(long now) {
              if (mSummedTime > 0) {
                  return mSummedTime;
              }
              return now - mStartUptimeTime;
          }
  
          void incPending(int uid, String pkg, long now) {
              PackageEntry pe = getOrCreateEntry(uid, pkg);
              if (pe.pendingNesting == 0) {
                  pe.pendingStartTime = now;
                  pe.pendingCount++;
              }
              pe.pendingNesting++;
          }
  
          void decPending(int uid, String pkg, long now) {
              PackageEntry pe = getOrCreateEntry(uid, pkg);
              if (pe.pendingNesting == 1) {
                  pe.pastPendingTime += now - pe.pendingStartTime;
              }
              pe.pendingNesting--;
          }
  
          void incActive(int uid, String pkg, long now) {
              PackageEntry pe = getOrCreateEntry(uid, pkg);
              if (pe.activeNesting == 0) {
                  pe.activeStartTime = now;
                  pe.activeCount++;
              }
              pe.activeNesting++;
          }
  
          void decActive(int uid, String pkg, long now, int stopReason) {
              PackageEntry pe = getOrCreateEntry(uid, pkg);
              if (pe.activeNesting == 1) {
                  pe.pastActiveTime += now - pe.activeStartTime;
              }
              pe.activeNesting--;
              int count = pe.stopReasons.get(stopReason, 0);
              pe.stopReasons.put(stopReason, count+1);
          }
  
          void incActiveTop(int uid, String pkg, long now) {
              PackageEntry pe = getOrCreateEntry(uid, pkg);
              if (pe.activeTopNesting == 0) {
                  pe.activeTopStartTime = now;
                  pe.activeTopCount++;
              }
              pe.activeTopNesting++;
          }
  
          void decActiveTop(int uid, String pkg, long now, int stopReason) {
              PackageEntry pe = getOrCreateEntry(uid, pkg);
              if (pe.activeTopNesting == 1) {
                  pe.pastActiveTopTime += now - pe.activeTopStartTime;
              }
              pe.activeTopNesting--;
              int count = pe.stopReasons.get(stopReason, 0);
              pe.stopReasons.put(stopReason, count+1);
          }
  
          void finish(DataSet next, long now) {
              for (int i = mEntries.size() - 1; i >= 0; i--) {
                  ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i);
                  for (int j = uidMap.size() - 1; j >= 0; j--) {
                      PackageEntry pe = uidMap.valueAt(j);
                      if (pe.activeNesting > 0 || pe.activeTopNesting > 0 || pe.pendingNesting > 0) {
                          // Propagate existing activity in to next data set.
                          PackageEntry nextPe = next.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j));
                          nextPe.activeStartTime = now;
                          nextPe.activeNesting = pe.activeNesting;
                          nextPe.activeTopStartTime = now;
                          nextPe.activeTopNesting = pe.activeTopNesting;
                          nextPe.pendingStartTime = now;
                          nextPe.pendingNesting = pe.pendingNesting;
                          // Finish it off.
                          if (pe.activeNesting > 0) {
                              pe.pastActiveTime += now - pe.activeStartTime;
                              pe.activeNesting = 0;
                          }
                          if (pe.activeTopNesting > 0) {
                              pe.pastActiveTopTime += now - pe.activeTopStartTime;
                              pe.activeTopNesting = 0;
                          }
                          if (pe.pendingNesting > 0) {
                              pe.pastPendingTime += now - pe.pendingStartTime;
                              pe.pendingNesting = 0;
                          }
                      }
                  }
              }
          }
  
          void addTo(DataSet out, long now) {
              out.mSummedTime += getTotalTime(now);
              for (int i = mEntries.size() - 1; i >= 0; i--) {
                  ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i);
                  for (int j = uidMap.size() - 1; j >= 0; j--) {
                      PackageEntry pe = uidMap.valueAt(j);
                      PackageEntry outPe = out.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j));
                      outPe.pastActiveTime += pe.pastActiveTime;
                      outPe.activeCount += pe.activeCount;
                      outPe.pastActiveTopTime += pe.pastActiveTopTime;
                      outPe.activeTopCount += pe.activeTopCount;
                      outPe.pastPendingTime += pe.pastPendingTime;
                      outPe.pendingCount += pe.pendingCount;
                      if (pe.activeNesting > 0) {
                          outPe.pastActiveTime += now - pe.activeStartTime;
                          outPe.hadActive = true;
                      }
                      if (pe.activeTopNesting > 0) {
                          outPe.pastActiveTopTime += now - pe.activeTopStartTime;
                          outPe.hadActiveTop = true;
                      }
                      if (pe.pendingNesting > 0) {
                          outPe.pastPendingTime += now - pe.pendingStartTime;
                          outPe.hadPending = true;
                      }
                      for (int k = pe.stopReasons.size()-1; k >= 0; k--) {
                          int type = pe.stopReasons.keyAt(k);
                          outPe.stopReasons.put(type, outPe.stopReasons.get(type, 0)
                                  + pe.stopReasons.valueAt(k));
                      }
                  }
              }
              if (mMaxTotalActive > out.mMaxTotalActive) {
                  out.mMaxTotalActive = mMaxTotalActive;
              }
              if (mMaxFgActive > out.mMaxFgActive) {
                  out.mMaxFgActive = mMaxFgActive;
              }
          }
  
          void printDuration(PrintWriter pw, long period, long duration, int count, String suffix) {
              float fraction = duration / (float) period;
              int percent = (int) ((fraction * 100) + .5f);
              if (percent > 0) {
                  pw.print(" ");
                  pw.print(percent);
                  pw.print("% ");
                  pw.print(count);
                  pw.print("x ");
                  pw.print(suffix);
              } else if (count > 0) {
                  pw.print(" ");
                  pw.print(count);
                  pw.print("x ");
                  pw.print(suffix);
              }
          }
  
          void dump(PrintWriter pw, String header, String prefix, long now, long nowElapsed,
                  int filterUid) {
              final long period = getTotalTime(now);
              pw.print(prefix); pw.print(header); pw.print(" at ");
              pw.print(DateFormat.format("yyyy-MM-dd-HH-mm-ss", mStartClockTime).toString());
              pw.print(" (");
              TimeUtils.formatDuration(mStartElapsedTime, nowElapsed, pw);
              pw.print(") over ");
              TimeUtils.formatDuration(period, pw);
              pw.println(":");
              final int NE = mEntries.size();
              for (int i = 0; i < NE; i++) {
                  int uid = mEntries.keyAt(i);
                  if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
                      continue;
                  }
                  ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i);
                  final int NP = uidMap.size();
                  for (int j = 0; j < NP; j++) {
                      PackageEntry pe = uidMap.valueAt(j);
                      pw.print(prefix); pw.print("  ");
                      UserHandle.formatUid(pw, uid);
                      pw.print(" / "); pw.print(uidMap.keyAt(j));
                      pw.println(":");
                      pw.print(prefix); pw.print("   ");
                      printDuration(pw, period, pe.getPendingTime(now), pe.pendingCount, "pending");
                      printDuration(pw, period, pe.getActiveTime(now), pe.activeCount, "active");
                      printDuration(pw, period, pe.getActiveTopTime(now), pe.activeTopCount,
                              "active-top");
                      if (pe.pendingNesting > 0 || pe.hadPending) {
                          pw.print(" (pending)");
                      }
                      if (pe.activeNesting > 0 || pe.hadActive) {
                          pw.print(" (active)");
                      }
                      if (pe.activeTopNesting > 0 || pe.hadActiveTop) {
                          pw.print(" (active-top)");
                      }
                      pw.println();
                      if (pe.stopReasons.size() > 0) {
                          pw.print(prefix); pw.print("    ");
                          for (int k = 0; k < pe.stopReasons.size(); k++) {
                              if (k > 0) {
                                  pw.print(", ");
                              }
                              pw.print(pe.stopReasons.valueAt(k));
                              pw.print("x ");
                              pw.print(JobParameters.getReasonName(pe.stopReasons.keyAt(k)));
                          }
                          pw.println();
                      }
                  }
              }
              pw.print(prefix); pw.print("  Max concurrency: ");
              pw.print(mMaxTotalActive); pw.print(" total, ");
              pw.print(mMaxFgActive); pw.println(" foreground");
          }
  
          private void printPackageEntryState(ProtoOutputStream proto, long fieldId,
                  long duration, int count) {
              final long token = proto.start(fieldId);
              proto.write(DataSetProto.PackageEntryProto.State.DURATION_MS, duration);
              proto.write(DataSetProto.PackageEntryProto.State.COUNT, count);
              proto.end(token);
          }
  
          void dump(ProtoOutputStream proto, long fieldId, long now, long nowElapsed, int filterUid) {
              final long token = proto.start(fieldId);
              final long period = getTotalTime(now);
  
              proto.write(DataSetProto.START_CLOCK_TIME_MS, mStartClockTime);
              proto.write(DataSetProto.ELAPSED_TIME_MS, nowElapsed - mStartElapsedTime);
              proto.write(DataSetProto.PERIOD_MS, period);
  
              final int NE = mEntries.size();
              for (int i = 0; i < NE; i++) {
                  int uid = mEntries.keyAt(i);
                  if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
                      continue;
                  }
                  ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i);
                  final int NP = uidMap.size();
                  for (int j = 0; j < NP; j++) {
                      final long peToken = proto.start(DataSetProto.PACKAGE_ENTRIES);
                      PackageEntry pe = uidMap.valueAt(j);
  
                      proto.write(DataSetProto.PackageEntryProto.UID, uid);
                      proto.write(DataSetProto.PackageEntryProto.PACKAGE_NAME, uidMap.keyAt(j));
  
                      printPackageEntryState(proto, DataSetProto.PackageEntryProto.PENDING_STATE,
                              pe.getPendingTime(now), pe.pendingCount);
                      printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_STATE,
                              pe.getActiveTime(now), pe.activeCount);
                      printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_TOP_STATE,
                              pe.getActiveTopTime(now), pe.activeTopCount);
  
                      proto.write(DataSetProto.PackageEntryProto.PENDING,
                            pe.pendingNesting > 0 || pe.hadPending);
                      proto.write(DataSetProto.PackageEntryProto.ACTIVE,
                            pe.activeNesting > 0 || pe.hadActive);
                      proto.write(DataSetProto.PackageEntryProto.ACTIVE_TOP,
                            pe.activeTopNesting > 0 || pe.hadActiveTop);
  
                      for (int k = 0; k < pe.stopReasons.size(); k++) {
                          final long srcToken =
                                  proto.start(DataSetProto.PackageEntryProto.STOP_REASONS);
  
                          proto.write(DataSetProto.PackageEntryProto.StopReasonCount.REASON,
                                  pe.stopReasons.keyAt(k));
                          proto.write(DataSetProto.PackageEntryProto.StopReasonCount.COUNT,
                                  pe.stopReasons.valueAt(k));
  
                          proto.end(srcToken);
                      }
  
                      proto.end(peToken);
                  }
              }
  
              proto.write(DataSetProto.MAX_CONCURRENCY, mMaxTotalActive);
              proto.write(DataSetProto.MAX_FOREGROUND_CONCURRENCY, mMaxFgActive);
  
              proto.end(token);
          }
      }
  
      void rebatchIfNeeded(long now) {
          long totalTime = mCurDataSet.getTotalTime(now);
          if (totalTime > BATCHING_TIME) {
              DataSet last = mCurDataSet;
              last.mSummedTime = totalTime;
              mCurDataSet = new DataSet();
              last.finish(mCurDataSet, now);
              System.arraycopy(mLastDataSets, 0, mLastDataSets, 1, mLastDataSets.length-1);
              mLastDataSets[0] = last;
          }
      }
  
      public void notePending(JobStatus job) {
          final long now = sUptimeMillisClock.millis();
          job.madePending = now;
          rebatchIfNeeded(now);
          mCurDataSet.incPending(job.getSourceUid(), job.getSourcePackageName(), now);
      }
  
      public void noteNonpending(JobStatus job) {
          final long now = sUptimeMillisClock.millis();
          mCurDataSet.decPending(job.getSourceUid(), job.getSourcePackageName(), now);
          rebatchIfNeeded(now);
      }
  
      public void noteActive(JobStatus job) {
          final long now = sUptimeMillisClock.millis();
          job.madeActive = now;
          rebatchIfNeeded(now);
          if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) {
              mCurDataSet.incActiveTop(job.getSourceUid(), job.getSourcePackageName(), now);
          } else {
              mCurDataSet.incActive(job.getSourceUid(), job.getSourcePackageName(), now);
          }
          addEvent(job.getJob().isPeriodic() ? EVENT_START_PERIODIC_JOB :  EVENT_START_JOB,
                  job.getSourceUid(), job.getBatteryName(), job.getJobId(), 0, null);
      }
  
      public void noteInactive(JobStatus job, int stopReason, String debugReason) {
          final long now = sUptimeMillisClock.millis();
          if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) {
              mCurDataSet.decActiveTop(job.getSourceUid(), job.getSourcePackageName(), now,
                      stopReason);
          } else {
              mCurDataSet.decActive(job.getSourceUid(), job.getSourcePackageName(), now, stopReason);
          }
          rebatchIfNeeded(now);
          addEvent(job.getJob().isPeriodic() ? EVENT_STOP_JOB :  EVENT_STOP_PERIODIC_JOB,
                  job.getSourceUid(), job.getBatteryName(), job.getJobId(), stopReason, debugReason);
      }
  
      public void noteConcurrency(int totalActive, int fgActive) {
          if (totalActive > mCurDataSet.mMaxTotalActive) {
              mCurDataSet.mMaxTotalActive = totalActive;
          }
          if (fgActive > mCurDataSet.mMaxFgActive) {
              mCurDataSet.mMaxFgActive = fgActive;
          }
      }
  
      public float getLoadFactor(JobStatus job) {
          final int uid = job.getSourceUid();
          final String pkg = job.getSourcePackageName();
          PackageEntry cur = mCurDataSet.getEntry(uid, pkg);
          PackageEntry last = mLastDataSets[0] != null ? mLastDataSets[0].getEntry(uid, pkg) : null;
          if (cur == null && last == null) {
              return 0;
          }
          final long now = sUptimeMillisClock.millis();
          long time = 0;
          if (cur != null) {
              time += cur.getActiveTime(now) + cur.getPendingTime(now);
          }
          long period = mCurDataSet.getTotalTime(now);
          if (last != null) {
              time += last.getActiveTime(now) + last.getPendingTime(now);
              period += mLastDataSets[0].getTotalTime(now);
          }
          return time / (float)period;
      }
  
      public void dump(PrintWriter pw, String prefix, int filterUid) {
          final long now = sUptimeMillisClock.millis();
          final long nowElapsed = sElapsedRealtimeClock.millis();
          final DataSet total;
          if (mLastDataSets[0] != null) {
              total = new DataSet(mLastDataSets[0]);
              mLastDataSets[0].addTo(total, now);
          } else {
              total = new DataSet(mCurDataSet);
          }
          mCurDataSet.addTo(total, now);
          for (int i = 1; i < mLastDataSets.length; i++) {
              if (mLastDataSets[i] != null) {
                  mLastDataSets[i].dump(pw, "Historical stats", prefix, now, nowElapsed, filterUid);
                  pw.println();
              }
          }
          total.dump(pw, "Current stats", prefix, now, nowElapsed, filterUid);
      }
  
      public void dump(ProtoOutputStream proto, long fieldId, int filterUid) {
          final long token = proto.start(fieldId);
          final long now = sUptimeMillisClock.millis();
          final long nowElapsed = sElapsedRealtimeClock.millis();
  
          final DataSet total;
          if (mLastDataSets[0] != null) {
              total = new DataSet(mLastDataSets[0]);
              mLastDataSets[0].addTo(total, now);
          } else {
              total = new DataSet(mCurDataSet);
          }
          mCurDataSet.addTo(total, now);
  
          for (int i = 1; i < mLastDataSets.length; i++) {
              if (mLastDataSets[i] != null) {
                  mLastDataSets[i].dump(proto, JobPackageTrackerDumpProto.HISTORICAL_STATS,
                          now, nowElapsed, filterUid);
              }
          }
          total.dump(proto, JobPackageTrackerDumpProto.CURRENT_STATS,
                  now, nowElapsed, filterUid);
  
          proto.end(token);
      }
  
      public boolean dumpHistory(PrintWriter pw, String prefix, int filterUid) {
          final int size = mEventIndices.size();
          if (size <= 0) {
              return false;
          }
          pw.println("  Job history:");
          final long now = sElapsedRealtimeClock.millis();
          for (int i=0; i<size; i++) {
              final int index = mEventIndices.indexOf(i);
              final int uid = mEventUids[index];
              if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
                  continue;
              }
              final int cmd = mEventCmds[index] & EVENT_CMD_MASK;
              if (cmd == EVENT_NULL) {
                  continue;
              }
              final String label;
              switch (cmd) {
                  case EVENT_START_JOB:           label = "  START"; break;
                  case EVENT_STOP_JOB:            label = "   STOP"; break;
                  case EVENT_START_PERIODIC_JOB:  label = "START-P"; break;
                  case EVENT_STOP_PERIODIC_JOB:   label = " STOP-P"; break;
                  default:                        label = "     ??"; break;
              }
              pw.print(prefix);
              TimeUtils.formatDuration(mEventTimes[index]-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN);
              pw.print(" ");
              pw.print(label);
              pw.print(": #");
              UserHandle.formatUid(pw, uid);
              pw.print("/");
              pw.print(mEventJobIds[index]);
              pw.print(" ");
              pw.print(mEventTags[index]);
              if (cmd == EVENT_STOP_JOB || cmd == EVENT_STOP_PERIODIC_JOB) {
                  pw.print(" ");
                  final String reason = mEventReasons[index];
                  if (reason != null) {
                      pw.print(mEventReasons[index]);
                  } else {
                      pw.print(JobParameters.getReasonName((mEventCmds[index] & EVENT_STOP_REASON_MASK)
                              >> EVENT_STOP_REASON_SHIFT));
                  }
              }
              pw.println();
          }
          return true;
      }
  
      public void dumpHistory(ProtoOutputStream proto, long fieldId, int filterUid) {
          final int size = mEventIndices.size();
          if (size == 0) {
              return;
          }
          final long token = proto.start(fieldId);
  
          final long now = sElapsedRealtimeClock.millis();
          for (int i = 0; i < size; i++) {
              final int index = mEventIndices.indexOf(i);
              final int uid = mEventUids[index];
              if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
                  continue;
              }
              final int cmd = mEventCmds[index] & EVENT_CMD_MASK;
              if (cmd == EVENT_NULL) {
                  continue;
              }
              final long heToken = proto.start(JobPackageHistoryProto.HISTORY_EVENT);
  
              proto.write(JobPackageHistoryProto.HistoryEvent.EVENT, cmd);
              proto.write(JobPackageHistoryProto.HistoryEvent.TIME_SINCE_EVENT_MS, now - mEventTimes[index]);
              proto.write(JobPackageHistoryProto.HistoryEvent.UID, uid);
              proto.write(JobPackageHistoryProto.HistoryEvent.JOB_ID, mEventJobIds[index]);
              proto.write(JobPackageHistoryProto.HistoryEvent.TAG, mEventTags[index]);
              if (cmd == EVENT_STOP_JOB || cmd == EVENT_STOP_PERIODIC_JOB) {
                  proto.write(JobPackageHistoryProto.HistoryEvent.STOP_REASON,
                      (mEventCmds[index] & EVENT_STOP_REASON_MASK) >> EVENT_STOP_REASON_SHIFT);
              }
  
              proto.end(heToken);
          }
  
          proto.end(token);
      }
  }
  