/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=8 sw=4 et tw=78: * * 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 #include #include "jscntxt.h" #include "jscompartment.h" #include "jscrashformat.h" #include "jscrashreport.h" #include "jsprf.h" #include "jsprobes.h" #include "jsutil.h" #include "prmjtime.h" #include "gc/Memory.h" #include "gc/Statistics.h" #include "gc/Barrier-inl.h" namespace js { namespace gcstats { /* Except for the first and last, slices of less than 12ms are not reported. */ static const int64_t SLICE_MIN_REPORT_TIME = 12 * PRMJ_USEC_PER_MSEC; class StatisticsSerializer { typedef Vector CharBuffer; CharBuffer buf_; bool asJSON_; bool needComma_; bool oom_; const static int MaxFieldValueLength = 128; public: enum Mode { AsJSON = true, AsText = false }; StatisticsSerializer(Mode asJSON) : buf_(), asJSON_(asJSON), needComma_(false), oom_(false) {} bool isJSON() { return asJSON_; } bool isOOM() { return oom_; } void endLine() { if (!asJSON_) { p("\n"); needComma_ = false; } } void extra(const char *str) { if (!asJSON_) { needComma_ = false; p(str); } } void appendString(const char *name, const char *value) { put(name, value, "", true); } void appendNumber(const char *name, const char *vfmt, const char *units, ...) { va_list va; va_start(va, units); append(name, vfmt, va, units); va_end(va); } void appendDecimal(const char *name, const char *units, double d) { if (asJSON_) appendNumber(name, "%d.%d", units, (int)d, (int)(d * 10.) % 10); else appendNumber(name, "%.1f", units, d); } void appendIfNonzeroMS(const char *name, double v) { if (asJSON_ || v >= 0.1) appendDecimal(name, "ms", v); } void beginObject(const char *name) { if (needComma_) pJSON(", "); if (asJSON_ && name) { putKey(name); pJSON(": "); } pJSON("{"); needComma_ = false; } void endObject() { needComma_ = false; pJSON("}"); needComma_ = true; } void beginArray(const char *name) { if (needComma_) pJSON(", "); if (asJSON_) putKey(name); pJSON(": ["); needComma_ = false; } void endArray() { needComma_ = false; pJSON("]"); needComma_ = true; } jschar *finishJSString() { char *buf = finishCString(); if (!buf) return NULL; size_t nchars = strlen(buf); jschar *out = (jschar *)js_malloc(sizeof(jschar) * (nchars + 1)); if (!out) { oom_ = true; js_free(buf); return NULL; } size_t outlen = nchars; bool ok = InflateStringToBuffer(NULL, buf, nchars, out, &outlen); js_free(buf); if (!ok) { oom_ = true; js_free(out); return NULL; } out[nchars] = 0; return out; } char *finishCString() { if (oom_) return NULL; buf_.append('\0'); char *buf = buf_.extractRawBuffer(); if (!buf) oom_ = true; return buf; } private: void append(const char *name, const char *vfmt, va_list va, const char *units) { char val[MaxFieldValueLength]; JS_vsnprintf(val, MaxFieldValueLength, vfmt, va); put(name, val, units, false); } void p(const char *cstr) { if (oom_) return; if (!buf_.append(cstr, strlen(cstr))) oom_ = true; } void p(const char c) { if (oom_) return; if (!buf_.append(c)) oom_ = true; } void pJSON(const char *str) { if (asJSON_) p(str); } void put(const char *name, const char *val, const char *units, bool valueIsQuoted) { if (needComma_) p(", "); needComma_ = true; putKey(name); p(": "); if (valueIsQuoted) putQuoted(val); else p(val); if (!asJSON_) p(units); } void putQuoted(const char *str) { pJSON("\""); p(str); pJSON("\""); } void putKey(const char *str) { if (!asJSON_) { p(str); return; } p("\""); const char *c = str; while (*c) { if (*c == ' ' || *c == '\t') p('_'); else if (isupper(*c)) p(tolower(*c)); else if (*c == '+') p("added_"); else if (*c == '-') p("removed_"); else if (*c != '(' && *c != ')') p(*c); c++; } p("\""); } }; /* * 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(gcreason::NUM_TELEMETRY_REASONS >= gcreason::NUM_REASONS); static const char * ExplainReason(gcreason::Reason reason) { switch (reason) { #define SWITCH_REASON(name) \ case gcreason::name: \ return #name; GCREASONS(SWITCH_REASON) default: JS_NOT_REACHED("bad GC reason"); return "?"; #undef SWITCH_REASON } } static double t(int64_t t) { return double(t) / PRMJ_USEC_PER_MSEC; } struct PhaseInfo { unsigned index; const char *name; }; static PhaseInfo phases[] = { { PHASE_GC_BEGIN, "Begin Callback" }, { PHASE_WAIT_BACKGROUND_THREAD, "Wait Background Thread" }, { PHASE_PURGE, "Purge" }, { PHASE_MARK, "Mark" }, { PHASE_MARK_ROOTS, "Mark Roots" }, { PHASE_MARK_TYPES, "Mark Types" }, { PHASE_MARK_DELAYED, "Mark Delayed" }, { PHASE_MARK_OTHER, "Mark Other" }, { PHASE_FINALIZE_START, "Finalize Start Callback" }, { PHASE_SWEEP, "Sweep" }, { PHASE_SWEEP_COMPARTMENTS, "Sweep Compartments" }, { PHASE_SWEEP_OBJECT, "Sweep Object" }, { PHASE_SWEEP_STRING, "Sweep String" }, { PHASE_SWEEP_SCRIPT, "Sweep Script" }, { PHASE_SWEEP_SHAPE, "Sweep Shape" }, { PHASE_DISCARD_CODE, "Discard Code" }, { PHASE_DISCARD_ANALYSIS, "Discard Analysis" }, { PHASE_DISCARD_TI, "Discard TI" }, { PHASE_SWEEP_TYPES, "Sweep Types" }, { PHASE_CLEAR_SCRIPT_ANALYSIS, "Clear Script Analysis" }, { PHASE_FINALIZE_END, "Finalize End Callback" }, { PHASE_DESTROY, "Deallocate" }, { PHASE_GC_END, "End Callback" }, { 0, NULL } }; static void FormatPhaseTimes(StatisticsSerializer &ss, const char *name, int64_t *times) { ss.beginObject(name); for (unsigned i = 0; phases[i].name; i++) ss.appendIfNonzeroMS(phases[i].name, t(times[phases[i].index])); ss.endObject(); } static void FormatPhaseFaults(StatisticsSerializer &ss, const char *name, size_t *faults) { ss.beginObject(name); for (unsigned i = 0; phases[i].name; i++) ss.appendNumber(phases[i].name, "%u", "", unsigned(faults[phases[i].index])); ss.endObject(); } bool Statistics::formatData(StatisticsSerializer &ss, uint64_t timestamp) { int64_t total = 0, longest = 0; for (SliceData *slice = slices.begin(); slice != slices.end(); slice++) { total += slice->duration(); if (slice->duration() > longest) longest = slice->duration(); } double mmu20 = computeMMU(20 * PRMJ_USEC_PER_MSEC); double mmu50 = computeMMU(50 * PRMJ_USEC_PER_MSEC); ss.beginObject(NULL); if (ss.isJSON()) ss.appendNumber("Timestamp", "%llu", "", (unsigned long long)timestamp); ss.appendDecimal("Total Time", "ms", t(total)); ss.appendNumber("Compartments Collected", "%d", "", collectedCount); ss.appendNumber("Total Compartments", "%d", "", compartmentCount); ss.appendNumber("MMU (20ms)", "%d", "%", int(mmu20 * 100)); ss.appendNumber("MMU (50ms)", "%d", "%", int(mmu50 * 100)); if (slices.length() > 1 || ss.isJSON()) ss.appendDecimal("Max Pause", "ms", t(longest)); else ss.appendString("Reason", ExplainReason(slices[0].reason)); if (nonincrementalReason || ss.isJSON()) { ss.appendString("Nonincremental Reason", nonincrementalReason ? nonincrementalReason : "none"); } ss.appendNumber("Allocated", "%u", "MB", unsigned(preBytes / 1024 / 1024)); ss.appendNumber("+Chunks", "%d", "", counts[STAT_NEW_CHUNK]); ss.appendNumber("-Chunks", "%d", "", counts[STAT_DESTROY_CHUNK]); ss.endLine(); if (slices.length() > 1 || ss.isJSON()) { ss.beginArray("Slices"); for (size_t i = 0; i < slices.length(); i++) { int64_t width = slices[i].duration(); if (i != 0 && i != slices.length() - 1 && width < SLICE_MIN_REPORT_TIME && !slices[i].resetReason && !ss.isJSON()) { continue; } ss.beginObject(NULL); ss.extra(" "); ss.appendNumber("Slice", "%d", "", i); ss.appendDecimal("Pause", "", t(width)); ss.extra(" ("); ss.appendDecimal("When", "ms", t(slices[i].start - slices[0].start)); ss.appendString("Reason", ExplainReason(slices[i].reason)); if (slices[i].resetReason) ss.appendString("Reset", slices[i].resetReason); ss.extra("): "); FormatPhaseTimes(ss, "Times", slices[i].phaseTimes); if (ss.isJSON()) FormatPhaseFaults(ss, "Page Faults", slices[i].phaseFaults); ss.endLine(); ss.endObject(); } ss.endArray(); } ss.extra(" Totals: "); FormatPhaseTimes(ss, "Totals", phaseTimes); if (ss.isJSON()) FormatPhaseFaults(ss, "Total Page Faults", phaseFaults); ss.endObject(); return !ss.isOOM(); } jschar * Statistics::formatMessage() { StatisticsSerializer ss(StatisticsSerializer::AsText); formatData(ss, 0); return ss.finishJSString(); } jschar * Statistics::formatJSON(uint64_t timestamp) { StatisticsSerializer ss(StatisticsSerializer::AsJSON); formatData(ss, timestamp); return ss.finishJSString(); } Statistics::Statistics(JSRuntime *rt) : runtime(rt), startupTime(PRMJ_Now()), fp(NULL), fullFormat(false), gcDepth(0), collectedCount(0), compartmentCount(0), nonincrementalReason(NULL) { PodArrayZero(phaseTotals); PodArrayZero(counts); char *env = getenv("MOZ_GCTIMER"); if (!env || strcmp(env, "none") == 0) { fp = NULL; return; } if (strcmp(env, "stdout") == 0) { fullFormat = false; fp = stdout; } else if (strcmp(env, "stderr") == 0) { fullFormat = false; fp = stderr; } else { fullFormat = true; fp = fopen(env, "a"); JS_ASSERT(fp); } } Statistics::~Statistics() { if (fp) { if (fullFormat) { StatisticsSerializer ss(StatisticsSerializer::AsText); FormatPhaseTimes(ss, "", phaseTotals); char *msg = ss.finishCString(); if (msg) { fprintf(fp, "TOTALS\n%s\n\n-------\n", msg); js_free(msg); } } if (fp != stdout && fp != stderr) fclose(fp); } } int64_t Statistics::gcDuration() { return slices.back().end - slices[0].start; } void Statistics::printStats() { if (fullFormat) { StatisticsSerializer ss(StatisticsSerializer::AsText); formatData(ss, 0); char *msg = ss.finishCString(); if (msg) { fprintf(fp, "GC(T+%.3fs) %s\n", t(slices[0].start - startupTime) / 1000.0, msg); js_free(msg); } } else { fprintf(fp, "%f %f %f\n", t(gcDuration()), t(phaseTimes[PHASE_MARK]), t(phaseTimes[PHASE_SWEEP])); } fflush(fp); } void Statistics::beginGC() { PodArrayZero(phaseStartTimes); PodArrayZero(phaseStartFaults); PodArrayZero(phaseTimes); PodArrayZero(phaseFaults); slices.clearAndFree(); nonincrementalReason = NULL; preBytes = runtime->gcBytes; Probes::GCStart(); } void Statistics::endGC() { Probes::GCEnd(); crash::SnapshotGCStack(); for (int i = 0; i < PHASE_LIMIT; i++) phaseTotals[i] += phaseTimes[i]; if (JSAccumulateTelemetryDataCallback cb = runtime->telemetryCallback) { (*cb)(JS_TELEMETRY_GC_IS_COMPARTMENTAL, collectedCount == compartmentCount ? 0 : 1); (*cb)(JS_TELEMETRY_GC_MS, t(gcDuration())); (*cb)(JS_TELEMETRY_GC_MARK_MS, t(phaseTimes[PHASE_MARK])); (*cb)(JS_TELEMETRY_GC_SWEEP_MS, t(phaseTimes[PHASE_SWEEP])); (*cb)(JS_TELEMETRY_GC_NON_INCREMENTAL, !!nonincrementalReason); (*cb)(JS_TELEMETRY_GC_INCREMENTAL_DISABLED, !runtime->gcIncrementalEnabled); double mmu50 = computeMMU(50 * PRMJ_USEC_PER_MSEC); (*cb)(JS_TELEMETRY_GC_MMU_50, mmu50 * 100); } if (fp) printStats(); } void Statistics::beginSlice(int collectedCount, int compartmentCount, gcreason::Reason reason) { this->collectedCount = collectedCount; this->compartmentCount = compartmentCount; bool first = runtime->gcIncrementalState == gc::NO_INCREMENTAL; if (first) beginGC(); SliceData data(reason, PRMJ_Now()); (void) slices.append(data); /* Ignore any OOMs here. */ if (JSAccumulateTelemetryDataCallback cb = runtime->telemetryCallback) (*cb)(JS_TELEMETRY_GC_REASON, reason); // Slice callbacks should only fire for the outermost level if (++gcDepth == 1) { bool wasFullGC = collectedCount == compartmentCount; if (GCSliceCallback cb = runtime->gcSliceCallback) (*cb)(runtime, first ? GC_CYCLE_BEGIN : GC_SLICE_BEGIN, GCDescription(!wasFullGC)); } } void Statistics::endSlice() { slices.back().end = PRMJ_Now(); if (JSAccumulateTelemetryDataCallback cb = runtime->telemetryCallback) { (*cb)(JS_TELEMETRY_GC_SLICE_MS, t(slices.back().end - slices.back().start)); (*cb)(JS_TELEMETRY_GC_RESET, !!slices.back().resetReason); } bool last = runtime->gcIncrementalState == gc::NO_INCREMENTAL; if (last) endGC(); // Slice callbacks should only fire for the outermost level if (--gcDepth == 0) { bool wasFullGC = collectedCount == compartmentCount; if (GCSliceCallback cb = runtime->gcSliceCallback) (*cb)(runtime, last ? GC_CYCLE_END : GC_SLICE_END, GCDescription(!wasFullGC)); } /* Do this after the slice callback since it uses these values. */ if (last) PodArrayZero(counts); } void Statistics::beginPhase(Phase phase) { /* Guard against re-entry */ JS_ASSERT(!phaseStartTimes[phase]); phaseStartTimes[phase] = PRMJ_Now(); phaseStartFaults[phase] = gc::GetPageFaultCount(); if (phase == gcstats::PHASE_MARK) Probes::GCStartMarkPhase(); else if (phase == gcstats::PHASE_SWEEP) Probes::GCStartSweepPhase(); } void Statistics::endPhase(Phase phase) { int64_t t = PRMJ_Now() - phaseStartTimes[phase]; slices.back().phaseTimes[phase] += t; phaseTimes[phase] += t; phaseStartTimes[phase] = 0; size_t faults = gc::GetPageFaultCount() - phaseStartFaults[phase]; slices.back().phaseFaults[phase] += faults; phaseFaults[phase] += faults; if (phase == gcstats::PHASE_MARK) Probes::GCEndMarkPhase(); else if (phase == gcstats::PHASE_SWEEP) Probes::GCEndSweepPhase(); } /* * 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) { JS_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; } } /* namespace gcstats */ } /* namespace js */