1 /*
2  * Copyright (C) 2021 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.server.pm;
18 
19 import android.annotation.Nullable;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.os.Message;
23 import android.os.SystemClock;
24 import android.text.TextUtils;
25 
26 import com.android.internal.annotations.GuardedBy;
27 import com.android.internal.util.FrameworkStatsLog;
28 import com.android.server.EventLogTags;
29 
30 import java.io.PrintWriter;
31 import java.util.Arrays;
32 import java.util.Locale;
33 import java.util.concurrent.TimeUnit;
34 
35 /**
36  * This class records statistics about PackageManagerService snapshots.  It maintains two sets of
37  * statistics: a periodic set which represents the last 10 minutes, and a cumulative set since
38  * process boot.  The key metrics that are recorded are:
39  * <ul>
40  * <li> The time to create a snapshot - this is the performance cost of a snapshot
41  * <li> The lifetime of the snapshot - creation time over lifetime is the amortized cost
42  * <li> The number of times a snapshot is reused - this is the number of times lock
43  *      contention was avoided.
44  * </ul>
45 
46  * The time conversions in this class are designed to keep arithmetic using ints, rather
47  * than longs.  Raw times are supplied as longs in units of us.  These are left long.
48  * Rebuild durations however, are converted to ints.  An int can express a duration of
49  * approximately 35 minutes.  This is longer than any expected snapshot rebuild time, so
50  * an int is satisfactory.  The exception is the cumulative rebuild time over the course
51  * of a monitoring cycle: this value is kept long since the cycle time is one week and in
52  * a badly behaved system, the rebuild time might exceed 35 minutes.
53 
54  * @hide
55  */
56 public class SnapshotStatistics {
57     /**
58      * The interval at which statistics should be ticked.  It is 60s.  The interval is in
59      * units of milliseconds because that is what's required by Handler.sendMessageDelayed().
60      */
61     public static final int SNAPSHOT_TICK_INTERVAL_MS = 60 * 1000;
62 
63     /**
64      * The interval of the snapshot statistics logging.
65      */
66     private static final long SNAPSHOT_LOG_INTERVAL_US = TimeUnit.DAYS.toMicros(1);
67 
68     /**
69      * The number snapshot event logs that can be generated in a single logging interval.
70      * A small number limits the logging generated by this class.  A snapshot event log is
71      * generated for every big snapshot build time, up to the limit, or whenever the
72      * maximum build time is exceeded in the logging interval.
73      */
74     public static final int SNAPSHOT_BUILD_REPORT_LIMIT = 10;
75 
76     /**
77      * The number of microseconds in a millisecond.
78      */
79     private static final int US_IN_MS = 1000;
80 
81     /**
82      * A snapshot build time is "big" if it takes longer than 10ms.
83      */
84     public static final int SNAPSHOT_BIG_BUILD_TIME_US = 10 * US_IN_MS;
85 
86     /**
87      * A snapshot build time is reportable if it takes longer than 30ms.  Testing shows
88      * that this is very rare.
89      */
90     public static final int SNAPSHOT_REPORTABLE_BUILD_TIME_US = 30 * US_IN_MS;
91 
92     /**
93      * A snapshot is short-lived it used fewer than 5 times.
94      */
95     public static final int SNAPSHOT_SHORT_LIFETIME = 5;
96 
97     /**
98      *  Buckets to represent a range of the rebuild latency for the histogram of
99      *  snapshot rebuild latency.
100      */
101     private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_1_MILLIS = 1;
102     private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_2_MILLIS = 2;
103     private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_5_MILLIS = 5;
104     private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_10_MILLIS = 10;
105     private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_20_MILLIS = 20;
106     private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_50_MILLIS = 50;
107     private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_100_MILLIS = 100;
108 
109     /**
110      *  Buckets to represent a range of the reuse count for the histogram of
111      *  snapshot reuse counts.
112      */
113     private static final int REUSE_COUNT_BUCKET_LESS_THAN_1 = 1;
114     private static final int REUSE_COUNT_BUCKET_LESS_THAN_10 = 10;
115     private static final int REUSE_COUNT_BUCKET_LESS_THAN_100 = 100;
116     private static final int REUSE_COUNT_BUCKET_LESS_THAN_1000 = 1000;
117     private static final int REUSE_COUNT_BUCKET_LESS_THAN_10000 = 10000;
118 
119     /**
120      * The lock to control access to this object.
121      */
122     private final Object mLock = new Object();
123 
124     /**
125      * The bins for the build time histogram.  Values are in us.
126      */
127     private final BinMap mTimeBins;
128 
129     /**
130      * The bins for the snapshot use histogram.
131      */
132     private final BinMap mUseBins;
133 
134     /**
135      * The number of events reported in the current tick.
136      */
137     private int mEventsReported = 0;
138 
139     /**
140      * The handler used for the periodic ticks.
141      */
142     private Handler mHandler = null;
143 
144     /**
145      * Convert ns to an int ms.  The maximum range of this method is about 24 days.  There
146      * is no expectation that an event will take longer than that.
147      */
usToMs(int us)148     private int usToMs(int us) {
149         return us / US_IN_MS;
150     }
151 
152     /**
153      * This class exists to provide a fast bin lookup for histograms.  An instance has an
154      * integer array that maps incoming values to bins.  Values larger than the array are
155      * mapped to the top-most bin.
156      */
157     private static class BinMap {
158 
159         // The number of bins
160         private final int mCount;
161         // The maximum mapped value.  Values at or above this are mapped to the
162         // top bin.
163         private final int mMaxBin;
164         // A copy of the original key
165         private final int[] mUserKey;
166 
167         /**
168          * Create a bin map.  The input is an array of integers, which must be
169          * monotonically increasing (this is not checked).  The result is an integer array
170          * as long as the largest value in the input.
171          */
BinMap(int[] userKey)172         BinMap(int[] userKey) {
173             mUserKey = Arrays.copyOf(userKey, userKey.length);
174             // The number of bins is the length of the keys, plus 1 (for the max).
175             mCount = mUserKey.length + 1;
176             // The maximum value is one more than the last one in the map.
177             mMaxBin = mUserKey[mUserKey.length - 1] + 1;
178         }
179 
180         /**
181          * Map a value to a bin.
182          */
getBin(int x)183         public int getBin(int x) {
184             if (x >= 0 && x < mMaxBin) {
185                 for (int i = 0; i < mUserKey.length; i++) {
186                     if (x <= mUserKey[i]) {
187                         return i;
188                     }
189                 }
190                 return 0; // should not happen
191             } else if (x >= mMaxBin) {
192                 return mUserKey.length;
193             } else {
194                 // x is negative.  The bin will not be used.
195                 return 0;
196             }
197         }
198 
199         /**
200          * The number of bins in this map
201          */
count()202         public int count() {
203             return mCount;
204         }
205 
206         /**
207          * For convenience, return the user key.
208          */
userKeys()209         public int[] userKeys() {
210             return mUserKey;
211         }
212     }
213 
214     /**
215      * A complete set of statistics.  These are public, making it simpler for a client to
216      * fetch the individual fields.
217      */
218     public class Stats {
219 
220         /**
221          * The start time for this set of statistics, in us.
222          */
223         public long mStartTimeUs = 0;
224 
225         /**
226          * The completion time for this set of statistics, in ns.  A value of zero means
227          * the statistics are still active.
228          */
229         public long mStopTimeUs = 0;
230 
231         /**
232          * The build-time histogram.  The total number of rebuilds is the sum over the
233          * histogram entries.
234          */
235         public final int[] mTimes;
236 
237         /**
238          * The reuse histogram.  The total number of snapshot uses is the sum over the
239          * histogram entries.
240          */
241         public final int[] mUsed;
242 
243         /**
244          * The total number of rebuilds.  This could be computed by summing over the use
245          * bins, but is maintained separately for convenience.
246          */
247         public int mTotalBuilds = 0;
248 
249         /**
250          * The total number of times any snapshot was used.
251          */
252         public int mTotalUsed = 0;
253 
254         /**
255          * The total number of builds that count as big, which means they took longer than
256          * SNAPSHOT_BIG_BUILD_TIME_NS.
257          */
258         public int mBigBuilds = 0;
259 
260         /**
261          * The total number of short-lived snapshots
262          */
263         public int mShortLived = 0;
264 
265         /**
266          * The time taken to build snapshots.  This is cumulative over the rebuilds
267          * recorded in mRebuilds, so the average time to build a snapshot is given by
268          * mBuildTimeNs/mRebuilds.  Note that this cannot be computed from the histogram.
269          */
270         public long mTotalTimeUs = 0;
271 
272         /**
273          * The maximum build time since the last log.
274          */
275         public int mMaxBuildTimeUs = 0;
276 
277         /**
278          * The maximum used count since the last log.
279          */
280         public int mMaxUsedCount = 0;
281 
282         /**
283          * Record the rebuild.  The parameters are the length of time it took to build the
284          * latest snapshot, and the number of times the _previous_ snapshot was used.  A
285          * negative value for used signals an invalid value, which is the case the first
286          * time a snapshot is every built.
287          */
rebuild(int duration, int used, int buildBin, int useBin, boolean big, boolean quick)288         private void rebuild(int duration, int used,
289                 int buildBin, int useBin, boolean big, boolean quick) {
290             mTotalBuilds++;
291             mTimes[buildBin]++;
292 
293             if (used >= 0) {
294                 mTotalUsed += used;
295                 mUsed[useBin]++;
296             }
297 
298             mTotalTimeUs += duration;
299 
300             if (big) {
301                 mBigBuilds++;
302             }
303             if (quick) {
304                 mShortLived++;
305             }
306             if (mMaxBuildTimeUs < duration) {
307                 mMaxBuildTimeUs = duration;
308             }
309             if (mMaxUsedCount < used) {
310                 mMaxUsedCount = used;
311             }
312         }
313 
Stats(long now)314         private Stats(long now) {
315             mStartTimeUs = now;
316             mTimes = new int[mTimeBins.count()];
317             mUsed = new int[mUseBins.count()];
318         }
319 
320         /**
321          * Create a copy of the argument.  The copy is made under lock but can then be
322          * used without holding the lock.
323          */
Stats(Stats orig)324         private Stats(Stats orig) {
325             mStartTimeUs = orig.mStartTimeUs;
326             mStopTimeUs = orig.mStopTimeUs;
327             mTimes = Arrays.copyOf(orig.mTimes, orig.mTimes.length);
328             mUsed = Arrays.copyOf(orig.mUsed, orig.mUsed.length);
329             mTotalBuilds = orig.mTotalBuilds;
330             mTotalUsed = orig.mTotalUsed;
331             mBigBuilds = orig.mBigBuilds;
332             mShortLived = orig.mShortLived;
333             mTotalTimeUs = orig.mTotalTimeUs;
334             mMaxBuildTimeUs = orig.mMaxBuildTimeUs;
335             mMaxUsedCount = orig.mMaxUsedCount;
336         }
337 
338         /**
339          * Set the end time for the statistics.  The end time is used only for reporting
340          * in the dump() method.
341          */
complete(long stop)342         private void complete(long stop) {
343             mStopTimeUs = stop;
344         }
345 
346         /**
347          * Format a time span into ddd:HH:MM:SS.  The input is in us.
348          */
durationToString(long us)349         private String durationToString(long us) {
350             // s has a range of several years
351             int s = (int) (us / (1000 * 1000));
352             int m = s / 60;
353             s %= 60;
354             int h = m / 60;
355             m %= 60;
356             int d = h / 24;
357             h %= 24;
358             if (d != 0) {
359                 return TextUtils.formatSimple("%2d:%02d:%02d:%02d", d, h, m, s);
360             } else if (h != 0) {
361                 return TextUtils.formatSimple("%2s %02d:%02d:%02d", "", h, m, s);
362             } else {
363                 return TextUtils.formatSimple("%2s %2s %2d:%02d", "", "", m, s);
364             }
365         }
366 
367         /**
368          * Print the prefix for dumping.  This does not generate a line to the output.
369          */
dumpPrefix(PrintWriter pw, String indent, long now, boolean header, String title)370         private void dumpPrefix(PrintWriter pw, String indent, long now, boolean header,
371                                 String title) {
372             pw.print(indent + " ");
373             if (header) {
374                 pw.format(Locale.US, "%-23s", title);
375             } else {
376                 pw.format(Locale.US, "%11s", durationToString(now - mStartTimeUs));
377                 if (mStopTimeUs != 0) {
378                     pw.format(Locale.US, " %11s", durationToString(now - mStopTimeUs));
379                 } else {
380                     pw.format(Locale.US, " %11s", "now");
381                 }
382             }
383         }
384 
385         /**
386          * Dump the summary statistics record.  Choose the header or the data.
387          *    number of builds
388          *    number of uses
389          *    number of big builds
390          *    number of short lifetimes
391          *    cumulative build time, in seconds
392          *    maximum build time, in ms
393          */
dumpStats(PrintWriter pw, String indent, long now, boolean header)394         private void dumpStats(PrintWriter pw, String indent, long now, boolean header) {
395             dumpPrefix(pw, indent, now, header, "Summary stats");
396             if (header) {
397                 pw.format(Locale.US, "  %10s  %10s  %10s  %10s  %10s  %10s",
398                           "TotBlds", "TotUsed", "BigBlds", "ShortLvd",
399                           "TotTime", "MaxTime");
400             } else {
401                 pw.format(Locale.US,
402                         "  %10d  %10d  %10d  %10d  %10d  %10d",
403                         mTotalBuilds, mTotalUsed, mBigBuilds, mShortLived,
404                         mTotalTimeUs / 1000, mMaxBuildTimeUs / 1000);
405             }
406             pw.println();
407         }
408 
409         /**
410          * Dump the build time histogram.  Choose the header or the data.
411          */
dumpTimes(PrintWriter pw, String indent, long now, boolean header)412         private void dumpTimes(PrintWriter pw, String indent, long now, boolean header) {
413             dumpPrefix(pw, indent, now, header, "Build times");
414             if (header) {
415                 int[] keys = mTimeBins.userKeys();
416                 for (int i = 0; i < keys.length; i++) {
417                     pw.format(Locale.US, "  %10s",
418                             TextUtils.formatSimple("<= %dms", keys[i]));
419                 }
420                 pw.format(Locale.US, "  %10s",
421                         TextUtils.formatSimple("> %dms", keys[keys.length - 1]));
422             } else {
423                 for (int i = 0; i < mTimes.length; i++) {
424                     pw.format(Locale.US, "  %10d", mTimes[i]);
425                 }
426             }
427             pw.println();
428         }
429 
430         /**
431          * Dump the usage histogram.  Choose the header or the data.
432          */
dumpUsage(PrintWriter pw, String indent, long now, boolean header)433         private void dumpUsage(PrintWriter pw, String indent, long now, boolean header) {
434             dumpPrefix(pw, indent, now, header, "Use counters");
435             if (header) {
436                 int[] keys = mUseBins.userKeys();
437                 for (int i = 0; i < keys.length; i++) {
438                     pw.format(Locale.US, "  %10s", TextUtils.formatSimple("<= %d", keys[i]));
439                 }
440                 pw.format(Locale.US, "  %10s",
441                         TextUtils.formatSimple("> %d", keys[keys.length - 1]));
442             } else {
443                 for (int i = 0; i < mUsed.length; i++) {
444                     pw.format(Locale.US, "  %10d", mUsed[i]);
445                 }
446             }
447             pw.println();
448         }
449 
450         /**
451          * Dump something, based on the "what" parameter.
452          */
dump(PrintWriter pw, String indent, long now, boolean header, String what)453         private void dump(PrintWriter pw, String indent, long now, boolean header, String what) {
454             if (what.equals("stats")) {
455                 dumpStats(pw, indent, now, header);
456             } else if (what.equals("times")) {
457                 dumpTimes(pw, indent, now, header);
458             } else if (what.equals("usage")) {
459                 dumpUsage(pw, indent, now, header);
460             } else {
461                 throw new IllegalArgumentException("unrecognized choice: " + what);
462             }
463         }
464 
465         /**
466          * Report the snapshot statistics to FrameworkStatsLog.
467          */
logSnapshotStatistics(int packageCount)468         private void logSnapshotStatistics(int packageCount) {
469             final long avgLatencyUs = (mTotalBuilds == 0 ? 0 : mTotalTimeUs / mTotalBuilds);
470             final int avgUsedCount = (mTotalBuilds == 0 ? 0 : mTotalUsed / mTotalBuilds);
471             FrameworkStatsLog.write(
472                     FrameworkStatsLog.PACKAGE_MANAGER_SNAPSHOT_REPORTED, mTimes, mUsed,
473                     mMaxBuildTimeUs, mMaxUsedCount, avgLatencyUs, avgUsedCount, packageCount);
474         }
475     }
476 
477     /**
478      * Long statistics.  These roll over approximately one day.
479      */
480     private final Stats[] mLong;
481 
482     /**
483      * Short statistics.  These roll over approximately every minute;
484      */
485     private final Stats[] mShort;
486 
487     /**
488      * The time of last logging to the FrameworkStatsLog.
489      */
490     private long mLastLogTimeUs;
491 
492     /**
493      * The number of packages on the device.
494      */
495     private int mPackageCount;
496 
497     /**
498      * Create a snapshot object.  Initialize the bin levels.  The last bin catches
499      * everything that is not caught earlier, so its value is not really important.
500      */
SnapshotStatistics()501     public SnapshotStatistics() {
502         // Create the bin thresholds.  The time bins are in units of us.
503         mTimeBins = new BinMap(new int[] {
504                 REBUILD_LATENCY_BUCKET_LESS_THAN_1_MILLIS,
505                 REBUILD_LATENCY_BUCKET_LESS_THAN_2_MILLIS,
506                 REBUILD_LATENCY_BUCKET_LESS_THAN_5_MILLIS,
507                 REBUILD_LATENCY_BUCKET_LESS_THAN_10_MILLIS,
508                 REBUILD_LATENCY_BUCKET_LESS_THAN_20_MILLIS,
509                 REBUILD_LATENCY_BUCKET_LESS_THAN_50_MILLIS,
510                 REBUILD_LATENCY_BUCKET_LESS_THAN_100_MILLIS });
511         mUseBins = new BinMap(new int[] {
512                 REUSE_COUNT_BUCKET_LESS_THAN_1,
513                 REUSE_COUNT_BUCKET_LESS_THAN_10,
514                 REUSE_COUNT_BUCKET_LESS_THAN_100,
515                 REUSE_COUNT_BUCKET_LESS_THAN_1000,
516                 REUSE_COUNT_BUCKET_LESS_THAN_10000 });
517 
518         // Create the raw statistics
519         final long now = SystemClock.currentTimeMicro();
520         mLong = new Stats[2];
521         mLong[0] = new Stats(now);
522         mShort = new Stats[10];
523         mShort[0] = new Stats(now);
524         mLastLogTimeUs = now;
525 
526         // Create the message handler for ticks and start the ticker.
527         mHandler = new Handler(Looper.getMainLooper()) {
528                 @Override
529                 public void handleMessage(Message msg) {
530                     SnapshotStatistics.this.handleMessage(msg);
531                 }
532             };
533         scheduleTick();
534     }
535 
536     /**
537      * Handle a message.  The only messages are ticks, so the message parameter is ignored.
538      */
handleMessage(@ullable Message msg)539     private void handleMessage(@Nullable Message msg) {
540         tick();
541         scheduleTick();
542     }
543 
544     /**
545      * Schedule one tick, a tick interval in the future.
546      */
scheduleTick()547     private void scheduleTick() {
548         mHandler.sendEmptyMessageDelayed(0, SNAPSHOT_TICK_INTERVAL_MS);
549     }
550 
551     /**
552      * Record a rebuild.  Cumulative and current statistics are updated.  Events may be
553      * generated.
554      * @param now The time at which the snapshot rebuild began, in ns.
555      * @param done The time at which the snapshot rebuild completed, in ns.
556      * @param hits The number of times the previous snapshot was used.
557      * @param packageCount The number of packages on the device.
558      */
rebuild(long now, long done, int hits, int packageCount)559     public final void rebuild(long now, long done, int hits, int packageCount) {
560         // The duration has a span of about 2000s
561         final int duration = (int) (done - now);
562         boolean reportEvent = false;
563         synchronized (mLock) {
564             mPackageCount = packageCount;
565 
566             final int timeBin = mTimeBins.getBin(duration / 1000);
567             final int useBin = mUseBins.getBin(hits);
568             final boolean big = duration >= SNAPSHOT_BIG_BUILD_TIME_US;
569             final boolean quick = hits <= SNAPSHOT_SHORT_LIFETIME;
570 
571             mShort[0].rebuild(duration, hits, timeBin, useBin, big, quick);
572             mLong[0].rebuild(duration, hits, timeBin, useBin, big, quick);
573             if (duration >= SNAPSHOT_REPORTABLE_BUILD_TIME_US) {
574                 if (mEventsReported++ < SNAPSHOT_BUILD_REPORT_LIMIT) {
575                     reportEvent = true;
576                 }
577             }
578         }
579         // The IO to the logger is done outside the lock.
580         if (reportEvent) {
581             // Report the first N big builds, and every new maximum after that.
582             EventLogTags.writePmSnapshotRebuild(duration / US_IN_MS, hits);
583         }
584     }
585 
586     /**
587      * Roll a stats array.  Shift the elements up an index and create a new element at
588      * index zero.  The old element zero is completed with the specified time.
589      */
590     @GuardedBy("mLock")
shift(Stats[] s, long now)591     private void shift(Stats[] s, long now) {
592         s[0].complete(now);
593         for (int i = s.length - 1; i > 0; i--) {
594             s[i] = s[i - 1];
595         }
596         s[0] = new Stats(now);
597     }
598 
599     /**
600      * Roll the statistics.
601      * <ul>
602      * <li> Roll the quick statistics immediately.
603      * <li> Roll the long statistics every SNAPSHOT_LONG_TICKER ticks.  The long
604      * statistics hold a week's worth of data.
605      * <li> Roll the logging statistics every SNAPSHOT_LOGGING_TICKER ticks.  The logging
606      * statistics hold 10 minutes worth of data.
607      * </ul>
608      */
tick()609     private void tick() {
610         synchronized (mLock) {
611             long now = SystemClock.currentTimeMicro();
612             if (now - mLastLogTimeUs > SNAPSHOT_LOG_INTERVAL_US) {
613                 shift(mLong, now);
614                 mLastLogTimeUs = now;
615                 mLong[mLong.length - 1].logSnapshotStatistics(mPackageCount);
616             }
617 
618             shift(mShort, now);
619             mEventsReported = 0;
620         }
621     }
622 
623     /**
624      * Dump the statistics.  The header is dumped from l[0], so that must not be null.
625      */
dump(PrintWriter pw, String indent, long now, Stats[] l, Stats[] s, String what)626     private void dump(PrintWriter pw, String indent, long now, Stats[] l, Stats[] s, String what) {
627         l[0].dump(pw, indent, now, true, what);
628         for (int i = 0; i < s.length; i++) {
629             if (s[i] != null) {
630                 s[i].dump(pw, indent, now, false, what);
631             }
632         }
633         for (int i = 0; i < l.length; i++) {
634             if (l[i] != null) {
635                 l[i].dump(pw, indent, now, false, what);
636             }
637         }
638     }
639 
640     /**
641      * Dump the statistics.  The format is compatible with the PackageManager dumpsys
642      * output.
643      */
dump(PrintWriter pw, String indent, long now, int unrecorded, boolean brief)644     public void dump(PrintWriter pw, String indent, long now, int unrecorded, boolean brief) {
645         // Grab the raw statistics under lock, but print them outside of the lock.
646         Stats[] l;
647         Stats[] s;
648         synchronized (mLock) {
649             l = Arrays.copyOf(mLong, mLong.length);
650             l[0] = new Stats(l[0]);
651             s = Arrays.copyOf(mShort, mShort.length);
652             s[0] = new Stats(s[0]);
653         }
654         pw.format(Locale.US, "%s Unrecorded-hits: %d", indent, unrecorded);
655         pw.println();
656         dump(pw, indent, now, l, s, "stats");
657         if (brief) {
658             return;
659         }
660         pw.println();
661         dump(pw, indent, now, l, s, "times");
662         pw.println();
663         dump(pw, indent, now, l, s, "usage");
664     }
665 }
666