1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * vim: set ts=8 sts=2 et sw=2 tw=80:
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "gc/Statistics.h"
8 
9 #include "mozilla/ArrayUtils.h"
10 #include "mozilla/DebugOnly.h"
11 #include "mozilla/Sprintf.h"
12 #include "mozilla/TimeStamp.h"
13 
14 #include <algorithm>
15 #include <stdarg.h>
16 #include <stdio.h>
17 #include <type_traits>
18 
19 #include "debugger/DebugAPI.h"
20 #include "gc/GC.h"
21 #include "gc/Memory.h"
22 #include "util/Text.h"
23 #include "vm/HelperThreads.h"
24 #include "vm/Runtime.h"
25 #include "vm/Time.h"
26 
27 #include "gc/PrivateIterators-inl.h"
28 
29 using namespace js;
30 using namespace js::gc;
31 using namespace js::gcstats;
32 
33 using mozilla::DebugOnly;
34 using mozilla::EnumeratedArray;
35 using mozilla::Maybe;
36 using mozilla::TimeDuration;
37 using mozilla::TimeStamp;
38 
39 static const size_t BYTES_PER_MB = 1024 * 1024;
40 
41 /*
42  * If this fails, then you can either delete this assertion and allow all
43  * larger-numbered reasons to pile up in the last telemetry bucket, or switch
44  * to GC_REASON_3 and bump the max value.
45  */
46 static_assert(JS::GCReason::NUM_TELEMETRY_REASONS >= JS::GCReason::NUM_REASONS);
47 
AllPhaseKinds()48 static inline auto AllPhaseKinds() {
49   return mozilla::MakeEnumeratedRange(PhaseKind::FIRST, PhaseKind::LIMIT);
50 }
51 
MajorGCPhaseKinds()52 static inline auto MajorGCPhaseKinds() {
53   return mozilla::MakeEnumeratedRange(PhaseKind::GC_BEGIN,
54                                       PhaseKind(size_t(PhaseKind::GC_END) + 1));
55 }
56 
ExplainInvocationKind(JSGCInvocationKind gckind)57 const char* js::gcstats::ExplainInvocationKind(JSGCInvocationKind gckind) {
58   MOZ_ASSERT(gckind == GC_NORMAL || gckind == GC_SHRINK);
59   if (gckind == GC_NORMAL) {
60     return "Normal";
61   } else {
62     return "Shrinking";
63   }
64 }
65 
ExplainGCReason(JS::GCReason reason)66 JS_PUBLIC_API const char* JS::ExplainGCReason(JS::GCReason reason) {
67   switch (reason) {
68 #define SWITCH_REASON(name, _) \
69   case JS::GCReason::name:     \
70     return #name;
71     GCREASONS(SWITCH_REASON)
72 #undef SWITCH_REASON
73 
74     case JS::GCReason::NO_REASON:
75       return "NO_REASON";
76 
77     default:
78       MOZ_CRASH("bad GC reason");
79   }
80 }
81 
InternalGCReason(JS::GCReason reason)82 JS_PUBLIC_API bool JS::InternalGCReason(JS::GCReason reason) {
83   return reason < JS::GCReason::FIRST_FIREFOX_REASON;
84 }
85 
ExplainAbortReason(gc::AbortReason reason)86 const char* js::gcstats::ExplainAbortReason(gc::AbortReason reason) {
87   switch (reason) {
88 #define SWITCH_REASON(name, _) \
89   case gc::AbortReason::name:  \
90     return #name;
91     GC_ABORT_REASONS(SWITCH_REASON)
92 
93     default:
94       MOZ_CRASH("bad GC abort reason");
95 #undef SWITCH_REASON
96   }
97 }
98 
MaybeOpenFileFromEnv(const char * env)99 static FILE* MaybeOpenFileFromEnv(const char* env) {
100   FILE* file;
101   const char* value = getenv(env);
102 
103   if (!value) {
104     return nullptr;
105   }
106 
107   if (strcmp(value, "none") == 0) {
108     file = nullptr;
109   } else if (strcmp(value, "stdout") == 0) {
110     file = stdout;
111   } else if (strcmp(value, "stderr") == 0) {
112     file = stderr;
113   } else {
114     char path[300];
115     if (value[0] != '/') {
116       const char* dir = getenv("MOZ_UPLOAD_DIR");
117       if (dir) {
118         SprintfLiteral(path, "%s/%s", dir, value);
119         value = path;
120       }
121     }
122 
123     file = fopen(value, "a");
124     if (!file) {
125       perror("opening log file");
126       MOZ_CRASH("Failed to open log file.");
127     }
128   }
129 
130   return file;
131 }
132 
133 struct PhaseKindInfo {
134   Phase firstPhase;
135   uint8_t telemetryBucket;
136 };
137 
138 // PhaseInfo objects form a tree.
139 struct PhaseInfo {
140   Phase parent;
141   Phase firstChild;
142   Phase nextSibling;
143   Phase nextWithPhaseKind;
144   PhaseKind phaseKind;
145   uint8_t depth;
146   const char* name;
147   const char* path;
148 };
149 
150 // A table of PhaseInfo indexed by Phase.
151 using PhaseTable = EnumeratedArray<Phase, Phase::LIMIT, PhaseInfo>;
152 
153 // A table of PhaseKindInfo indexed by PhaseKind.
154 using PhaseKindTable =
155     EnumeratedArray<PhaseKind, PhaseKind::LIMIT, PhaseKindInfo>;
156 
157 #include "gc/StatsPhasesGenerated.inc"
158 
159 // Iterate the phases in a phase kind.
160 class PhaseIter {
161   Phase phase;
162 
163  public:
PhaseIter(PhaseKind kind)164   explicit PhaseIter(PhaseKind kind) : phase(phaseKinds[kind].firstPhase) {}
done() const165   bool done() const { return phase == Phase::NONE; }
next()166   void next() { phase = phases[phase].nextWithPhaseKind; }
get() const167   Phase get() const { return phase; }
operator Phase() const168   operator Phase() const { return phase; }
169 };
170 
t(TimeDuration duration)171 static double t(TimeDuration duration) { return duration.ToMilliseconds(); }
172 
context()173 inline JSContext* Statistics::context() {
174   return gc->rt->mainContextFromOwnThread();
175 }
176 
currentPhase() const177 inline Phase Statistics::currentPhase() const {
178   return phaseStack.empty() ? Phase::NONE : phaseStack.back();
179 }
180 
currentPhaseKind() const181 PhaseKind Statistics::currentPhaseKind() const {
182   // Public API to get the current phase kind, suppressing the synthetic
183   // PhaseKind::MUTATOR phase.
184 
185   Phase phase = currentPhase();
186   MOZ_ASSERT_IF(phase == Phase::MUTATOR, phaseStack.length() == 1);
187   if (phase == Phase::NONE || phase == Phase::MUTATOR) {
188     return PhaseKind::NONE;
189   }
190 
191   return phases[phase].phaseKind;
192 }
193 
LookupPhaseWithParent(PhaseKind phaseKind,Phase parentPhase)194 static Phase LookupPhaseWithParent(PhaseKind phaseKind, Phase parentPhase) {
195   for (PhaseIter phase(phaseKind); !phase.done(); phase.next()) {
196     if (phases[phase].parent == parentPhase) {
197       return phase;
198     }
199   }
200 
201   return Phase::NONE;
202 }
203 
lookupChildPhase(PhaseKind phaseKind) const204 Phase Statistics::lookupChildPhase(PhaseKind phaseKind) const {
205   if (phaseKind == PhaseKind::IMPLICIT_SUSPENSION) {
206     return Phase::IMPLICIT_SUSPENSION;
207   }
208   if (phaseKind == PhaseKind::EXPLICIT_SUSPENSION) {
209     return Phase::EXPLICIT_SUSPENSION;
210   }
211 
212   MOZ_ASSERT(phaseKind < PhaseKind::LIMIT);
213 
214   // Search all expanded phases that correspond to the required
215   // phase to find the one whose parent is the current expanded phase.
216   Phase phase = LookupPhaseWithParent(phaseKind, currentPhase());
217 
218   if (phase == Phase::NONE) {
219     MOZ_CRASH_UNSAFE_PRINTF(
220         "Child phase kind %u not found under current phase kind %u",
221         unsigned(phaseKind), unsigned(currentPhaseKind()));
222   }
223 
224   return phase;
225 }
226 
AllPhases()227 inline auto AllPhases() {
228   return mozilla::MakeEnumeratedRange(Phase::FIRST, Phase::LIMIT);
229 }
230 
gcDuration(TimeDuration * total,TimeDuration * maxPause) const231 void Statistics::gcDuration(TimeDuration* total, TimeDuration* maxPause) const {
232   *total = *maxPause = 0;
233   for (auto& slice : slices_) {
234     *total += slice.duration();
235     if (slice.duration() > *maxPause) {
236       *maxPause = slice.duration();
237     }
238   }
239   if (*maxPause > maxPauseInInterval) {
240     maxPauseInInterval = *maxPause;
241   }
242 }
243 
sccDurations(TimeDuration * total,TimeDuration * maxPause) const244 void Statistics::sccDurations(TimeDuration* total,
245                               TimeDuration* maxPause) const {
246   *total = *maxPause = 0;
247   for (size_t i = 0; i < sccTimes.length(); i++) {
248     *total += sccTimes[i];
249     *maxPause = std::max(*maxPause, sccTimes[i]);
250   }
251 }
252 
253 typedef Vector<UniqueChars, 8, SystemAllocPolicy> FragmentVector;
254 
Join(const FragmentVector & fragments,const char * separator="")255 static UniqueChars Join(const FragmentVector& fragments,
256                         const char* separator = "") {
257   const size_t separatorLength = strlen(separator);
258   size_t length = 0;
259   for (size_t i = 0; i < fragments.length(); ++i) {
260     length += fragments[i] ? strlen(fragments[i].get()) : 0;
261     if (i < (fragments.length() - 1)) {
262       length += separatorLength;
263     }
264   }
265 
266   char* joined = js_pod_malloc<char>(length + 1);
267   if (!joined) {
268     return UniqueChars();
269   }
270 
271   joined[length] = '\0';
272   char* cursor = joined;
273   for (size_t i = 0; i < fragments.length(); ++i) {
274     if (fragments[i]) {
275       strcpy(cursor, fragments[i].get());
276     }
277     cursor += fragments[i] ? strlen(fragments[i].get()) : 0;
278     if (i < (fragments.length() - 1)) {
279       if (separatorLength) {
280         strcpy(cursor, separator);
281       }
282       cursor += separatorLength;
283     }
284   }
285 
286   return UniqueChars(joined);
287 }
288 
SumChildTimes(Phase phase,const Statistics::PhaseTimeTable & phaseTimes)289 static TimeDuration SumChildTimes(
290     Phase phase, const Statistics::PhaseTimeTable& phaseTimes) {
291   TimeDuration total = 0;
292   for (phase = phases[phase].firstChild; phase != Phase::NONE;
293        phase = phases[phase].nextSibling) {
294     total += phaseTimes[phase];
295   }
296   return total;
297 }
298 
formatCompactSliceMessage() const299 UniqueChars Statistics::formatCompactSliceMessage() const {
300   // Skip if we OOM'ed.
301   if (slices_.length() == 0) {
302     return UniqueChars(nullptr);
303   }
304 
305   const size_t index = slices_.length() - 1;
306   const SliceData& slice = slices_.back();
307 
308   char budgetDescription[200];
309   slice.budget.describe(budgetDescription, sizeof(budgetDescription) - 1);
310 
311   const char* format =
312       "GC Slice %u - Pause: %.3fms of %s budget (@ %.3fms); Reason: %s; Reset: "
313       "%s%s; Times: ";
314   char buffer[1024];
315   SprintfLiteral(buffer, format, index, t(slice.duration()), budgetDescription,
316                  t(slice.start - slices_[0].start),
317                  ExplainGCReason(slice.reason),
318                  slice.wasReset() ? "yes - " : "no",
319                  slice.wasReset() ? ExplainAbortReason(slice.resetReason) : "");
320 
321   FragmentVector fragments;
322   if (!fragments.append(DuplicateString(buffer)) ||
323       !fragments.append(
324           formatCompactSlicePhaseTimes(slices_[index].phaseTimes))) {
325     return UniqueChars(nullptr);
326   }
327   return Join(fragments);
328 }
329 
formatCompactSummaryMessage() const330 UniqueChars Statistics::formatCompactSummaryMessage() const {
331   FragmentVector fragments;
332   if (!fragments.append(DuplicateString("Summary - "))) {
333     return UniqueChars(nullptr);
334   }
335 
336   TimeDuration total, longest;
337   gcDuration(&total, &longest);
338 
339   const double mmu20 = computeMMU(TimeDuration::FromMilliseconds(20));
340   const double mmu50 = computeMMU(TimeDuration::FromMilliseconds(50));
341 
342   char buffer[1024];
343   if (!nonincremental()) {
344     SprintfLiteral(buffer,
345                    "Max Pause: %.3fms; MMU 20ms: %.1f%%; MMU 50ms: %.1f%%; "
346                    "Total: %.3fms; ",
347                    t(longest), mmu20 * 100., mmu50 * 100., t(total));
348   } else {
349     SprintfLiteral(buffer, "Non-Incremental: %.3fms (%s); ", t(total),
350                    ExplainAbortReason(nonincrementalReason_));
351   }
352   if (!fragments.append(DuplicateString(buffer))) {
353     return UniqueChars(nullptr);
354   }
355 
356   SprintfLiteral(buffer,
357                  "Zones: %d of %d (-%d); Compartments: %d of %d (-%d); "
358                  "HeapSize: %.3f MiB; "
359                  "HeapChange (abs): %+d (%u); ",
360                  zoneStats.collectedZoneCount, zoneStats.zoneCount,
361                  zoneStats.sweptZoneCount, zoneStats.collectedCompartmentCount,
362                  zoneStats.compartmentCount, zoneStats.sweptCompartmentCount,
363                  double(preTotalHeapBytes) / BYTES_PER_MB,
364                  int32_t(counts[COUNT_NEW_CHUNK] - counts[COUNT_DESTROY_CHUNK]),
365                  counts[COUNT_NEW_CHUNK] + counts[COUNT_DESTROY_CHUNK]);
366   if (!fragments.append(DuplicateString(buffer))) {
367     return UniqueChars(nullptr);
368   }
369 
370   MOZ_ASSERT_IF(counts[COUNT_ARENA_RELOCATED], gckind == GC_SHRINK);
371   if (gckind == GC_SHRINK) {
372     SprintfLiteral(
373         buffer, "Kind: %s; Relocated: %.3f MiB; ",
374         ExplainInvocationKind(gckind),
375         double(ArenaSize * counts[COUNT_ARENA_RELOCATED]) / BYTES_PER_MB);
376     if (!fragments.append(DuplicateString(buffer))) {
377       return UniqueChars(nullptr);
378     }
379   }
380 
381   return Join(fragments);
382 }
383 
formatCompactSlicePhaseTimes(const PhaseTimeTable & phaseTimes) const384 UniqueChars Statistics::formatCompactSlicePhaseTimes(
385     const PhaseTimeTable& phaseTimes) const {
386   static const TimeDuration MaxUnaccountedTime =
387       TimeDuration::FromMicroseconds(100);
388 
389   FragmentVector fragments;
390   char buffer[128];
391   for (auto phase : AllPhases()) {
392     DebugOnly<uint8_t> level = phases[phase].depth;
393     MOZ_ASSERT(level < 4);
394 
395     TimeDuration ownTime = phaseTimes[phase];
396     TimeDuration childTime = SumChildTimes(phase, phaseTimes);
397     if (ownTime > MaxUnaccountedTime) {
398       SprintfLiteral(buffer, "%s: %.3fms", phases[phase].name, t(ownTime));
399       if (!fragments.append(DuplicateString(buffer))) {
400         return UniqueChars(nullptr);
401       }
402 
403       if (childTime && (ownTime - childTime) > MaxUnaccountedTime) {
404         MOZ_ASSERT(level < 3);
405         SprintfLiteral(buffer, "%s: %.3fms", "Other", t(ownTime - childTime));
406         if (!fragments.append(DuplicateString(buffer))) {
407           return UniqueChars(nullptr);
408         }
409       }
410     }
411   }
412   return Join(fragments, ", ");
413 }
414 
formatDetailedMessage() const415 UniqueChars Statistics::formatDetailedMessage() const {
416   FragmentVector fragments;
417 
418   if (!fragments.append(formatDetailedDescription())) {
419     return UniqueChars(nullptr);
420   }
421 
422   if (!slices_.empty()) {
423     for (unsigned i = 0; i < slices_.length(); i++) {
424       if (!fragments.append(formatDetailedSliceDescription(i, slices_[i]))) {
425         return UniqueChars(nullptr);
426       }
427       if (!fragments.append(formatDetailedPhaseTimes(slices_[i].phaseTimes))) {
428         return UniqueChars(nullptr);
429       }
430     }
431   }
432   if (!fragments.append(formatDetailedTotals())) {
433     return UniqueChars(nullptr);
434   }
435   if (!fragments.append(formatDetailedPhaseTimes(phaseTimes))) {
436     return UniqueChars(nullptr);
437   }
438 
439   return Join(fragments);
440 }
441 
formatDetailedDescription() const442 UniqueChars Statistics::formatDetailedDescription() const {
443   TimeDuration sccTotal, sccLongest;
444   sccDurations(&sccTotal, &sccLongest);
445 
446   const double mmu20 = computeMMU(TimeDuration::FromMilliseconds(20));
447   const double mmu50 = computeMMU(TimeDuration::FromMilliseconds(50));
448 
449   const char* format =
450       "=================================================================\n\
451   Invocation Kind: %s\n\
452   Reason: %s\n\
453   Incremental: %s%s\n\
454   Zones Collected: %d of %d (-%d)\n\
455   Compartments Collected: %d of %d (-%d)\n\
456   MinorGCs since last GC: %d\n\
457   Store Buffer Overflows: %d\n\
458   MMU 20ms:%.1f%%; 50ms:%.1f%%\n\
459   SCC Sweep Total (MaxPause): %.3fms (%.3fms)\n\
460   HeapSize: %.3f MiB\n\
461   Chunk Delta (magnitude): %+d  (%d)\n\
462   Arenas Relocated: %.3f MiB\n\
463 ";
464 
465   char buffer[1024];
466   SprintfLiteral(
467       buffer, format, ExplainInvocationKind(gckind),
468       ExplainGCReason(slices_[0].reason), nonincremental() ? "no - " : "yes",
469       nonincremental() ? ExplainAbortReason(nonincrementalReason_) : "",
470       zoneStats.collectedZoneCount, zoneStats.zoneCount,
471       zoneStats.sweptZoneCount, zoneStats.collectedCompartmentCount,
472       zoneStats.compartmentCount, zoneStats.sweptCompartmentCount,
473       getCount(COUNT_MINOR_GC), getCount(COUNT_STOREBUFFER_OVERFLOW),
474       mmu20 * 100., mmu50 * 100., t(sccTotal), t(sccLongest),
475       double(preTotalHeapBytes) / BYTES_PER_MB,
476       getCount(COUNT_NEW_CHUNK) - getCount(COUNT_DESTROY_CHUNK),
477       getCount(COUNT_NEW_CHUNK) + getCount(COUNT_DESTROY_CHUNK),
478       double(ArenaSize * getCount(COUNT_ARENA_RELOCATED)) / BYTES_PER_MB);
479 
480   return DuplicateString(buffer);
481 }
482 
formatDetailedSliceDescription(unsigned i,const SliceData & slice) const483 UniqueChars Statistics::formatDetailedSliceDescription(
484     unsigned i, const SliceData& slice) const {
485   char budgetDescription[200];
486   slice.budget.describe(budgetDescription, sizeof(budgetDescription) - 1);
487 
488   const char* format =
489       "\
490   ---- Slice %u ----\n\
491     Reason: %s\n\
492     Trigger: %s\n\
493     Reset: %s%s\n\
494     State: %s -> %s\n\
495     Page Faults: %" PRIu64
496       "\n\
497     Pause: %.3fms of %s budget (@ %.3fms)\n\
498 ";
499 
500   char triggerBuffer[100] = "n/a";
501   if (slice.trigger) {
502     Trigger trigger = slice.trigger.value();
503     SprintfLiteral(triggerBuffer, "%.3f MiB of %.3f MiB threshold\n",
504                    double(trigger.amount) / BYTES_PER_MB,
505                    double(trigger.threshold) / BYTES_PER_MB);
506   }
507 
508   char buffer[1024];
509   SprintfLiteral(
510       buffer, format, i, ExplainGCReason(slice.reason), triggerBuffer,
511       slice.wasReset() ? "yes - " : "no",
512       slice.wasReset() ? ExplainAbortReason(slice.resetReason) : "",
513       gc::StateName(slice.initialState), gc::StateName(slice.finalState),
514       uint64_t(slice.endFaults - slice.startFaults), t(slice.duration()),
515       budgetDescription, t(slice.start - slices_[0].start));
516   return DuplicateString(buffer);
517 }
518 
IncludePhase(TimeDuration duration)519 static bool IncludePhase(TimeDuration duration) {
520   // Don't include durations that will print as "0.000ms".
521   return duration.ToMilliseconds() >= 0.001;
522 }
523 
formatDetailedPhaseTimes(const PhaseTimeTable & phaseTimes) const524 UniqueChars Statistics::formatDetailedPhaseTimes(
525     const PhaseTimeTable& phaseTimes) const {
526   static const TimeDuration MaxUnaccountedChildTime =
527       TimeDuration::FromMicroseconds(50);
528 
529   FragmentVector fragments;
530   char buffer[128];
531   for (auto phase : AllPhases()) {
532     uint8_t level = phases[phase].depth;
533     TimeDuration ownTime = phaseTimes[phase];
534     TimeDuration childTime = SumChildTimes(phase, phaseTimes);
535     if (IncludePhase(ownTime)) {
536       SprintfLiteral(buffer, "      %*s%s: %.3fms\n", level * 2, "",
537                      phases[phase].name, t(ownTime));
538       if (!fragments.append(DuplicateString(buffer))) {
539         return UniqueChars(nullptr);
540       }
541 
542       if (childTime && (ownTime - childTime) > MaxUnaccountedChildTime) {
543         SprintfLiteral(buffer, "      %*s%s: %.3fms\n", (level + 1) * 2, "",
544                        "Other", t(ownTime - childTime));
545         if (!fragments.append(DuplicateString(buffer))) {
546           return UniqueChars(nullptr);
547         }
548       }
549     }
550   }
551   return Join(fragments);
552 }
553 
formatDetailedTotals() const554 UniqueChars Statistics::formatDetailedTotals() const {
555   TimeDuration total, longest;
556   gcDuration(&total, &longest);
557 
558   const char* format =
559       "\
560   ---- Totals ----\n\
561     Total Time: %.3fms\n\
562     Max Pause: %.3fms\n\
563 ";
564   char buffer[1024];
565   SprintfLiteral(buffer, format, t(total), t(longest));
566   return DuplicateString(buffer);
567 }
568 
formatJsonSlice(size_t sliceNum,JSONPrinter & json) const569 void Statistics::formatJsonSlice(size_t sliceNum, JSONPrinter& json) const {
570   /*
571    * We number each of the slice properties to keep the code in
572    * GCTelemetry.jsm in sync.  See MAX_SLICE_KEYS.
573    */
574   json.beginObject();
575   formatJsonSliceDescription(sliceNum, slices_[sliceNum], json);  // # 1-11
576 
577   json.beginObjectProperty("times");  // # 12
578   formatJsonPhaseTimes(slices_[sliceNum].phaseTimes, json);
579   json.endObject();
580 
581   json.endObject();
582 }
583 
renderJsonSlice(size_t sliceNum) const584 UniqueChars Statistics::renderJsonSlice(size_t sliceNum) const {
585   Sprinter printer(nullptr, false);
586   if (!printer.init()) {
587     return UniqueChars(nullptr);
588   }
589   JSONPrinter json(printer);
590 
591   formatJsonSlice(sliceNum, json);
592   return printer.release();
593 }
594 
renderNurseryJson() const595 UniqueChars Statistics::renderNurseryJson() const {
596   Sprinter printer(nullptr, false);
597   if (!printer.init()) {
598     return UniqueChars(nullptr);
599   }
600   JSONPrinter json(printer);
601   gc->nursery().renderProfileJSON(json);
602   return printer.release();
603 }
604 
605 #ifdef DEBUG
writeLogMessage(const char * fmt,...)606 void Statistics::writeLogMessage(const char* fmt, ...) {
607   va_list args;
608   va_start(args, fmt);
609   if (gcDebugFile) {
610     TimeDuration sinceStart = TimeStamp::Now() - TimeStamp::ProcessCreation();
611     fprintf(gcDebugFile, "%12.3f: ", sinceStart.ToMicroseconds());
612     vfprintf(gcDebugFile, fmt, args);
613     fprintf(gcDebugFile, "\n");
614     fflush(gcDebugFile);
615   }
616   va_end(args);
617 }
618 #endif
619 
renderJsonMessage(uint64_t timestamp,Statistics::JSONUse use) const620 UniqueChars Statistics::renderJsonMessage(uint64_t timestamp,
621                                           Statistics::JSONUse use) const {
622   /*
623    * The format of the JSON message is specified by the GCMajorMarkerPayload
624    * type in profiler.firefox.com
625    * https://github.com/firefox-devtools/profiler/blob/master/src/types/markers.js#L62
626    *
627    * All the properties listed here are created within the timings property
628    * of the GCMajor marker.
629    */
630   if (aborted) {
631     return DuplicateString("{status:\"aborted\"}");  // May return nullptr
632   }
633 
634   Sprinter printer(nullptr, false);
635   if (!printer.init()) {
636     return UniqueChars(nullptr);
637   }
638   JSONPrinter json(printer);
639 
640   json.beginObject();
641   json.property("status", "completed");         // JSON Key #1
642   formatJsonDescription(timestamp, json, use);  // #2-22
643 
644   if (use == Statistics::JSONUse::TELEMETRY) {
645     json.beginListProperty("slices_list");  // #23
646     for (unsigned i = 0; i < slices_.length(); i++) {
647       formatJsonSlice(i, json);
648     }
649     json.endList();
650   }
651 
652   json.beginObjectProperty("totals");  // #24
653   formatJsonPhaseTimes(phaseTimes, json);
654   json.endObject();
655 
656   json.endObject();
657 
658   return printer.release();
659 }
660 
formatJsonDescription(uint64_t timestamp,JSONPrinter & json,JSONUse use) const661 void Statistics::formatJsonDescription(uint64_t timestamp, JSONPrinter& json,
662                                        JSONUse use) const {
663   // If you change JSON properties here, please update:
664   // Telemetry ping code:
665   //   toolkit/components/telemetry/other/GCTelemetry.jsm
666   // Telemetry documentation:
667   //   toolkit/components/telemetry/docs/data/main-ping.rst
668   // Telemetry tests:
669   //   toolkit/components/telemetry/tests/browser/browser_TelemetryGC.js,
670   //   toolkit/components/telemetry/tests/unit/test_TelemetryGC.js
671   // Firefox Profiler:
672   //   https://github.com/firefox-devtools/profiler
673   //
674   // Please also number each property to help correctly maintain the Telemetry
675   // ping code
676 
677   json.property("timestamp", timestamp);  // # JSON Key #2
678 
679   TimeDuration total, longest;
680   gcDuration(&total, &longest);
681   json.property("max_pause", longest, JSONPrinter::MILLISECONDS);  // #3
682   json.property("total_time", total, JSONPrinter::MILLISECONDS);   // #4
683   // We might be able to omit reason if profiler.firefox.com was able to retrive
684   // it from the first slice.  But it doesn't do this yet.
685   json.property("reason", ExplainGCReason(slices_[0].reason));      // #5
686   json.property("zones_collected", zoneStats.collectedZoneCount);   // #6
687   json.property("total_zones", zoneStats.zoneCount);                // #7
688   json.property("total_compartments", zoneStats.compartmentCount);  // #8
689   json.property("minor_gcs", getCount(COUNT_MINOR_GC));             // #9
690   uint32_t storebufferOverflows = getCount(COUNT_STOREBUFFER_OVERFLOW);
691   if (storebufferOverflows) {
692     json.property("store_buffer_overflows", storebufferOverflows);  // #10
693   }
694   json.property("slices", slices_.length());  // #11
695 
696   const double mmu20 = computeMMU(TimeDuration::FromMilliseconds(20));
697   const double mmu50 = computeMMU(TimeDuration::FromMilliseconds(50));
698   json.property("mmu_20ms", int(mmu20 * 100));  // #12
699   json.property("mmu_50ms", int(mmu50 * 100));  // #13
700 
701   TimeDuration sccTotal, sccLongest;
702   sccDurations(&sccTotal, &sccLongest);
703   json.property("scc_sweep_total", sccTotal, JSONPrinter::MILLISECONDS);  // #14
704   json.property("scc_sweep_max_pause", sccLongest,
705                 JSONPrinter::MILLISECONDS);  // #15
706 
707   if (nonincrementalReason_ != AbortReason::None) {
708     json.property("nonincremental_reason",
709                   ExplainAbortReason(nonincrementalReason_));  // #16
710   }
711   json.property("allocated_bytes", preTotalHeapBytes);  // #17
712   if (use == Statistics::JSONUse::PROFILER) {
713     json.property("post_heap_size", postTotalHeapBytes);
714   }
715 
716   uint32_t addedChunks = getCount(COUNT_NEW_CHUNK);
717   if (addedChunks) {
718     json.property("added_chunks", addedChunks);  // #18
719   }
720   uint32_t removedChunks = getCount(COUNT_DESTROY_CHUNK);
721   if (removedChunks) {
722     json.property("removed_chunks", removedChunks);  // #19
723   }
724   json.property("major_gc_number", startingMajorGCNumber);  // #20
725   json.property("minor_gc_number", startingMinorGCNumber);  // #21
726   json.property("slice_number", startingSliceNumber);       // #22
727 }
728 
formatJsonSliceDescription(unsigned i,const SliceData & slice,JSONPrinter & json) const729 void Statistics::formatJsonSliceDescription(unsigned i, const SliceData& slice,
730                                             JSONPrinter& json) const {
731   // If you change JSON properties here, please update:
732   // Telemetry ping code:
733   //   toolkit/components/telemetry/other/GCTelemetry.jsm
734   // Telemetry documentation:
735   //   toolkit/components/telemetry/docs/data/main-ping.rst
736   // Telemetry tests:
737   //   toolkit/components/telemetry/tests/browser/browser_TelemetryGC.js,
738   //   toolkit/components/telemetry/tests/unit/test_TelemetryGC.js
739   // Firefox Profiler:
740   //   https://github.com/firefox-devtools/profiler
741   //
742   char budgetDescription[200];
743   slice.budget.describe(budgetDescription, sizeof(budgetDescription) - 1);
744   TimeStamp originTime = TimeStamp::ProcessCreation();
745 
746   json.property("slice", i);  // JSON Property #1
747   json.property("pause", slice.duration(), JSONPrinter::MILLISECONDS);  // #2
748   json.property("reason", ExplainGCReason(slice.reason));               // #3
749   json.property("initial_state", gc::StateName(slice.initialState));    // #4
750   json.property("final_state", gc::StateName(slice.finalState));        // #5
751   json.property("budget", budgetDescription);                           // #6
752   json.property("major_gc_number", startingMajorGCNumber);              // #7
753   if (slice.trigger) {
754     Trigger trigger = slice.trigger.value();
755     json.property("trigger_amount", trigger.amount);        // #8
756     json.property("trigger_threshold", trigger.threshold);  // #9
757   }
758   int64_t numFaults = slice.endFaults - slice.startFaults;
759   if (numFaults != 0) {
760     json.property("page_faults", numFaults);  // #10
761   }
762   json.property("start_timestamp", slice.start - originTime,
763                 JSONPrinter::SECONDS);  // #11
764 }
765 
formatJsonPhaseTimes(const PhaseTimeTable & phaseTimes,JSONPrinter & json) const766 void Statistics::formatJsonPhaseTimes(const PhaseTimeTable& phaseTimes,
767                                       JSONPrinter& json) const {
768   for (auto phase : AllPhases()) {
769     TimeDuration ownTime = phaseTimes[phase];
770     if (!ownTime.IsZero()) {
771       json.property(phases[phase].path, ownTime, JSONPrinter::MILLISECONDS);
772     }
773   }
774 }
775 
Statistics(GCRuntime * gc)776 Statistics::Statistics(GCRuntime* gc)
777     : gc(gc),
778       gcTimerFile(nullptr),
779       gcDebugFile(nullptr),
780       nonincrementalReason_(gc::AbortReason::None),
781       allocsSinceMinorGC({0, 0}),
782       preTotalHeapBytes(0),
783       postTotalHeapBytes(0),
784       preCollectedHeapBytes(0),
785       startingMinorGCNumber(0),
786       startingMajorGCNumber(0),
787       startingSliceNumber(0),
788       maxPauseInInterval(0),
789       sliceCallback(nullptr),
790       nurseryCollectionCallback(nullptr),
791       aborted(false),
792       enableProfiling_(false),
793       sliceCount_(0) {
794   for (auto& count : counts) {
795     count = 0;
796   }
797 
798   for (auto& stat : stats) {
799     stat = 0;
800   }
801 
802 #ifdef DEBUG
803   for (const auto& duration : totalTimes_) {
804     using ElementType = std::remove_reference_t<decltype(duration)>;
805     static_assert(!std::is_trivially_constructible_v<ElementType>,
806                   "Statistics::Statistics will only initialize "
807                   "totalTimes_'s elements if their default constructor is "
808                   "non-trivial");
809     MOZ_ASSERT(duration.IsZero(),
810                "totalTimes_ default-initialization should have "
811                "default-initialized every element of totalTimes_ to zero");
812   }
813 #endif
814 
815   MOZ_ALWAYS_TRUE(phaseStack.reserve(MAX_PHASE_NESTING));
816   MOZ_ALWAYS_TRUE(suspendedPhases.reserve(MAX_SUSPENDED_PHASES));
817 
818   gcTimerFile = MaybeOpenFileFromEnv("MOZ_GCTIMER");
819   gcDebugFile = MaybeOpenFileFromEnv("JS_GC_DEBUG");
820 
821   const char* env = getenv("JS_GC_PROFILE");
822   if (env) {
823     if (0 == strcmp(env, "help")) {
824       fprintf(stderr,
825               "JS_GC_PROFILE=N\n"
826               "\tReport major GC's taking more than N milliseconds.\n");
827       exit(0);
828     }
829     enableProfiling_ = true;
830     profileThreshold_ = TimeDuration::FromMilliseconds(atoi(env));
831   }
832 }
833 
~Statistics()834 Statistics::~Statistics() {
835   if (gcTimerFile && gcTimerFile != stdout && gcTimerFile != stderr) {
836     fclose(gcTimerFile);
837   }
838   if (gcDebugFile && gcDebugFile != stdout && gcDebugFile != stderr) {
839     fclose(gcDebugFile);
840   }
841 }
842 
843 /* static */
initialize()844 bool Statistics::initialize() {
845 #ifdef DEBUG
846   // Sanity check generated tables.
847   for (auto i : AllPhases()) {
848     auto parent = phases[i].parent;
849     if (parent != Phase::NONE) {
850       MOZ_ASSERT(phases[i].depth == phases[parent].depth + 1);
851     }
852     auto firstChild = phases[i].firstChild;
853     if (firstChild != Phase::NONE) {
854       MOZ_ASSERT(i == phases[firstChild].parent);
855       MOZ_ASSERT(phases[i].depth == phases[firstChild].depth - 1);
856     }
857     auto nextSibling = phases[i].nextSibling;
858     if (nextSibling != Phase::NONE) {
859       MOZ_ASSERT(parent == phases[nextSibling].parent);
860       MOZ_ASSERT(phases[i].depth == phases[nextSibling].depth);
861     }
862     auto nextWithPhaseKind = phases[i].nextWithPhaseKind;
863     if (nextWithPhaseKind != Phase::NONE) {
864       MOZ_ASSERT(phases[i].phaseKind == phases[nextWithPhaseKind].phaseKind);
865       MOZ_ASSERT(parent != phases[nextWithPhaseKind].parent);
866     }
867   }
868   for (auto i : AllPhaseKinds()) {
869     MOZ_ASSERT(phases[phaseKinds[i].firstPhase].phaseKind == i);
870     for (auto j : AllPhaseKinds()) {
871       MOZ_ASSERT_IF(i != j, phaseKinds[i].telemetryBucket !=
872                                 phaseKinds[j].telemetryBucket);
873     }
874   }
875 #endif
876 
877   return true;
878 }
879 
setSliceCallback(JS::GCSliceCallback newCallback)880 JS::GCSliceCallback Statistics::setSliceCallback(
881     JS::GCSliceCallback newCallback) {
882   JS::GCSliceCallback oldCallback = sliceCallback;
883   sliceCallback = newCallback;
884   return oldCallback;
885 }
886 
setNurseryCollectionCallback(JS::GCNurseryCollectionCallback newCallback)887 JS::GCNurseryCollectionCallback Statistics::setNurseryCollectionCallback(
888     JS::GCNurseryCollectionCallback newCallback) {
889   auto oldCallback = nurseryCollectionCallback;
890   nurseryCollectionCallback = newCallback;
891   return oldCallback;
892 }
893 
clearMaxGCPauseAccumulator()894 TimeDuration Statistics::clearMaxGCPauseAccumulator() {
895   TimeDuration prior = maxPauseInInterval;
896   maxPauseInInterval = 0;
897   return prior;
898 }
899 
getMaxGCPauseSinceClear()900 TimeDuration Statistics::getMaxGCPauseSinceClear() {
901   return maxPauseInInterval;
902 }
903 
904 // Sum up the time for a phase, including instances of the phase with different
905 // parents.
SumPhase(PhaseKind phaseKind,const Statistics::PhaseTimeTable & times)906 static TimeDuration SumPhase(PhaseKind phaseKind,
907                              const Statistics::PhaseTimeTable& times) {
908   TimeDuration sum;
909   for (PhaseIter phase(phaseKind); !phase.done(); phase.next()) {
910     sum += times[phase];
911   }
912   return sum;
913 }
914 
CheckSelfTime(Phase parent,Phase child,const Statistics::PhaseTimeTable & times,const Statistics::PhaseTimeTable & selfTimes,TimeDuration childTime)915 static bool CheckSelfTime(Phase parent, Phase child,
916                           const Statistics::PhaseTimeTable& times,
917                           const Statistics::PhaseTimeTable& selfTimes,
918                           TimeDuration childTime) {
919   if (selfTimes[parent] < childTime) {
920     fprintf(
921         stderr,
922         "Parent %s time = %.3fms with %.3fms remaining, child %s time %.3fms\n",
923         phases[parent].name, times[parent].ToMilliseconds(),
924         selfTimes[parent].ToMilliseconds(), phases[child].name,
925         childTime.ToMilliseconds());
926     fflush(stderr);
927     return false;
928   }
929 
930   return true;
931 }
932 
933 using PhaseKindTimes =
934     EnumeratedArray<PhaseKind, PhaseKind::LIMIT, TimeDuration>;
935 
FindLongestPhaseKind(const PhaseKindTimes & times)936 static PhaseKind FindLongestPhaseKind(const PhaseKindTimes& times) {
937   TimeDuration longestTime;
938   PhaseKind phaseKind = PhaseKind::NONE;
939   for (auto i : MajorGCPhaseKinds()) {
940     if (times[i] > longestTime) {
941       longestTime = times[i];
942       phaseKind = i;
943     }
944   }
945 
946   return phaseKind;
947 }
948 
LongestPhaseSelfTimeInMajorGC(const Statistics::PhaseTimeTable & times)949 static PhaseKind LongestPhaseSelfTimeInMajorGC(
950     const Statistics::PhaseTimeTable& times) {
951   // Start with total times per expanded phase, including children's times.
952   Statistics::PhaseTimeTable selfTimes(times);
953 
954   // We have the total time spent in each phase, including descendant times.
955   // Loop over the children and subtract their times from their parent's self
956   // time.
957   for (auto i : AllPhases()) {
958     Phase parent = phases[i].parent;
959     if (parent != Phase::NONE) {
960       bool ok = CheckSelfTime(parent, i, times, selfTimes, times[i]);
961 
962       // This happens very occasionally in release builds and frequently
963       // in Windows debug builds. Skip collecting longest phase telemetry
964       // if it does.
965 #ifndef XP_WIN
966       MOZ_ASSERT(ok, "Inconsistent time data; see bug 1400153");
967 #endif
968       if (!ok) {
969         return PhaseKind::NONE;
970       }
971 
972       selfTimes[parent] -= times[i];
973     }
974   }
975 
976   // Sum expanded phases corresponding to the same phase.
977   PhaseKindTimes phaseKindTimes;
978   for (auto i : AllPhaseKinds()) {
979     phaseKindTimes[i] = SumPhase(i, selfTimes);
980   }
981 
982   return FindLongestPhaseKind(phaseKindTimes);
983 }
984 
PhaseMax(PhaseKind phaseKind,const Statistics::PhaseTimeTable & times)985 static TimeDuration PhaseMax(PhaseKind phaseKind,
986                              const Statistics::PhaseTimeTable& times) {
987   TimeDuration max;
988   for (PhaseIter phase(phaseKind); !phase.done(); phase.next()) {
989     max = std::max(max, times[phase]);
990   }
991 
992   return max;
993 }
994 
LongestParallelPhaseKind(const Statistics::PhaseTimeTable & times)995 static PhaseKind LongestParallelPhaseKind(
996     const Statistics::PhaseTimeTable& times) {
997   // Find longest time for each phase kind.
998   PhaseKindTimes phaseKindTimes;
999   for (auto i : AllPhaseKinds()) {
1000     phaseKindTimes[i] = PhaseMax(i, times);
1001   }
1002 
1003   return FindLongestPhaseKind(phaseKindTimes);
1004 }
1005 
printStats()1006 void Statistics::printStats() {
1007   if (aborted) {
1008     fprintf(gcTimerFile,
1009             "OOM during GC statistics collection. The report is unavailable "
1010             "for this GC.\n");
1011   } else {
1012     UniqueChars msg = formatDetailedMessage();
1013     if (msg) {
1014       double secSinceStart =
1015           (slices_[0].start - TimeStamp::ProcessCreation()).ToSeconds();
1016       fprintf(gcTimerFile, "GC(T+%.3fs) %s\n", secSinceStart, msg.get());
1017     }
1018   }
1019   fflush(gcTimerFile);
1020 }
1021 
beginGC(JSGCInvocationKind kind,const TimeStamp & currentTime)1022 void Statistics::beginGC(JSGCInvocationKind kind,
1023                          const TimeStamp& currentTime) {
1024   slices_.clearAndFree();
1025   sccTimes.clearAndFree();
1026   gckind = kind;
1027   nonincrementalReason_ = gc::AbortReason::None;
1028 
1029   preTotalHeapBytes = gc->heapSize.bytes();
1030 
1031   preCollectedHeapBytes = 0;
1032 
1033   startingMajorGCNumber = gc->majorGCCount();
1034   startingSliceNumber = gc->gcNumber();
1035 
1036   if (gc->lastGCEndTime()) {
1037     timeSinceLastGC = currentTime - gc->lastGCEndTime();
1038   }
1039 }
1040 
measureInitialHeapSize()1041 void Statistics::measureInitialHeapSize() {
1042   MOZ_ASSERT(preCollectedHeapBytes == 0);
1043   for (GCZonesIter zone(gc, WithAtoms); !zone.done(); zone.next()) {
1044     preCollectedHeapBytes += zone->gcHeapSize.bytes();
1045   }
1046 }
1047 
adoptHeapSizeDuringIncrementalGC(Zone * mergedZone)1048 void Statistics::adoptHeapSizeDuringIncrementalGC(Zone* mergedZone) {
1049   // A zone is being merged into a zone that's currently being collected so we
1050   // need to adjust our record of the total size of heap for collected zones.
1051   MOZ_ASSERT(gc->isIncrementalGCInProgress());
1052   preCollectedHeapBytes += mergedZone->gcHeapSize.bytes();
1053 }
1054 
endGC()1055 void Statistics::endGC() {
1056   postTotalHeapBytes = gc->heapSize.bytes();
1057 
1058   sendGCTelemetry();
1059 }
1060 
sendGCTelemetry()1061 void Statistics::sendGCTelemetry() {
1062   JSRuntime* runtime = gc->rt;
1063   runtime->addTelemetry(JS_TELEMETRY_GC_IS_ZONE_GC,
1064                         !zoneStats.isFullCollection());
1065   TimeDuration prepareTotal = SumPhase(PhaseKind::PREPARE, phaseTimes);
1066   TimeDuration markTotal = SumPhase(PhaseKind::MARK, phaseTimes);
1067   TimeDuration markRootsTotal = SumPhase(PhaseKind::MARK_ROOTS, phaseTimes);
1068   TimeDuration markWeakTotal = phaseTimes[Phase::SWEEP_MARK_WEAK] +
1069                                phaseTimes[Phase::SWEEP_MARK_GRAY_WEAK];
1070   TimeDuration markGrayTotal = phaseTimes[Phase::SWEEP_MARK_GRAY] +
1071                                phaseTimes[Phase::SWEEP_MARK_GRAY_WEAK];
1072   size_t markCount = gc->marker.getMarkCount();
1073   double markRate = markCount / t(markTotal);
1074   runtime->addTelemetry(JS_TELEMETRY_GC_PREPARE_MS, t(prepareTotal));
1075   runtime->addTelemetry(JS_TELEMETRY_GC_MARK_MS, t(markTotal));
1076   runtime->addTelemetry(JS_TELEMETRY_GC_MARK_RATE, markRate);
1077   runtime->addTelemetry(JS_TELEMETRY_GC_MARK_RATE_2, markRate);
1078   runtime->addTelemetry(JS_TELEMETRY_GC_SWEEP_MS, t(phaseTimes[Phase::SWEEP]));
1079   if (gc->didCompactZones()) {
1080     runtime->addTelemetry(JS_TELEMETRY_GC_COMPACT_MS,
1081                           t(phaseTimes[Phase::COMPACT]));
1082   }
1083   runtime->addTelemetry(JS_TELEMETRY_GC_MARK_ROOTS_MS, t(markRootsTotal));
1084   runtime->addTelemetry(JS_TELEMETRY_GC_MARK_ROOTS_US,
1085                         markRootsTotal.ToMicroseconds());
1086   runtime->addTelemetry(JS_TELEMETRY_GC_MARK_GRAY_MS, t(markGrayTotal));
1087   runtime->addTelemetry(JS_TELEMETRY_GC_MARK_GRAY_MS_2, t(markGrayTotal));
1088   runtime->addTelemetry(JS_TELEMETRY_GC_MARK_WEAK_MS, t(markWeakTotal));
1089   runtime->addTelemetry(JS_TELEMETRY_GC_NON_INCREMENTAL, nonincremental());
1090   if (nonincremental()) {
1091     runtime->addTelemetry(JS_TELEMETRY_GC_NON_INCREMENTAL_REASON,
1092                           uint32_t(nonincrementalReason_));
1093   }
1094 
1095 #ifdef DEBUG
1096   // Reset happens non-incrementally, so only the last slice can be reset.
1097   for (size_t i = 0; i < slices_.length() - 1; i++) {
1098     MOZ_ASSERT(!slices_[i].wasReset());
1099   }
1100 #endif
1101   const auto& lastSlice = slices_.back();
1102   runtime->addTelemetry(JS_TELEMETRY_GC_RESET, lastSlice.wasReset());
1103   if (lastSlice.wasReset()) {
1104     runtime->addTelemetry(JS_TELEMETRY_GC_RESET_REASON,
1105                           uint32_t(lastSlice.resetReason));
1106   }
1107 
1108   TimeDuration total, longest;
1109   gcDuration(&total, &longest);
1110 
1111   runtime->addTelemetry(JS_TELEMETRY_GC_MS, t(total));
1112   runtime->addTelemetry(JS_TELEMETRY_GC_MAX_PAUSE_MS_2, t(longest));
1113 
1114   const double mmu50 = computeMMU(TimeDuration::FromMilliseconds(50));
1115   runtime->addTelemetry(JS_TELEMETRY_GC_MMU_50, mmu50 * 100);
1116 
1117   // Record scheduling telemetry for the main runtime but not for workers, which
1118   // are scheduled differently.
1119   if (!runtime->parentRuntime && timeSinceLastGC) {
1120     runtime->addTelemetry(JS_TELEMETRY_GC_TIME_BETWEEN_S,
1121                           timeSinceLastGC.ToSeconds());
1122     if (!nonincremental()) {
1123       runtime->addTelemetry(JS_TELEMETRY_GC_SLICE_COUNT, slices_.length());
1124     }
1125   }
1126 
1127   size_t bytesSurvived = 0;
1128   for (ZonesIter zone(runtime, WithAtoms); !zone.done(); zone.next()) {
1129     if (zone->wasCollected()) {
1130       bytesSurvived += zone->gcHeapSize.retainedBytes();
1131     }
1132   }
1133 
1134   MOZ_ASSERT(preCollectedHeapBytes >= bytesSurvived);
1135   double survialRate =
1136       100.0 * double(bytesSurvived) / double(preCollectedHeapBytes);
1137   runtime->addTelemetry(JS_TELEMETRY_GC_TENURED_SURVIVAL_RATE,
1138                         uint32_t(survialRate));
1139 
1140   // Calculate 'effectiveness' in MB / second, on main thread only for now.
1141   if (!runtime->parentRuntime) {
1142     size_t bytesFreed = preCollectedHeapBytes - bytesSurvived;
1143     TimeDuration clampedTotal =
1144         TimeDuration::Max(total, TimeDuration::FromMilliseconds(1));
1145     double effectiveness =
1146         (double(bytesFreed) / BYTES_PER_MB) / clampedTotal.ToSeconds();
1147     runtime->addTelemetry(JS_TELEMETRY_GC_EFFECTIVENESS,
1148                           uint32_t(effectiveness));
1149   }
1150 }
1151 
beginNurseryCollection(JS::GCReason reason)1152 void Statistics::beginNurseryCollection(JS::GCReason reason) {
1153   count(COUNT_MINOR_GC);
1154   startingMinorGCNumber = gc->minorGCCount();
1155   if (nurseryCollectionCallback) {
1156     (*nurseryCollectionCallback)(
1157         context(), JS::GCNurseryProgress::GC_NURSERY_COLLECTION_START, reason);
1158   }
1159 }
1160 
endNurseryCollection(JS::GCReason reason)1161 void Statistics::endNurseryCollection(JS::GCReason reason) {
1162   if (nurseryCollectionCallback) {
1163     (*nurseryCollectionCallback)(
1164         context(), JS::GCNurseryProgress::GC_NURSERY_COLLECTION_END, reason);
1165   }
1166 
1167   allocsSinceMinorGC = {0, 0};
1168 }
1169 
SliceData(SliceBudget budget,Maybe<Trigger> trigger,JS::GCReason reason,TimeStamp start,size_t startFaults,gc::State initialState)1170 Statistics::SliceData::SliceData(SliceBudget budget, Maybe<Trigger> trigger,
1171                                  JS::GCReason reason, TimeStamp start,
1172                                  size_t startFaults, gc::State initialState)
1173     : budget(budget),
1174       reason(reason),
1175       trigger(trigger),
1176       initialState(initialState),
1177       start(start),
1178       startFaults(startFaults) {}
1179 
beginSlice(const ZoneGCStats & zoneStats,JSGCInvocationKind gckind,SliceBudget budget,JS::GCReason reason)1180 void Statistics::beginSlice(const ZoneGCStats& zoneStats,
1181                             JSGCInvocationKind gckind, SliceBudget budget,
1182                             JS::GCReason reason) {
1183   MOZ_ASSERT(phaseStack.empty() ||
1184              (phaseStack.length() == 1 && phaseStack[0] == Phase::MUTATOR));
1185 
1186   this->zoneStats = zoneStats;
1187 
1188   TimeStamp currentTime = ReallyNow();
1189 
1190   bool first = !gc->isIncrementalGCInProgress();
1191   if (first) {
1192     beginGC(gckind, currentTime);
1193   }
1194 
1195   JSRuntime* runtime = gc->rt;
1196   if (!runtime->parentRuntime && !slices_.empty()) {
1197     TimeDuration timeSinceLastSlice = currentTime - slices_.back().end;
1198     runtime->addTelemetry(JS_TELEMETRY_GC_TIME_BETWEEN_SLICES_MS,
1199                           uint32_t(timeSinceLastSlice.ToMilliseconds()));
1200   }
1201 
1202   Maybe<Trigger> trigger = recordedTrigger;
1203   recordedTrigger.reset();
1204 
1205   if (!slices_.emplaceBack(budget, trigger, reason, currentTime,
1206                            GetPageFaultCount(), gc->state())) {
1207     // If we are OOM, set a flag to indicate we have missing slice data.
1208     aborted = true;
1209     return;
1210   }
1211 
1212   runtime->addTelemetry(JS_TELEMETRY_GC_REASON, uint32_t(reason));
1213 
1214   // Slice callbacks should only fire for the outermost level.
1215   bool wasFullGC = zoneStats.isFullCollection();
1216   if (sliceCallback) {
1217     JSContext* cx = context();
1218     JS::GCDescription desc(!wasFullGC, false, gckind, reason);
1219     if (first) {
1220       (*sliceCallback)(cx, JS::GC_CYCLE_BEGIN, desc);
1221     }
1222     (*sliceCallback)(cx, JS::GC_SLICE_BEGIN, desc);
1223   }
1224 
1225   writeLogMessage("begin slice");
1226 }
1227 
endSlice()1228 void Statistics::endSlice() {
1229   MOZ_ASSERT(phaseStack.empty() ||
1230              (phaseStack.length() == 1 && phaseStack[0] == Phase::MUTATOR));
1231 
1232   if (!aborted) {
1233     auto& slice = slices_.back();
1234     slice.end = ReallyNow();
1235     slice.endFaults = GetPageFaultCount();
1236     slice.finalState = gc->state();
1237 
1238     writeLogMessage("end slice");
1239 
1240     sendSliceTelemetry(slice);
1241 
1242     sliceCount_++;
1243   }
1244 
1245   bool last = !gc->isIncrementalGCInProgress();
1246   if (last) {
1247     if (gcTimerFile) {
1248       printStats();
1249     }
1250 
1251     if (!aborted) {
1252       endGC();
1253     }
1254   }
1255 
1256   if (enableProfiling_ && !aborted &&
1257       slices_.back().duration() >= profileThreshold_) {
1258     printSliceProfile();
1259   }
1260 
1261   // Slice callbacks should only fire for the outermost level.
1262   if (!aborted) {
1263     bool wasFullGC = zoneStats.isFullCollection();
1264     if (sliceCallback) {
1265       JSContext* cx = context();
1266       JS::GCDescription desc(!wasFullGC, last, gckind, slices_.back().reason);
1267       (*sliceCallback)(cx, JS::GC_SLICE_END, desc);
1268       if (last) {
1269         (*sliceCallback)(cx, JS::GC_CYCLE_END, desc);
1270       }
1271     }
1272   }
1273 
1274   // Do this after the slice callback since it uses these values.
1275   if (last) {
1276     for (auto& count : counts) {
1277       count = 0;
1278     }
1279 
1280     // Clear the timers at the end of a GC, preserving the data for
1281     // PhaseKind::MUTATOR.
1282     auto mutatorStartTime = phaseStartTimes[Phase::MUTATOR];
1283     auto mutatorTime = phaseTimes[Phase::MUTATOR];
1284 
1285     for (mozilla::TimeStamp& t : phaseStartTimes) {
1286       t = TimeStamp();
1287     }
1288 #ifdef DEBUG
1289     for (mozilla::TimeStamp& t : phaseEndTimes) {
1290       t = TimeStamp();
1291     }
1292 #endif
1293 
1294     for (TimeDuration& duration : phaseTimes) {
1295       duration = TimeDuration();
1296       MOZ_ASSERT(duration.IsZero());
1297     }
1298 
1299     phaseStartTimes[Phase::MUTATOR] = mutatorStartTime;
1300     phaseTimes[Phase::MUTATOR] = mutatorTime;
1301   }
1302 
1303   aborted = false;
1304 }
1305 
sendSliceTelemetry(const SliceData & slice)1306 void Statistics::sendSliceTelemetry(const SliceData& slice) {
1307   JSRuntime* runtime = gc->rt;
1308   TimeDuration sliceTime = slice.end - slice.start;
1309   runtime->addTelemetry(JS_TELEMETRY_GC_SLICE_MS, t(sliceTime));
1310 
1311   if (slice.budget.isTimeBudget()) {
1312     int64_t budget_ms = slice.budget.timeBudget.budget;
1313     runtime->addTelemetry(JS_TELEMETRY_GC_BUDGET_MS, budget_ms);
1314     runtime->addTelemetry(JS_TELEMETRY_GC_BUDGET_MS_2, budget_ms);
1315     if (IsCurrentlyAnimating(runtime->lastAnimationTime, slice.end)) {
1316       runtime->addTelemetry(JS_TELEMETRY_GC_ANIMATION_MS, t(sliceTime));
1317     }
1318 
1319     // Record any phase that goes 1.5 times or 5ms over its budget.
1320     double longSliceThreshold = std::min(1.5 * budget_ms, budget_ms + 5.0);
1321     if (sliceTime.ToMilliseconds() > longSliceThreshold) {
1322       PhaseKind longest = LongestPhaseSelfTimeInMajorGC(slice.phaseTimes);
1323       reportLongestPhaseInMajorGC(longest, JS_TELEMETRY_GC_SLOW_PHASE);
1324 
1325       // If the longest phase was waiting for parallel tasks then record the
1326       // longest task.
1327       if (longest == PhaseKind::JOIN_PARALLEL_TASKS) {
1328         PhaseKind longestParallel =
1329             LongestParallelPhaseKind(slice.maxParallelTimes);
1330         reportLongestPhaseInMajorGC(longestParallel, JS_TELEMETRY_GC_SLOW_TASK);
1331       }
1332     }
1333 
1334     // Record how long we went over budget.
1335     int64_t overrun = int64_t(sliceTime.ToMicroseconds()) - (1000 * budget_ms);
1336     if (overrun > 0) {
1337       runtime->addTelemetry(JS_TELEMETRY_GC_BUDGET_OVERRUN, uint32_t(overrun));
1338     }
1339   }
1340 }
1341 
reportLongestPhaseInMajorGC(PhaseKind longest,int telemetryId)1342 void Statistics::reportLongestPhaseInMajorGC(PhaseKind longest,
1343                                              int telemetryId) {
1344   JSRuntime* runtime = gc->rt;
1345   if (longest != PhaseKind::NONE) {
1346     uint8_t bucket = phaseKinds[longest].telemetryBucket;
1347     runtime->addTelemetry(telemetryId, bucket);
1348   }
1349 }
1350 
startTimingMutator()1351 bool Statistics::startTimingMutator() {
1352   if (phaseStack.length() != 0) {
1353     // Should only be called from outside of GC.
1354     MOZ_ASSERT(phaseStack.length() == 1);
1355     MOZ_ASSERT(phaseStack[0] == Phase::MUTATOR);
1356     return false;
1357   }
1358 
1359   MOZ_ASSERT(suspendedPhases.empty());
1360 
1361   timedGCTime = 0;
1362   phaseStartTimes[Phase::MUTATOR] = TimeStamp();
1363   phaseTimes[Phase::MUTATOR] = 0;
1364   timedGCStart = TimeStamp();
1365 
1366   beginPhase(PhaseKind::MUTATOR);
1367   return true;
1368 }
1369 
stopTimingMutator(double & mutator_ms,double & gc_ms)1370 bool Statistics::stopTimingMutator(double& mutator_ms, double& gc_ms) {
1371   // This should only be called from outside of GC, while timing the mutator.
1372   if (phaseStack.length() != 1 || phaseStack[0] != Phase::MUTATOR) {
1373     return false;
1374   }
1375 
1376   endPhase(PhaseKind::MUTATOR);
1377   mutator_ms = t(phaseTimes[Phase::MUTATOR]);
1378   gc_ms = t(timedGCTime);
1379 
1380   return true;
1381 }
1382 
suspendPhases(PhaseKind suspension)1383 void Statistics::suspendPhases(PhaseKind suspension) {
1384   MOZ_ASSERT(suspension == PhaseKind::EXPLICIT_SUSPENSION ||
1385              suspension == PhaseKind::IMPLICIT_SUSPENSION);
1386   while (!phaseStack.empty()) {
1387     MOZ_ASSERT(suspendedPhases.length() < MAX_SUSPENDED_PHASES);
1388     Phase parent = phaseStack.back();
1389     suspendedPhases.infallibleAppend(parent);
1390     recordPhaseEnd(parent);
1391   }
1392   suspendedPhases.infallibleAppend(lookupChildPhase(suspension));
1393 }
1394 
resumePhases()1395 void Statistics::resumePhases() {
1396   MOZ_ASSERT(suspendedPhases.back() == Phase::EXPLICIT_SUSPENSION ||
1397              suspendedPhases.back() == Phase::IMPLICIT_SUSPENSION);
1398   suspendedPhases.popBack();
1399 
1400   while (!suspendedPhases.empty() &&
1401          suspendedPhases.back() != Phase::EXPLICIT_SUSPENSION &&
1402          suspendedPhases.back() != Phase::IMPLICIT_SUSPENSION) {
1403     Phase resumePhase = suspendedPhases.popCopy();
1404     if (resumePhase == Phase::MUTATOR) {
1405       timedGCTime += ReallyNow() - timedGCStart;
1406     }
1407     recordPhaseBegin(resumePhase);
1408   }
1409 }
1410 
beginPhase(PhaseKind phaseKind)1411 void Statistics::beginPhase(PhaseKind phaseKind) {
1412   // No longer timing these phases. We should never see these.
1413   MOZ_ASSERT(phaseKind != PhaseKind::GC_BEGIN &&
1414              phaseKind != PhaseKind::GC_END);
1415 
1416   // PhaseKind::MUTATOR is suspended while performing GC.
1417   if (currentPhase() == Phase::MUTATOR) {
1418     suspendPhases(PhaseKind::IMPLICIT_SUSPENSION);
1419   }
1420 
1421   recordPhaseBegin(lookupChildPhase(phaseKind));
1422 }
1423 
recordPhaseBegin(Phase phase)1424 void Statistics::recordPhaseBegin(Phase phase) {
1425   MOZ_ASSERT(CurrentThreadCanAccessRuntime(gc->rt));
1426 
1427   // Guard against any other re-entry.
1428   MOZ_ASSERT(!phaseStartTimes[phase]);
1429 
1430   MOZ_ASSERT(phaseStack.length() < MAX_PHASE_NESTING);
1431 
1432   Phase current = currentPhase();
1433   MOZ_ASSERT(phases[phase].parent == current);
1434 
1435   TimeStamp now = ReallyNow();
1436 
1437   if (current != Phase::NONE) {
1438     MOZ_ASSERT(now >= phaseStartTimes[currentPhase()],
1439                "Inconsistent time data; see bug 1400153");
1440     if (now < phaseStartTimes[currentPhase()]) {
1441       now = phaseStartTimes[currentPhase()];
1442       aborted = true;
1443     }
1444   }
1445 
1446   phaseStack.infallibleAppend(phase);
1447   phaseStartTimes[phase] = now;
1448   writeLogMessage("begin: %s", phases[phase].path);
1449 }
1450 
recordPhaseEnd(Phase phase)1451 void Statistics::recordPhaseEnd(Phase phase) {
1452   MOZ_ASSERT(CurrentThreadCanAccessRuntime(gc->rt));
1453 
1454   MOZ_ASSERT(phaseStartTimes[phase]);
1455 
1456   TimeStamp now = ReallyNow();
1457 
1458   // Make sure this phase ends after it starts.
1459   MOZ_ASSERT(now >= phaseStartTimes[phase],
1460              "Inconsistent time data; see bug 1400153");
1461 
1462 #ifdef DEBUG
1463   // Make sure this phase ends after all of its children. Note that some
1464   // children might not have run in this instance, in which case they will
1465   // have run in a previous instance of this parent or not at all.
1466   for (Phase kid = phases[phase].firstChild; kid != Phase::NONE;
1467        kid = phases[kid].nextSibling) {
1468     if (phaseEndTimes[kid].IsNull()) {
1469       continue;
1470     }
1471     if (phaseEndTimes[kid] > now) {
1472       fprintf(stderr,
1473               "Parent %s ended at %.3fms, before child %s ended at %.3fms?\n",
1474               phases[phase].name, t(now - TimeStamp::ProcessCreation()),
1475               phases[kid].name,
1476               t(phaseEndTimes[kid] - TimeStamp::ProcessCreation()));
1477     }
1478     MOZ_ASSERT(phaseEndTimes[kid] <= now,
1479                "Inconsistent time data; see bug 1400153");
1480   }
1481 #endif
1482 
1483   if (now < phaseStartTimes[phase]) {
1484     now = phaseStartTimes[phase];
1485     aborted = true;
1486   }
1487 
1488   if (phase == Phase::MUTATOR) {
1489     timedGCStart = now;
1490   }
1491 
1492   phaseStack.popBack();
1493 
1494   TimeDuration t = now - phaseStartTimes[phase];
1495   if (!slices_.empty()) {
1496     slices_.back().phaseTimes[phase] += t;
1497   }
1498   phaseTimes[phase] += t;
1499   phaseStartTimes[phase] = TimeStamp();
1500 
1501 #ifdef DEBUG
1502   phaseEndTimes[phase] = now;
1503   writeLogMessage("end: %s", phases[phase].path);
1504 #endif
1505 }
1506 
endPhase(PhaseKind phaseKind)1507 void Statistics::endPhase(PhaseKind phaseKind) {
1508   Phase phase = currentPhase();
1509   MOZ_ASSERT(phase != Phase::NONE);
1510   MOZ_ASSERT(phases[phase].phaseKind == phaseKind);
1511 
1512   recordPhaseEnd(phase);
1513 
1514   // When emptying the stack, we may need to return to timing the mutator
1515   // (PhaseKind::MUTATOR).
1516   if (phaseStack.empty() && !suspendedPhases.empty() &&
1517       suspendedPhases.back() == Phase::IMPLICIT_SUSPENSION) {
1518     resumePhases();
1519   }
1520 }
1521 
recordParallelPhase(PhaseKind phaseKind,TimeDuration duration)1522 void Statistics::recordParallelPhase(PhaseKind phaseKind,
1523                                      TimeDuration duration) {
1524   MOZ_ASSERT(CurrentThreadCanAccessRuntime(gc->rt));
1525 
1526   if (aborted) {
1527     return;
1528   }
1529 
1530   // Record the maximum task time for each phase. Don't record times for parent
1531   // phases.
1532   Phase phase = lookupChildPhase(phaseKind);
1533   TimeDuration& time = slices_.back().maxParallelTimes[phase];
1534   time = std::max(time, duration);
1535 }
1536 
beginSCC()1537 TimeStamp Statistics::beginSCC() { return ReallyNow(); }
1538 
endSCC(unsigned scc,TimeStamp start)1539 void Statistics::endSCC(unsigned scc, TimeStamp start) {
1540   if (scc >= sccTimes.length() && !sccTimes.resize(scc + 1)) {
1541     return;
1542   }
1543 
1544   sccTimes[scc] += ReallyNow() - start;
1545 }
1546 
1547 /*
1548  * MMU (minimum mutator utilization) is a measure of how much garbage collection
1549  * is affecting the responsiveness of the system. MMU measurements are given
1550  * with respect to a certain window size. If we report MMU(50ms) = 80%, then
1551  * that means that, for any 50ms window of time, at least 80% of the window is
1552  * devoted to the mutator. In other words, the GC is running for at most 20% of
1553  * the window, or 10ms. The GC can run multiple slices during the 50ms window
1554  * as long as the total time it spends is at most 10ms.
1555  */
computeMMU(TimeDuration window) const1556 double Statistics::computeMMU(TimeDuration window) const {
1557   MOZ_ASSERT(!slices_.empty());
1558 
1559   TimeDuration gc = slices_[0].end - slices_[0].start;
1560   TimeDuration gcMax = gc;
1561 
1562   if (gc >= window) {
1563     return 0.0;
1564   }
1565 
1566   int startIndex = 0;
1567   for (size_t endIndex = 1; endIndex < slices_.length(); endIndex++) {
1568     auto* startSlice = &slices_[startIndex];
1569     auto& endSlice = slices_[endIndex];
1570     gc += endSlice.end - endSlice.start;
1571 
1572     while (endSlice.end - startSlice->end >= window) {
1573       gc -= startSlice->end - startSlice->start;
1574       startSlice = &slices_[++startIndex];
1575     }
1576 
1577     TimeDuration cur = gc;
1578     if (endSlice.end - startSlice->start > window) {
1579       cur -= (endSlice.end - startSlice->start - window);
1580     }
1581     if (cur > gcMax) {
1582       gcMax = cur;
1583     }
1584   }
1585 
1586   return double((window - gcMax) / window);
1587 }
1588 
maybePrintProfileHeaders()1589 void Statistics::maybePrintProfileHeaders() {
1590   static int printedHeader = 0;
1591   if ((printedHeader++ % 200) == 0) {
1592     printProfileHeader();
1593     if (gc->nursery().enableProfiling()) {
1594       Nursery::printProfileHeader();
1595     }
1596   }
1597 }
1598 
printProfileHeader()1599 void Statistics::printProfileHeader() {
1600   if (!enableProfiling_) {
1601     return;
1602   }
1603 
1604   fprintf(stderr, "MajorGC:               Reason States FSNR ");
1605   fprintf(stderr, " %6s", "budget");
1606   fprintf(stderr, " %6s", "total");
1607 #define PRINT_PROFILE_HEADER(name, text, phase) fprintf(stderr, " %6s", text);
1608   FOR_EACH_GC_PROFILE_TIME(PRINT_PROFILE_HEADER)
1609 #undef PRINT_PROFILE_HEADER
1610   fprintf(stderr, "\n");
1611 }
1612 
1613 /* static */
printProfileTimes(const ProfileDurations & times)1614 void Statistics::printProfileTimes(const ProfileDurations& times) {
1615   for (auto time : times) {
1616     fprintf(stderr, " %6" PRIi64, static_cast<int64_t>(time.ToMilliseconds()));
1617   }
1618   fprintf(stderr, "\n");
1619 }
1620 
printSliceProfile()1621 void Statistics::printSliceProfile() {
1622   const SliceData& slice = slices_.back();
1623 
1624   maybePrintProfileHeaders();
1625 
1626   bool shrinking = gckind == GC_SHRINK;
1627   bool reset = slice.resetReason != AbortReason::None;
1628   bool nonIncremental = nonincrementalReason_ != AbortReason::None;
1629   bool full = zoneStats.isFullCollection();
1630 
1631   fprintf(stderr, "MajorGC: %20s %1d -> %1d %1s%1s%1s%1s ",
1632           ExplainGCReason(slice.reason), int(slice.initialState),
1633           int(slice.finalState), full ? "F" : "", shrinking ? "S" : "",
1634           nonIncremental ? "N" : "", reset ? "R" : "");
1635 
1636   if (!nonIncremental && !slice.budget.isUnlimited() &&
1637       slice.budget.isTimeBudget()) {
1638     fprintf(stderr, " %6" PRIi64,
1639             static_cast<int64_t>(slice.budget.timeBudget.budget));
1640   } else {
1641     fprintf(stderr, "       ");
1642   }
1643 
1644   ProfileDurations times;
1645   times[ProfileKey::Total] = slice.duration();
1646   totalTimes_[ProfileKey::Total] += times[ProfileKey::Total];
1647 
1648 #define GET_PROFILE_TIME(name, text, phase)                    \
1649   times[ProfileKey::name] = SumPhase(phase, slice.phaseTimes); \
1650   totalTimes_[ProfileKey::name] += times[ProfileKey::name];
1651   FOR_EACH_GC_PROFILE_TIME(GET_PROFILE_TIME)
1652 #undef GET_PROFILE_TIME
1653 
1654   printProfileTimes(times);
1655 }
1656 
printTotalProfileTimes()1657 void Statistics::printTotalProfileTimes() {
1658   if (enableProfiling_) {
1659     fprintf(stderr, "MajorGC TOTALS: %7" PRIu64 " slices:                  ",
1660             sliceCount_);
1661     printProfileTimes(totalTimes_);
1662   }
1663 }
1664