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