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