/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=8 sts=4 et sw=4 tw=99: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "gc/Statistics.h" #include "mozilla/ArrayUtils.h" #include "mozilla/DebugOnly.h" #include "mozilla/IntegerRange.h" #include "mozilla/PodOperations.h" #include "mozilla/Sprintf.h" #include <ctype.h> #include <stdarg.h> #include <stdio.h> #include "jsprf.h" #include "jsutil.h" #include "gc/Memory.h" #include "vm/Debugger.h" #include "vm/HelperThreads.h" #include "vm/Runtime.h" #include "vm/Time.h" using namespace js; using namespace js::gc; using namespace js::gcstats; using mozilla::DebugOnly; using mozilla::MakeRange; using mozilla::PodArrayZero; using mozilla::PodZero; /* * If this fails, then you can either delete this assertion and allow all * larger-numbered reasons to pile up in the last telemetry bucket, or switch * to GC_REASON_3 and bump the max value. */ JS_STATIC_ASSERT(JS::gcreason::NUM_TELEMETRY_REASONS >= JS::gcreason::NUM_REASONS); const char* js::gcstats::ExplainInvocationKind(JSGCInvocationKind gckind) { MOZ_ASSERT(gckind == GC_NORMAL || gckind == GC_SHRINK); if (gckind == GC_NORMAL) return "Normal"; else return "Shrinking"; } JS_PUBLIC_API(const char*) JS::gcreason::ExplainReason(JS::gcreason::Reason reason) { switch (reason) { #define SWITCH_REASON(name) \ case JS::gcreason::name: \ return #name; GCREASONS(SWITCH_REASON) default: MOZ_CRASH("bad GC reason"); #undef SWITCH_REASON } } const char* js::gcstats::ExplainAbortReason(gc::AbortReason reason) { switch (reason) { #define SWITCH_REASON(name) \ case gc::AbortReason::name: \ return #name; GC_ABORT_REASONS(SWITCH_REASON) default: MOZ_CRASH("bad GC abort reason"); #undef SWITCH_REASON } } static double t(int64_t t) { return double(t) / PRMJ_USEC_PER_MSEC; } struct PhaseInfo { Phase index; const char* name; Phase parent; const uint8_t telemetryBucket; }; // The zeroth entry in the timing arrays is used for phases that have a // unique lineage. static const size_t PHASE_DAG_NONE = 0; // These are really just fields of PhaseInfo, but I have to initialize them // programmatically, which prevents making phases[] const. (And marking these // fields mutable does not work on Windows; the whole thing gets created in // read-only memory anyway.) struct ExtraPhaseInfo { // Depth in the tree of each phase type size_t depth; // Index into the set of parallel arrays of timing data, for parents with // at least one multi-parented child size_t dagSlot; }; static const Phase PHASE_NO_PARENT = PHASE_LIMIT; struct DagChildEdge { Phase parent; Phase child; } dagChildEdges[] = { { PHASE_MARK, PHASE_MARK_ROOTS }, { PHASE_MINOR_GC, PHASE_MARK_ROOTS }, { PHASE_TRACE_HEAP, PHASE_MARK_ROOTS }, { PHASE_EVICT_NURSERY, PHASE_MARK_ROOTS }, { PHASE_COMPACT_UPDATE, PHASE_MARK_ROOTS } }; /* * Note that PHASE_MUTATOR, PHASE_GC_BEGIN, and PHASE_GC_END never have any * child phases. If beginPhase is called while one of these is active, they * will automatically be suspended and resumed when the phase stack is next * empty. Timings for these phases are thus exclusive of any other phase. */ static const PhaseInfo phases[] = { { PHASE_MUTATOR, "Mutator Running", PHASE_NO_PARENT, 0 }, { PHASE_GC_BEGIN, "Begin Callback", PHASE_NO_PARENT, 1 }, { PHASE_WAIT_BACKGROUND_THREAD, "Wait Background Thread", PHASE_NO_PARENT, 2 }, { PHASE_MARK_DISCARD_CODE, "Mark Discard Code", PHASE_NO_PARENT, 3 }, { PHASE_RELAZIFY_FUNCTIONS, "Relazify Functions", PHASE_NO_PARENT, 4 }, { PHASE_PURGE, "Purge", PHASE_NO_PARENT, 5 }, { PHASE_MARK, "Mark", PHASE_NO_PARENT, 6 }, { PHASE_UNMARK, "Unmark", PHASE_MARK, 7 }, /* PHASE_MARK_ROOTS */ { PHASE_MARK_DELAYED, "Mark Delayed", PHASE_MARK, 8 }, { PHASE_SWEEP, "Sweep", PHASE_NO_PARENT, 9 }, { PHASE_SWEEP_MARK, "Mark During Sweeping", PHASE_SWEEP, 10 }, { PHASE_SWEEP_MARK_TYPES, "Mark Types During Sweeping", PHASE_SWEEP_MARK, 11 }, { PHASE_SWEEP_MARK_INCOMING_BLACK, "Mark Incoming Black Pointers", PHASE_SWEEP_MARK, 12 }, { PHASE_SWEEP_MARK_WEAK, "Mark Weak", PHASE_SWEEP_MARK, 13 }, { PHASE_SWEEP_MARK_INCOMING_GRAY, "Mark Incoming Gray Pointers", PHASE_SWEEP_MARK, 14 }, { PHASE_SWEEP_MARK_GRAY, "Mark Gray", PHASE_SWEEP_MARK, 15 }, { PHASE_SWEEP_MARK_GRAY_WEAK, "Mark Gray and Weak", PHASE_SWEEP_MARK, 16 }, { PHASE_FINALIZE_START, "Finalize Start Callbacks", PHASE_SWEEP, 17 }, { PHASE_WEAK_ZONEGROUP_CALLBACK, "Per-Slice Weak Callback", PHASE_FINALIZE_START, 57 }, { PHASE_WEAK_COMPARTMENT_CALLBACK, "Per-Compartment Weak Callback", PHASE_FINALIZE_START, 58 }, { PHASE_SWEEP_ATOMS, "Sweep Atoms", PHASE_SWEEP, 18 }, { PHASE_SWEEP_SYMBOL_REGISTRY, "Sweep Symbol Registry", PHASE_SWEEP, 19 }, { PHASE_SWEEP_COMPARTMENTS, "Sweep Compartments", PHASE_SWEEP, 20 }, { PHASE_SWEEP_DISCARD_CODE, "Sweep Discard Code", PHASE_SWEEP_COMPARTMENTS, 21 }, { PHASE_SWEEP_INNER_VIEWS, "Sweep Inner Views", PHASE_SWEEP_COMPARTMENTS, 22 }, { PHASE_SWEEP_CC_WRAPPER, "Sweep Cross Compartment Wrappers", PHASE_SWEEP_COMPARTMENTS, 23 }, { PHASE_SWEEP_BASE_SHAPE, "Sweep Base Shapes", PHASE_SWEEP_COMPARTMENTS, 24 }, { PHASE_SWEEP_INITIAL_SHAPE, "Sweep Initial Shapes", PHASE_SWEEP_COMPARTMENTS, 25 }, { PHASE_SWEEP_TYPE_OBJECT, "Sweep Type Objects", PHASE_SWEEP_COMPARTMENTS, 26 }, { PHASE_SWEEP_BREAKPOINT, "Sweep Breakpoints", PHASE_SWEEP_COMPARTMENTS, 27 }, { PHASE_SWEEP_REGEXP, "Sweep Regexps", PHASE_SWEEP_COMPARTMENTS, 28 }, { PHASE_SWEEP_MISC, "Sweep Miscellaneous", PHASE_SWEEP_COMPARTMENTS, 29 }, { PHASE_SWEEP_TYPES, "Sweep type information", PHASE_SWEEP_COMPARTMENTS, 30 }, { PHASE_SWEEP_TYPES_BEGIN, "Sweep type tables and compilations", PHASE_SWEEP_TYPES, 31 }, { PHASE_SWEEP_TYPES_END, "Free type arena", PHASE_SWEEP_TYPES, 32 }, { PHASE_SWEEP_OBJECT, "Sweep Object", PHASE_SWEEP, 33 }, { PHASE_SWEEP_STRING, "Sweep String", PHASE_SWEEP, 34 }, { PHASE_SWEEP_SCRIPT, "Sweep Script", PHASE_SWEEP, 35 }, { PHASE_SWEEP_SCOPE, "Sweep Scope", PHASE_SWEEP, 59 }, { PHASE_SWEEP_SHAPE, "Sweep Shape", PHASE_SWEEP, 36 }, { PHASE_SWEEP_JITCODE, "Sweep JIT code", PHASE_SWEEP, 37 }, { PHASE_FINALIZE_END, "Finalize End Callback", PHASE_SWEEP, 38 }, { PHASE_DESTROY, "Deallocate", PHASE_SWEEP, 39 }, { PHASE_COMPACT, "Compact", PHASE_NO_PARENT, 40 }, { PHASE_COMPACT_MOVE, "Compact Move", PHASE_COMPACT, 41 }, { PHASE_COMPACT_UPDATE, "Compact Update", PHASE_COMPACT, 42 }, /* PHASE_MARK_ROOTS */ { PHASE_COMPACT_UPDATE_CELLS, "Compact Update Cells", PHASE_COMPACT_UPDATE, 43 }, { PHASE_GC_END, "End Callback", PHASE_NO_PARENT, 44 }, { PHASE_MINOR_GC, "All Minor GCs", PHASE_NO_PARENT, 45 }, /* PHASE_MARK_ROOTS */ { PHASE_EVICT_NURSERY, "Minor GCs to Evict Nursery", PHASE_NO_PARENT, 46 }, /* PHASE_MARK_ROOTS */ { PHASE_TRACE_HEAP, "Trace Heap", PHASE_NO_PARENT, 47 }, /* PHASE_MARK_ROOTS */ { PHASE_BARRIER, "Barriers", PHASE_NO_PARENT, 55 }, { PHASE_UNMARK_GRAY, "Unmark gray", PHASE_BARRIER, 56 }, { PHASE_MARK_ROOTS, "Mark Roots", PHASE_MULTI_PARENTS, 48 }, { PHASE_BUFFER_GRAY_ROOTS, "Buffer Gray Roots", PHASE_MARK_ROOTS, 49 }, { PHASE_MARK_CCWS, "Mark Cross Compartment Wrappers", PHASE_MARK_ROOTS, 50 }, { PHASE_MARK_STACK, "Mark C and JS stacks", PHASE_MARK_ROOTS, 51 }, { PHASE_MARK_RUNTIME_DATA, "Mark Runtime-wide Data", PHASE_MARK_ROOTS, 52 }, { PHASE_MARK_EMBEDDING, "Mark Embedding", PHASE_MARK_ROOTS, 53 }, { PHASE_MARK_COMPARTMENTS, "Mark Compartments", PHASE_MARK_ROOTS, 54 }, { PHASE_PURGE_SHAPE_TABLES, "Purge ShapeTables", PHASE_NO_PARENT, 60 }, { PHASE_LIMIT, nullptr, PHASE_NO_PARENT, 60 } // Current number of telemetryBuckets is 60. If you insert new phases // somewhere, start at that number and count up. Do not change any existing // numbers. }; static ExtraPhaseInfo phaseExtra[PHASE_LIMIT] = { { 0, 0 } }; // Mapping from all nodes with a multi-parented child to a Vector of all // multi-parented children and their descendants. (Single-parented children will // not show up in this list.) static mozilla::Vector<Phase, 0, SystemAllocPolicy> dagDescendants[Statistics::NumTimingArrays]; // Preorder iterator over all phases in the expanded tree. Positions are // returned as <phase,dagSlot> pairs (dagSlot will be zero aka PHASE_DAG_NONE // for the top nodes with a single path from the parent, and 1 or more for // nodes in multiparented subtrees). struct AllPhaseIterator { // If 'descendants' is empty, the current Phase position. int current; // The depth of the current multiparented node that we are processing, or // zero if we are pointing to the top portion of the tree. int baseLevel; // When looking at multiparented descendants, the dag slot (index into // PhaseTimeTables) containing the entries for the current parent. size_t activeSlot; // When iterating over a multiparented subtree, the list of (remaining) // subtree nodes. mozilla::Vector<Phase, 0, SystemAllocPolicy>::Range descendants; explicit AllPhaseIterator(const Statistics::PhaseTimeTable table) : current(0) , baseLevel(0) , activeSlot(PHASE_DAG_NONE) , descendants(dagDescendants[PHASE_DAG_NONE].all()) /* empty range */ { } void get(Phase* phase, size_t* dagSlot, size_t* level = nullptr) { MOZ_ASSERT(!done()); *dagSlot = activeSlot; *phase = descendants.empty() ? Phase(current) : descendants.front(); if (level) *level = phaseExtra[*phase].depth + baseLevel; } void advance() { MOZ_ASSERT(!done()); if (!descendants.empty()) { // Currently iterating over a multiparented subtree. descendants.popFront(); if (!descendants.empty()) return; // Just before leaving the last child, reset the iterator to look // at "main" phases (in PHASE_DAG_NONE) instead of multiparented // subtree phases. ++current; activeSlot = PHASE_DAG_NONE; baseLevel = 0; return; } if (phaseExtra[current].dagSlot != PHASE_DAG_NONE) { // The current phase has a shared subtree. Load them up into // 'descendants' and advance to the first child. activeSlot = phaseExtra[current].dagSlot; descendants = dagDescendants[activeSlot].all(); MOZ_ASSERT(!descendants.empty()); baseLevel += phaseExtra[current].depth + 1; return; } ++current; } bool done() const { return phases[current].parent == PHASE_MULTI_PARENTS; } }; void Statistics::gcDuration(int64_t* total, int64_t* maxPause) const { *total = *maxPause = 0; for (const SliceData* slice = slices.begin(); slice != slices.end(); slice++) { *total += slice->duration(); if (slice->duration() > *maxPause) *maxPause = slice->duration(); } if (*maxPause > maxPauseInInterval) maxPauseInInterval = *maxPause; } void Statistics::sccDurations(int64_t* total, int64_t* maxPause) { *total = *maxPause = 0; for (size_t i = 0; i < sccTimes.length(); i++) { *total += sccTimes[i]; *maxPause = Max(*maxPause, sccTimes[i]); } } typedef Vector<UniqueChars, 8, SystemAllocPolicy> FragmentVector; static UniqueChars Join(const FragmentVector& fragments, const char* separator = "") { const size_t separatorLength = strlen(separator); size_t length = 0; for (size_t i = 0; i < fragments.length(); ++i) { length += fragments[i] ? strlen(fragments[i].get()) : 0; if (i < (fragments.length() - 1)) length += separatorLength; } char* joined = js_pod_malloc<char>(length + 1); joined[length] = '\0'; char* cursor = joined; for (size_t i = 0; i < fragments.length(); ++i) { if (fragments[i]) strcpy(cursor, fragments[i].get()); cursor += fragments[i] ? strlen(fragments[i].get()) : 0; if (i < (fragments.length() - 1)) { if (separatorLength) strcpy(cursor, separator); cursor += separatorLength; } } return UniqueChars(joined); } static int64_t SumChildTimes(size_t phaseSlot, Phase phase, const Statistics::PhaseTimeTable phaseTimes) { // Sum the contributions from single-parented children. int64_t total = 0; size_t depth = phaseExtra[phase].depth; for (unsigned i = phase + 1; i < PHASE_LIMIT && phaseExtra[i].depth > depth; i++) { if (phases[i].parent == phase) total += phaseTimes[phaseSlot][i]; } // Sum the contributions from multi-parented children. size_t dagSlot = phaseExtra[phase].dagSlot; if (dagSlot != PHASE_DAG_NONE) { for (auto edge : dagChildEdges) { if (edge.parent == phase) total += phaseTimes[dagSlot][edge.child]; } } return total; } UniqueChars Statistics::formatCompactSliceMessage() const { // Skip if we OOM'ed. if (slices.length() == 0) return UniqueChars(nullptr); const size_t index = slices.length() - 1; const SliceData& slice = slices[index]; char budgetDescription[200]; slice.budget.describe(budgetDescription, sizeof(budgetDescription) - 1); const char* format = "GC Slice %u - Pause: %.3fms of %s budget (@ %.3fms); Reason: %s; Reset: %s%s; Times: "; char buffer[1024]; SprintfLiteral(buffer, format, index, t(slice.duration()), budgetDescription, t(slice.start - slices[0].start), ExplainReason(slice.reason), slice.wasReset() ? "yes - " : "no", slice.wasReset() ? ExplainAbortReason(slice.resetReason) : ""); FragmentVector fragments; if (!fragments.append(DuplicateString(buffer)) || !fragments.append(formatCompactSlicePhaseTimes(slices[index].phaseTimes))) { return UniqueChars(nullptr); } return Join(fragments); } UniqueChars Statistics::formatCompactSummaryMessage() const { const double bytesPerMiB = 1024 * 1024; FragmentVector fragments; if (!fragments.append(DuplicateString("Summary - "))) return UniqueChars(nullptr); int64_t total, longest; gcDuration(&total, &longest); const double mmu20 = computeMMU(20 * PRMJ_USEC_PER_MSEC); const double mmu50 = computeMMU(50 * PRMJ_USEC_PER_MSEC); char buffer[1024]; if (!nonincremental()) { SprintfLiteral(buffer, "Max Pause: %.3fms; MMU 20ms: %.1f%%; MMU 50ms: %.1f%%; Total: %.3fms; ", t(longest), mmu20 * 100., mmu50 * 100., t(total)); } else { SprintfLiteral(buffer, "Non-Incremental: %.3fms (%s); ", t(total), ExplainAbortReason(nonincrementalReason_)); } if (!fragments.append(DuplicateString(buffer))) return UniqueChars(nullptr); SprintfLiteral(buffer, "Zones: %d of %d (-%d); Compartments: %d of %d (-%d); HeapSize: %.3f MiB; " \ "HeapChange (abs): %+d (%d); ", zoneStats.collectedZoneCount, zoneStats.zoneCount, zoneStats.sweptZoneCount, zoneStats.collectedCompartmentCount, zoneStats.compartmentCount, zoneStats.sweptCompartmentCount, double(preBytes) / bytesPerMiB, counts[STAT_NEW_CHUNK] - counts[STAT_DESTROY_CHUNK], counts[STAT_NEW_CHUNK] + counts[STAT_DESTROY_CHUNK]); if (!fragments.append(DuplicateString(buffer))) return UniqueChars(nullptr); MOZ_ASSERT_IF(counts[STAT_ARENA_RELOCATED], gckind == GC_SHRINK); if (gckind == GC_SHRINK) { SprintfLiteral(buffer, "Kind: %s; Relocated: %.3f MiB; ", ExplainInvocationKind(gckind), double(ArenaSize * counts[STAT_ARENA_RELOCATED]) / bytesPerMiB); if (!fragments.append(DuplicateString(buffer))) return UniqueChars(nullptr); } return Join(fragments); } UniqueChars Statistics::formatCompactSlicePhaseTimes(const PhaseTimeTable phaseTimes) const { static const int64_t MaxUnaccountedTimeUS = 100; FragmentVector fragments; char buffer[128]; for (AllPhaseIterator iter(phaseTimes); !iter.done(); iter.advance()) { Phase phase; size_t dagSlot; size_t level; iter.get(&phase, &dagSlot, &level); MOZ_ASSERT(level < 4); int64_t ownTime = phaseTimes[dagSlot][phase]; int64_t childTime = SumChildTimes(dagSlot, phase, phaseTimes); if (ownTime > MaxUnaccountedTimeUS) { SprintfLiteral(buffer, "%s: %.3fms", phases[phase].name, t(ownTime)); if (!fragments.append(DuplicateString(buffer))) return UniqueChars(nullptr); if (childTime && (ownTime - childTime) > MaxUnaccountedTimeUS) { MOZ_ASSERT(level < 3); SprintfLiteral(buffer, "%s: %.3fms", "Other", t(ownTime - childTime)); if (!fragments.append(DuplicateString(buffer))) return UniqueChars(nullptr); } } } return Join(fragments, ", "); } UniqueChars Statistics::formatDetailedMessage() { FragmentVector fragments; if (!fragments.append(formatDetailedDescription())) return UniqueChars(nullptr); if (slices.length() > 1) { for (unsigned i = 0; i < slices.length(); i++) { if (!fragments.append(formatDetailedSliceDescription(i, slices[i]))) return UniqueChars(nullptr); if (!fragments.append(formatDetailedPhaseTimes(slices[i].phaseTimes))) return UniqueChars(nullptr); } } if (!fragments.append(formatDetailedTotals())) return UniqueChars(nullptr); if (!fragments.append(formatDetailedPhaseTimes(phaseTimes))) return UniqueChars(nullptr); return Join(fragments); } UniqueChars Statistics::formatDetailedDescription() { const double bytesPerMiB = 1024 * 1024; int64_t sccTotal, sccLongest; sccDurations(&sccTotal, &sccLongest); double mmu20 = computeMMU(20 * PRMJ_USEC_PER_MSEC); double mmu50 = computeMMU(50 * PRMJ_USEC_PER_MSEC); const char* format = "=================================================================\n\ Invocation Kind: %s\n\ Reason: %s\n\ Incremental: %s%s\n\ Zones Collected: %d of %d (-%d)\n\ Compartments Collected: %d of %d (-%d)\n\ MinorGCs since last GC: %d\n\ Store Buffer Overflows: %d\n\ MMU 20ms:%.1f%%; 50ms:%.1f%%\n\ SCC Sweep Total (MaxPause): %.3fms (%.3fms)\n\ HeapSize: %.3f MiB\n\ Chunk Delta (magnitude): %+d (%d)\n\ Arenas Relocated: %.3f MiB\n\ "; char buffer[1024]; SprintfLiteral(buffer, format, ExplainInvocationKind(gckind), ExplainReason(slices[0].reason), nonincremental() ? "no - " : "yes", nonincremental() ? ExplainAbortReason(nonincrementalReason_) : "", zoneStats.collectedZoneCount, zoneStats.zoneCount, zoneStats.sweptZoneCount, zoneStats.collectedCompartmentCount, zoneStats.compartmentCount, zoneStats.sweptCompartmentCount, counts[STAT_MINOR_GC], counts[STAT_STOREBUFFER_OVERFLOW], mmu20 * 100., mmu50 * 100., t(sccTotal), t(sccLongest), double(preBytes) / bytesPerMiB, counts[STAT_NEW_CHUNK] - counts[STAT_DESTROY_CHUNK], counts[STAT_NEW_CHUNK] + counts[STAT_DESTROY_CHUNK], double(ArenaSize * counts[STAT_ARENA_RELOCATED]) / bytesPerMiB); return DuplicateString(buffer); } UniqueChars Statistics::formatDetailedSliceDescription(unsigned i, const SliceData& slice) { char budgetDescription[200]; slice.budget.describe(budgetDescription, sizeof(budgetDescription) - 1); const char* format = "\ ---- Slice %u ----\n\ Reason: %s\n\ Reset: %s%s\n\ State: %s -> %s\n\ Page Faults: %ld\n\ Pause: %.3fms of %s budget (@ %.3fms)\n\ "; char buffer[1024]; SprintfLiteral(buffer, format, i, ExplainReason(slice.reason), slice.wasReset() ? "yes - " : "no", slice.wasReset() ? ExplainAbortReason(slice.resetReason) : "", gc::StateName(slice.initialState), gc::StateName(slice.finalState), uint64_t(slice.endFaults - slice.startFaults), t(slice.duration()), budgetDescription, t(slice.start - slices[0].start)); return DuplicateString(buffer); } UniqueChars Statistics::formatDetailedPhaseTimes(const PhaseTimeTable phaseTimes) { static const char* LevelToIndent[] = { "", " ", " ", " " }; static const int64_t MaxUnaccountedChildTimeUS = 50; FragmentVector fragments; char buffer[128]; for (AllPhaseIterator iter(phaseTimes); !iter.done(); iter.advance()) { Phase phase; size_t dagSlot; size_t level; iter.get(&phase, &dagSlot, &level); MOZ_ASSERT(level < 4); int64_t ownTime = phaseTimes[dagSlot][phase]; int64_t childTime = SumChildTimes(dagSlot, phase, phaseTimes); if (ownTime > 0) { SprintfLiteral(buffer, " %s%s: %.3fms\n", LevelToIndent[level], phases[phase].name, t(ownTime)); if (!fragments.append(DuplicateString(buffer))) return UniqueChars(nullptr); if (childTime && (ownTime - childTime) > MaxUnaccountedChildTimeUS) { MOZ_ASSERT(level < 3); SprintfLiteral(buffer, " %s%s: %.3fms\n", LevelToIndent[level + 1], "Other", t(ownTime - childTime)); if (!fragments.append(DuplicateString(buffer))) return UniqueChars(nullptr); } } } return Join(fragments); } UniqueChars Statistics::formatDetailedTotals() { int64_t total, longest; gcDuration(&total, &longest); const char* format = "\ ---- Totals ----\n\ Total Time: %.3fms\n\ Max Pause: %.3fms\n\ "; char buffer[1024]; SprintfLiteral(buffer, format, t(total), t(longest)); return DuplicateString(buffer); } UniqueChars Statistics::formatJsonMessage(uint64_t timestamp) { MOZ_ASSERT(!aborted); FragmentVector fragments; if (!fragments.append(DuplicateString("{")) || !fragments.append(formatJsonDescription(timestamp)) || !fragments.append(DuplicateString("\"slices\":["))) { return UniqueChars(nullptr); } for (unsigned i = 0; i < slices.length(); i++) { if (!fragments.append(DuplicateString("{")) || !fragments.append(formatJsonSliceDescription(i, slices[i])) || !fragments.append(DuplicateString("\"times\":{")) || !fragments.append(formatJsonPhaseTimes(slices[i].phaseTimes)) || !fragments.append(DuplicateString("}}")) || (i < (slices.length() - 1) && !fragments.append(DuplicateString(",")))) { return UniqueChars(nullptr); } } if (!fragments.append(DuplicateString("],\"totals\":{")) || !fragments.append(formatJsonPhaseTimes(phaseTimes)) || !fragments.append(DuplicateString("}}"))) { return UniqueChars(nullptr); } return Join(fragments); } UniqueChars Statistics::formatJsonDescription(uint64_t timestamp) { int64_t total, longest; gcDuration(&total, &longest); int64_t sccTotal, sccLongest; sccDurations(&sccTotal, &sccLongest); double mmu20 = computeMMU(20 * PRMJ_USEC_PER_MSEC); double mmu50 = computeMMU(50 * PRMJ_USEC_PER_MSEC); const char *format = "\"timestamp\":%llu," "\"max_pause\":%llu.%03llu," "\"total_time\":%llu.%03llu," "\"zones_collected\":%d," "\"total_zones\":%d," "\"total_compartments\":%d," "\"minor_gcs\":%d," "\"store_buffer_overflows\":%d," "\"mmu_20ms\":%d," "\"mmu_50ms\":%d," "\"scc_sweep_total\":%llu.%03llu," "\"scc_sweep_max_pause\":%llu.%03llu," "\"nonincremental_reason\":\"%s\"," "\"allocated\":%u," "\"added_chunks\":%d," "\"removed_chunks\":%d,"; char buffer[1024]; SprintfLiteral(buffer, format, (unsigned long long)timestamp, longest / 1000, longest % 1000, total / 1000, total % 1000, zoneStats.collectedZoneCount, zoneStats.zoneCount, zoneStats.compartmentCount, counts[STAT_MINOR_GC], counts[STAT_STOREBUFFER_OVERFLOW], int(mmu20 * 100), int(mmu50 * 100), sccTotal / 1000, sccTotal % 1000, sccLongest / 1000, sccLongest % 1000, ExplainAbortReason(nonincrementalReason_), unsigned(preBytes / 1024 / 1024), counts[STAT_NEW_CHUNK], counts[STAT_DESTROY_CHUNK]); return DuplicateString(buffer); } UniqueChars Statistics::formatJsonSliceDescription(unsigned i, const SliceData& slice) { int64_t duration = slice.duration(); int64_t when = slice.start - slices[0].start; char budgetDescription[200]; slice.budget.describe(budgetDescription, sizeof(budgetDescription) - 1); int64_t pageFaults = slice.endFaults - slice.startFaults; const char* format = "\"slice\":%d," "\"pause\":%llu.%03llu," "\"when\":%llu.%03llu," "\"reason\":\"%s\"," "\"initial_state\":\"%s\"," "\"final_state\":\"%s\"," "\"budget\":\"%s\"," "\"page_faults\":%llu," "\"start_timestamp\":%llu," "\"end_timestamp\":%llu,"; char buffer[1024]; SprintfLiteral(buffer, format, i, duration / 1000, duration % 1000, when / 1000, when % 1000, ExplainReason(slice.reason), gc::StateName(slice.initialState), gc::StateName(slice.finalState), budgetDescription, pageFaults, slice.start, slice.end); return DuplicateString(buffer); } UniqueChars FilterJsonKey(const char*const buffer) { char* mut = strdup(buffer); char* c = mut; while (*c) { if (!isalpha(*c)) *c = '_'; else if (isupper(*c)) *c = tolower(*c); ++c; } return UniqueChars(mut); } UniqueChars Statistics::formatJsonPhaseTimes(const PhaseTimeTable phaseTimes) { FragmentVector fragments; char buffer[128]; for (AllPhaseIterator iter(phaseTimes); !iter.done(); iter.advance()) { Phase phase; size_t dagSlot; iter.get(&phase, &dagSlot); UniqueChars name = FilterJsonKey(phases[phase].name); int64_t ownTime = phaseTimes[dagSlot][phase]; if (ownTime > 0) { SprintfLiteral(buffer, "\"%s\":%" PRId64 ".%03" PRId64, name.get(), ownTime / 1000, ownTime % 1000); if (!fragments.append(DuplicateString(buffer))) return UniqueChars(nullptr); } } return Join(fragments, ","); } Statistics::Statistics(JSRuntime* rt) : runtime(rt), startupTime(PRMJ_Now()), fp(nullptr), gcDepth(0), nonincrementalReason_(gc::AbortReason::None), timedGCStart(0), preBytes(0), maxPauseInInterval(0), phaseNestingDepth(0), activeDagSlot(PHASE_DAG_NONE), suspended(0), sliceCallback(nullptr), nurseryCollectionCallback(nullptr), aborted(false), enableProfiling_(false), sliceCount_(0) { PodArrayZero(phaseTotals); PodArrayZero(counts); PodArrayZero(phaseStartTimes); for (auto d : MakeRange(NumTimingArrays)) PodArrayZero(phaseTimes[d]); const char* env = getenv("MOZ_GCTIMER"); if (env) { if (strcmp(env, "none") == 0) { fp = nullptr; } else if (strcmp(env, "stdout") == 0) { fp = stdout; } else if (strcmp(env, "stderr") == 0) { fp = stderr; } else { fp = fopen(env, "a"); if (!fp) MOZ_CRASH("Failed to open MOZ_GCTIMER log file."); } } env = getenv("JS_GC_PROFILE"); if (env) { if (0 == strcmp(env, "help")) { fprintf(stderr, "JS_GC_PROFILE=N\n" "\tReport major GC's taking more than N milliseconds.\n"); exit(0); } enableProfiling_ = true; profileThreshold_ = atoi(env); } PodZero(&totalTimes_); } Statistics::~Statistics() { if (fp && fp != stdout && fp != stderr) fclose(fp); } /* static */ bool Statistics::initialize() { for (size_t i = 0; i < PHASE_LIMIT; i++) { MOZ_ASSERT(phases[i].index == i); for (size_t j = 0; j < PHASE_LIMIT; j++) MOZ_ASSERT_IF(i != j, phases[i].telemetryBucket != phases[j].telemetryBucket); } // Create a static table of descendants for every phase with multiple // children. This assumes that all descendants come linearly in the // list, which is reasonable since full dags are not supported; any // path from the leaf to the root must encounter at most one node with // multiple parents. size_t dagSlot = 0; for (size_t i = 0; i < mozilla::ArrayLength(dagChildEdges); i++) { Phase parent = dagChildEdges[i].parent; if (!phaseExtra[parent].dagSlot) phaseExtra[parent].dagSlot = ++dagSlot; Phase child = dagChildEdges[i].child; MOZ_ASSERT(phases[child].parent == PHASE_MULTI_PARENTS); int j = child; do { if (!dagDescendants[phaseExtra[parent].dagSlot].append(Phase(j))) return false; j++; } while (j != PHASE_LIMIT && phases[j].parent != PHASE_MULTI_PARENTS); } MOZ_ASSERT(dagSlot <= MaxMultiparentPhases - 1); // Fill in the depth of each node in the tree. Multi-parented nodes // have depth 0. mozilla::Vector<Phase, 0, SystemAllocPolicy> stack; if (!stack.append(PHASE_LIMIT)) // Dummy entry to avoid special-casing the first node return false; for (int i = 0; i < PHASE_LIMIT; i++) { if (phases[i].parent == PHASE_NO_PARENT || phases[i].parent == PHASE_MULTI_PARENTS) { stack.clear(); } else { while (stack.back() != phases[i].parent) stack.popBack(); } phaseExtra[i].depth = stack.length(); if (!stack.append(Phase(i))) return false; } return true; } JS::GCSliceCallback Statistics::setSliceCallback(JS::GCSliceCallback newCallback) { JS::GCSliceCallback oldCallback = sliceCallback; sliceCallback = newCallback; return oldCallback; } JS::GCNurseryCollectionCallback Statistics::setNurseryCollectionCallback(JS::GCNurseryCollectionCallback newCallback) { auto oldCallback = nurseryCollectionCallback; nurseryCollectionCallback = newCallback; return oldCallback; } int64_t Statistics::clearMaxGCPauseAccumulator() { int64_t prior = maxPauseInInterval; maxPauseInInterval = 0; return prior; } int64_t Statistics::getMaxGCPauseSinceClear() { return maxPauseInInterval; } // Sum up the time for a phase, including instances of the phase with different // parents. static int64_t SumPhase(Phase phase, const Statistics::PhaseTimeTable times) { int64_t sum = 0; for (auto i : MakeRange(Statistics::NumTimingArrays)) sum += times[i][phase]; return sum; } static Phase LongestPhase(const Statistics::PhaseTimeTable times) { int64_t longestTime = 0; Phase longestPhase = PHASE_NONE; for (size_t i = 0; i < PHASE_LIMIT; ++i) { int64_t phaseTime = SumPhase(Phase(i), times); if (phaseTime > longestTime) { longestTime = phaseTime; longestPhase = Phase(i); } } return longestPhase; } void Statistics::printStats() { if (aborted) { fprintf(fp, "OOM during GC statistics collection. The report is unavailable for this GC.\n"); } else { UniqueChars msg = formatDetailedMessage(); if (msg) fprintf(fp, "GC(T+%.3fs) %s\n", t(slices[0].start - startupTime) / 1000.0, msg.get()); } fflush(fp); } void Statistics::beginGC(JSGCInvocationKind kind) { slices.clearAndFree(); sccTimes.clearAndFree(); gckind = kind; nonincrementalReason_ = gc::AbortReason::None; preBytes = runtime->gc.usage.gcBytes(); } void Statistics::endGC() { for (auto j : MakeRange(NumTimingArrays)) for (int i = 0; i < PHASE_LIMIT; i++) phaseTotals[j][i] += phaseTimes[j][i]; int64_t total, longest; gcDuration(&total, &longest); int64_t sccTotal, sccLongest; sccDurations(&sccTotal, &sccLongest); runtime->addTelemetry(JS_TELEMETRY_GC_IS_ZONE_GC, !zoneStats.isCollectingAllZones()); runtime->addTelemetry(JS_TELEMETRY_GC_MS, t(total)); runtime->addTelemetry(JS_TELEMETRY_GC_MAX_PAUSE_MS, t(longest)); int64_t markTotal = SumPhase(PHASE_MARK, phaseTimes); int64_t markRootsTotal = SumPhase(PHASE_MARK_ROOTS, phaseTimes); runtime->addTelemetry(JS_TELEMETRY_GC_MARK_MS, t(markTotal)); runtime->addTelemetry(JS_TELEMETRY_GC_SWEEP_MS, t(phaseTimes[PHASE_DAG_NONE][PHASE_SWEEP])); if (runtime->gc.isCompactingGc()) { runtime->addTelemetry(JS_TELEMETRY_GC_COMPACT_MS, t(phaseTimes[PHASE_DAG_NONE][PHASE_COMPACT])); } runtime->addTelemetry(JS_TELEMETRY_GC_MARK_ROOTS_MS, t(markRootsTotal)); runtime->addTelemetry(JS_TELEMETRY_GC_MARK_GRAY_MS, t(phaseTimes[PHASE_DAG_NONE][PHASE_SWEEP_MARK_GRAY])); runtime->addTelemetry(JS_TELEMETRY_GC_NON_INCREMENTAL, nonincremental()); if (nonincremental()) runtime->addTelemetry(JS_TELEMETRY_GC_NON_INCREMENTAL_REASON, uint32_t(nonincrementalReason_)); runtime->addTelemetry(JS_TELEMETRY_GC_INCREMENTAL_DISABLED, !runtime->gc.isIncrementalGCAllowed()); runtime->addTelemetry(JS_TELEMETRY_GC_SCC_SWEEP_TOTAL_MS, t(sccTotal)); runtime->addTelemetry(JS_TELEMETRY_GC_SCC_SWEEP_MAX_PAUSE_MS, t(sccLongest)); if (!aborted) { double mmu50 = computeMMU(50 * PRMJ_USEC_PER_MSEC); runtime->addTelemetry(JS_TELEMETRY_GC_MMU_50, mmu50 * 100); } if (fp) printStats(); // Clear the OOM flag but only if we are not in a nested GC. if (gcDepth == 1) aborted = false; } void Statistics::beginNurseryCollection(JS::gcreason::Reason reason) { count(STAT_MINOR_GC); if (nurseryCollectionCallback) { (*nurseryCollectionCallback)(runtime->contextFromMainThread(), JS::GCNurseryProgress::GC_NURSERY_COLLECTION_START, reason); } } void Statistics::endNurseryCollection(JS::gcreason::Reason reason) { if (nurseryCollectionCallback) { (*nurseryCollectionCallback)(runtime->contextFromMainThread(), JS::GCNurseryProgress::GC_NURSERY_COLLECTION_END, reason); } } void Statistics::beginSlice(const ZoneGCStats& zoneStats, JSGCInvocationKind gckind, SliceBudget budget, JS::gcreason::Reason reason) { gcDepth++; this->zoneStats = zoneStats; bool first = !runtime->gc.isIncrementalGCInProgress(); if (first) beginGC(gckind); SliceData data(budget, reason, PRMJ_Now(), JS_GetCurrentEmbedderTime(), GetPageFaultCount(), runtime->gc.state()); if (!slices.append(data)) { // If we are OOM, set a flag to indicate we have missing slice data. aborted = true; return; } runtime->addTelemetry(JS_TELEMETRY_GC_REASON, reason); // Slice callbacks should only fire for the outermost level. if (gcDepth == 1) { bool wasFullGC = zoneStats.isCollectingAllZones(); if (sliceCallback) (*sliceCallback)(runtime->contextFromMainThread(), first ? JS::GC_CYCLE_BEGIN : JS::GC_SLICE_BEGIN, JS::GCDescription(!wasFullGC, gckind, reason)); } } void Statistics::endSlice() { if (!aborted) { slices.back().end = PRMJ_Now(); slices.back().endTimestamp = JS_GetCurrentEmbedderTime(); slices.back().endFaults = GetPageFaultCount(); slices.back().finalState = runtime->gc.state(); int64_t sliceTime = slices.back().end - slices.back().start; runtime->addTelemetry(JS_TELEMETRY_GC_SLICE_MS, t(sliceTime)); runtime->addTelemetry(JS_TELEMETRY_GC_RESET, slices.back().wasReset()); if (slices.back().wasReset()) runtime->addTelemetry(JS_TELEMETRY_GC_RESET_REASON, uint32_t(slices.back().resetReason)); if (slices.back().budget.isTimeBudget()) { int64_t budget_ms = slices.back().budget.timeBudget.budget; runtime->addTelemetry(JS_TELEMETRY_GC_BUDGET_MS, budget_ms); if (budget_ms == runtime->gc.defaultSliceBudget()) runtime->addTelemetry(JS_TELEMETRY_GC_ANIMATION_MS, t(sliceTime)); // Record any phase that goes more than 2x over its budget. if (sliceTime > 2 * budget_ms * 1000) { Phase longest = LongestPhase(slices.back().phaseTimes); runtime->addTelemetry(JS_TELEMETRY_GC_SLOW_PHASE, phases[longest].telemetryBucket); } } sliceCount_++; } bool last = !runtime->gc.isIncrementalGCInProgress(); if (last) endGC(); if (enableProfiling_ && !aborted && slices.back().duration() >= profileThreshold_) printSliceProfile(); // Slice callbacks should only fire for the outermost level. if (gcDepth == 1 && !aborted) { bool wasFullGC = zoneStats.isCollectingAllZones(); if (sliceCallback) (*sliceCallback)(runtime->contextFromMainThread(), last ? JS::GC_CYCLE_END : JS::GC_SLICE_END, JS::GCDescription(!wasFullGC, gckind, slices.back().reason)); } /* Do this after the slice callback since it uses these values. */ if (last) { PodArrayZero(counts); // Clear the timers at the end of a GC because we accumulate time in // between GCs for some (which come before PHASE_GC_BEGIN in the list.) PodZero(&phaseStartTimes[PHASE_GC_BEGIN], PHASE_LIMIT - PHASE_GC_BEGIN); for (size_t d = PHASE_DAG_NONE; d < NumTimingArrays; d++) PodZero(&phaseTimes[d][PHASE_GC_BEGIN], PHASE_LIMIT - PHASE_GC_BEGIN); } gcDepth--; MOZ_ASSERT(gcDepth >= 0); } bool Statistics::startTimingMutator() { if (phaseNestingDepth != 0) { // Should only be called from outside of GC. MOZ_ASSERT(phaseNestingDepth == 1); MOZ_ASSERT(phaseNesting[0] == PHASE_MUTATOR); return false; } MOZ_ASSERT(suspended == 0); timedGCTime = 0; phaseStartTimes[PHASE_MUTATOR] = 0; phaseTimes[PHASE_DAG_NONE][PHASE_MUTATOR] = 0; timedGCStart = 0; beginPhase(PHASE_MUTATOR); return true; } bool Statistics::stopTimingMutator(double& mutator_ms, double& gc_ms) { // This should only be called from outside of GC, while timing the mutator. if (phaseNestingDepth != 1 || phaseNesting[0] != PHASE_MUTATOR) return false; endPhase(PHASE_MUTATOR); mutator_ms = t(phaseTimes[PHASE_DAG_NONE][PHASE_MUTATOR]); gc_ms = t(timedGCTime); return true; } void Statistics::suspendPhases(Phase suspension) { MOZ_ASSERT(suspension == PHASE_EXPLICIT_SUSPENSION || suspension == PHASE_IMPLICIT_SUSPENSION); while (phaseNestingDepth) { MOZ_ASSERT(suspended < mozilla::ArrayLength(suspendedPhases)); Phase parent = phaseNesting[phaseNestingDepth - 1]; suspendedPhases[suspended++] = parent; recordPhaseEnd(parent); } suspendedPhases[suspended++] = suspension; } void Statistics::resumePhases() { DebugOnly<Phase> popped = suspendedPhases[--suspended]; MOZ_ASSERT(popped == PHASE_EXPLICIT_SUSPENSION || popped == PHASE_IMPLICIT_SUSPENSION); while (suspended && suspendedPhases[suspended - 1] != PHASE_EXPLICIT_SUSPENSION && suspendedPhases[suspended - 1] != PHASE_IMPLICIT_SUSPENSION) { Phase resumePhase = suspendedPhases[--suspended]; if (resumePhase == PHASE_MUTATOR) timedGCTime += PRMJ_Now() - timedGCStart; beginPhase(resumePhase); } } void Statistics::beginPhase(Phase phase) { Phase parent = phaseNestingDepth ? phaseNesting[phaseNestingDepth - 1] : PHASE_NO_PARENT; // Re-entry is allowed during callbacks, so pause callback phases while // other phases are in progress, auto-resuming after they end. As a result, // nested GC time will not be accounted against the callback phases. // // Reuse this mechanism for managing PHASE_MUTATOR. if (parent == PHASE_GC_BEGIN || parent == PHASE_GC_END || parent == PHASE_MUTATOR) { suspendPhases(PHASE_IMPLICIT_SUSPENSION); parent = phaseNestingDepth ? phaseNesting[phaseNestingDepth - 1] : PHASE_NO_PARENT; } // Guard against any other re-entry. MOZ_ASSERT(!phaseStartTimes[phase]); MOZ_ASSERT(phases[phase].index == phase); MOZ_ASSERT(phaseNestingDepth < MAX_NESTING); MOZ_ASSERT(phases[phase].parent == parent || phases[phase].parent == PHASE_MULTI_PARENTS); phaseNesting[phaseNestingDepth] = phase; phaseNestingDepth++; if (phases[phase].parent == PHASE_MULTI_PARENTS) activeDagSlot = phaseExtra[parent].dagSlot; phaseStartTimes[phase] = PRMJ_Now(); } void Statistics::recordPhaseEnd(Phase phase) { int64_t now = PRMJ_Now(); if (phase == PHASE_MUTATOR) timedGCStart = now; phaseNestingDepth--; int64_t t = now - phaseStartTimes[phase]; if (!slices.empty()) slices.back().phaseTimes[activeDagSlot][phase] += t; phaseTimes[activeDagSlot][phase] += t; phaseStartTimes[phase] = 0; } void Statistics::endPhase(Phase phase) { recordPhaseEnd(phase); if (phases[phase].parent == PHASE_MULTI_PARENTS) activeDagSlot = PHASE_DAG_NONE; // When emptying the stack, we may need to resume a callback phase // (PHASE_GC_BEGIN/END) or return to timing the mutator (PHASE_MUTATOR). if (phaseNestingDepth == 0 && suspended > 0 && suspendedPhases[suspended - 1] == PHASE_IMPLICIT_SUSPENSION) resumePhases(); } void Statistics::endParallelPhase(Phase phase, const GCParallelTask* task) { phaseNestingDepth--; if (!slices.empty()) slices.back().phaseTimes[PHASE_DAG_NONE][phase] += task->duration(); phaseTimes[PHASE_DAG_NONE][phase] += task->duration(); phaseStartTimes[phase] = 0; } int64_t Statistics::beginSCC() { return PRMJ_Now(); } void Statistics::endSCC(unsigned scc, int64_t start) { if (scc >= sccTimes.length() && !sccTimes.resize(scc + 1)) return; sccTimes[scc] += PRMJ_Now() - start; } /* * MMU (minimum mutator utilization) is a measure of how much garbage collection * is affecting the responsiveness of the system. MMU measurements are given * with respect to a certain window size. If we report MMU(50ms) = 80%, then * that means that, for any 50ms window of time, at least 80% of the window is * devoted to the mutator. In other words, the GC is running for at most 20% of * the window, or 10ms. The GC can run multiple slices during the 50ms window * as long as the total time it spends is at most 10ms. */ double Statistics::computeMMU(int64_t window) const { MOZ_ASSERT(!slices.empty()); int64_t gc = slices[0].end - slices[0].start; int64_t gcMax = gc; if (gc >= window) return 0.0; int startIndex = 0; for (size_t endIndex = 1; endIndex < slices.length(); endIndex++) { gc += slices[endIndex].end - slices[endIndex].start; while (slices[endIndex].end - slices[startIndex].end >= window) { gc -= slices[startIndex].end - slices[startIndex].start; startIndex++; } int64_t cur = gc; if (slices[endIndex].end - slices[startIndex].start > window) cur -= (slices[endIndex].end - slices[startIndex].start - window); if (cur > gcMax) gcMax = cur; } return double(window - gcMax) / window; } /* static */ void Statistics::printProfileHeader() { fprintf(stderr, " %6s", "total"); #define PRINT_PROFILE_HEADER(name, text, phase) \ fprintf(stderr, " %6s", text); FOR_EACH_GC_PROFILE_TIME(PRINT_PROFILE_HEADER) #undef PRINT_PROFILE_HEADER fprintf(stderr, "\n"); } /* static */ void Statistics::printProfileTimes(const ProfileTimes& times) { for (auto time : times) fprintf(stderr, " %6" PRIi64, time / PRMJ_USEC_PER_MSEC); fprintf(stderr, "\n"); } void Statistics::printSliceProfile() { const SliceData& slice = slices.back(); static int printedHeader = 0; if ((printedHeader++ % 200) == 0) { fprintf(stderr, "MajorGC: Reason States "); printProfileHeader(); } fprintf(stderr, "MajorGC: %20s %1d -> %1d ", ExplainReason(slice.reason), int(slice.initialState), int(slice.finalState)); ProfileTimes times; times[ProfileKey::Total] = slice.duration(); totalTimes_[ProfileKey::Total] += times[ProfileKey::Total]; #define GET_PROFILE_TIME(name, text, phase) \ times[ProfileKey::name] = slice.phaseTimes[PHASE_DAG_NONE][phase]; \ totalTimes_[ProfileKey::name] += times[ProfileKey::name]; FOR_EACH_GC_PROFILE_TIME(GET_PROFILE_TIME) #undef GET_PROFILE_TIME printProfileTimes(times); } void Statistics::printTotalProfileTimes() { if (enableProfiling_) { fprintf(stderr, "MajorGC TOTALS: %7" PRIu64 " slices: ", sliceCount_); printProfileTimes(totalTimes_); } }