blob: b29bc5f996f6cf7e1c918d4669b71a7700cdfdbc [file] [log] [blame]
/* -*- 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 <stdio.h>
#include <stdarg.h>
#include "mozilla/PodOperations.h"
#include "jscntxt.h"
#include "jscrashreport.h"
#include "jsprf.h"
#include "jsutil.h"
#include "prmjtime.h"
#include "gc/Memory.h"
using namespace js;
using namespace js::gcstats;
using mozilla::PodArrayZero;
/* Except for the first and last, slices of less than 42ms are not reported. */
static const int64_t SLICE_MIN_REPORT_TIME = 42 * PRMJ_USEC_PER_MSEC;
class gcstats::StatisticsSerializer
{
typedef Vector<char, 128, SystemAllocPolicy> 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 = js_pod_malloc<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(JS::gcreason::NUM_TELEMETRY_REASONS >= JS::gcreason::NUM_REASONS);
static const char *
ExplainReason(JS::gcreason::Reason reason)
{
switch (reason) {
#define SWITCH_REASON(name) \
case JS::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
{
Phase index;
const char *name;
Phase parent;
};
static const Phase PHASE_NO_PARENT = PHASE_LIMIT;
static const PhaseInfo phases[] = {
{ PHASE_GC_BEGIN, "Begin Callback", PHASE_NO_PARENT },
{ PHASE_WAIT_BACKGROUND_THREAD, "Wait Background Thread", PHASE_NO_PARENT },
{ PHASE_MARK_DISCARD_CODE, "Mark Discard Code", PHASE_NO_PARENT },
{ PHASE_PURGE, "Purge", PHASE_NO_PARENT },
{ PHASE_MARK, "Mark", PHASE_NO_PARENT },
{ PHASE_MARK_ROOTS, "Mark Roots", PHASE_MARK },
{ PHASE_MARK_TYPES, "Mark Types", PHASE_MARK_ROOTS },
{ PHASE_MARK_DELAYED, "Mark Delayed", PHASE_MARK },
{ PHASE_SWEEP, "Sweep", PHASE_NO_PARENT },
{ PHASE_SWEEP_MARK, "Mark During Sweeping", PHASE_SWEEP },
{ PHASE_SWEEP_MARK_TYPES, "Mark Types During Sweeping", PHASE_SWEEP_MARK },
{ PHASE_SWEEP_MARK_DELAYED, "Mark Delayed During Sweeping", PHASE_SWEEP_MARK },
{ PHASE_SWEEP_MARK_INCOMING_BLACK, "Mark Incoming Black Pointers", PHASE_SWEEP_MARK },
{ PHASE_SWEEP_MARK_WEAK, "Mark Weak", PHASE_SWEEP_MARK },
{ PHASE_SWEEP_MARK_INCOMING_GRAY, "Mark Incoming Gray Pointers", PHASE_SWEEP_MARK },
{ PHASE_SWEEP_MARK_GRAY, "Mark Gray", PHASE_SWEEP_MARK },
{ PHASE_SWEEP_MARK_GRAY_WEAK, "Mark Gray and Weak", PHASE_SWEEP_MARK },
{ PHASE_FINALIZE_START, "Finalize Start Callback", PHASE_SWEEP },
{ PHASE_SWEEP_ATOMS, "Sweep Atoms", PHASE_SWEEP },
{ PHASE_SWEEP_COMPARTMENTS, "Sweep Compartments", PHASE_SWEEP },
{ PHASE_SWEEP_DISCARD_CODE, "Sweep Discard Code", PHASE_SWEEP_COMPARTMENTS },
{ PHASE_SWEEP_TABLES, "Sweep Tables", PHASE_SWEEP_COMPARTMENTS },
{ PHASE_SWEEP_TABLES_WRAPPER, "Sweep Cross Compartment Wrappers", PHASE_SWEEP_TABLES },
{ PHASE_SWEEP_TABLES_BASE_SHAPE, "Sweep Base Shapes", PHASE_SWEEP_TABLES },
{ PHASE_SWEEP_TABLES_INITIAL_SHAPE, "Sweep Intital Shapes", PHASE_SWEEP_TABLES },
{ PHASE_SWEEP_TABLES_TYPE_OBJECT, "Sweep Type Objects", PHASE_SWEEP_TABLES },
{ PHASE_SWEEP_TABLES_BREAKPOINT, "Sweep Breakpoints", PHASE_SWEEP_TABLES },
{ PHASE_SWEEP_TABLES_REGEXP, "Sweep Regexps", PHASE_SWEEP_TABLES },
{ PHASE_DISCARD_ANALYSIS, "Discard Analysis", PHASE_SWEEP_COMPARTMENTS },
{ PHASE_DISCARD_TI, "Discard TI", PHASE_DISCARD_ANALYSIS },
{ PHASE_FREE_TI_ARENA, "Free TI Arena", PHASE_DISCARD_ANALYSIS },
{ PHASE_SWEEP_TYPES, "Sweep Types", PHASE_DISCARD_ANALYSIS },
{ PHASE_CLEAR_SCRIPT_ANALYSIS, "Clear Script Analysis", PHASE_DISCARD_ANALYSIS },
{ PHASE_SWEEP_OBJECT, "Sweep Object", PHASE_SWEEP },
{ PHASE_SWEEP_STRING, "Sweep String", PHASE_SWEEP },
{ PHASE_SWEEP_SCRIPT, "Sweep Script", PHASE_SWEEP },
{ PHASE_SWEEP_SHAPE, "Sweep Shape", PHASE_SWEEP },
{ PHASE_SWEEP_IONCODE, "Sweep Ion code", PHASE_SWEEP },
{ PHASE_FINALIZE_END, "Finalize End Callback", PHASE_SWEEP },
{ PHASE_DESTROY, "Deallocate", PHASE_SWEEP },
{ PHASE_GC_END, "End Callback", PHASE_NO_PARENT },
{ PHASE_LIMIT, NULL, PHASE_NO_PARENT }
};
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();
}
void
Statistics::gcDuration(int64_t *total, int64_t *maxPause)
{
*total = *maxPause = 0;
for (SliceData *slice = slices.begin(); slice != slices.end(); slice++) {
*total += slice->duration();
if (slice->duration() > *maxPause)
*maxPause = slice->duration();
}
}
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]);
}
}
bool
Statistics::formatData(StatisticsSerializer &ss, 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);
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("Total Zones", "%d", "", zoneCount);
ss.appendNumber("MMU (20ms)", "%d", "%", int(mmu20 * 100));
ss.appendNumber("MMU (50ms)", "%d", "%", int(mmu50 * 100));
ss.appendDecimal("SCC Sweep Total", "ms", t(sccTotal));
ss.appendDecimal("SCC Sweep Max Pause", "ms", t(sccLongest));
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 (ss.isJSON()) {
ss.appendDecimal("Page Faults", "",
double(slices[i].endFaults - slices[i].startFaults));
ss.appendNumber("Start Timestamp", "%llu", "", (unsigned long long)slices[i].start);
ss.appendNumber("End Timestamp", "%llu", "", (unsigned long long)slices[i].end);
}
if (slices[i].resetReason)
ss.appendString("Reset", slices[i].resetReason);
ss.extra("): ");
FormatPhaseTimes(ss, "Times", slices[i].phaseTimes);
ss.endLine();
ss.endObject();
}
ss.endArray();
}
ss.extra(" Totals: ");
FormatPhaseTimes(ss, "Totals", phaseTimes);
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),
zoneCount(0),
compartmentCount(0),
nonincrementalReason(NULL),
preBytes(0),
phaseNestingDepth(0)
{
PodArrayZero(phaseTotals);
PodArrayZero(counts);
#if defined(STARBOARD)
return;
#else
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);
}
#endif
}
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);
}
}
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 {
int64_t total, longest;
gcDuration(&total, &longest);
fprintf(fp, "%f %f %f\n",
t(total),
t(phaseTimes[PHASE_MARK]),
t(phaseTimes[PHASE_SWEEP]));
}
fflush(fp);
}
void
Statistics::beginGC()
{
PodArrayZero(phaseStartTimes);
PodArrayZero(phaseTimes);
slices.clearAndFree();
sccTimes.clearAndFree();
nonincrementalReason = NULL;
preBytes = runtime->gcBytes;
}
void
Statistics::endGC()
{
crash::SnapshotGCStack();
for (int i = 0; i < PHASE_LIMIT; i++)
phaseTotals[i] += phaseTimes[i];
if (JSAccumulateTelemetryDataCallback cb = runtime->telemetryCallback) {
int64_t total, longest;
gcDuration(&total, &longest);
int64_t sccTotal, sccLongest;
sccDurations(&sccTotal, &sccLongest);
(*cb)(JS_TELEMETRY_GC_IS_COMPARTMENTAL, collectedCount == zoneCount ? 0 : 1);
(*cb)(JS_TELEMETRY_GC_MS, t(total));
(*cb)(JS_TELEMETRY_GC_MAX_PAUSE_MS, t(longest));
(*cb)(JS_TELEMETRY_GC_MARK_MS, t(phaseTimes[PHASE_MARK]));
(*cb)(JS_TELEMETRY_GC_SWEEP_MS, t(phaseTimes[PHASE_SWEEP]));
(*cb)(JS_TELEMETRY_GC_MARK_ROOTS_MS, t(phaseTimes[PHASE_MARK_ROOTS]));
(*cb)(JS_TELEMETRY_GC_MARK_GRAY_MS, t(phaseTimes[PHASE_SWEEP_MARK_GRAY]));
(*cb)(JS_TELEMETRY_GC_NON_INCREMENTAL, !!nonincrementalReason);
(*cb)(JS_TELEMETRY_GC_INCREMENTAL_DISABLED, !runtime->gcIncrementalEnabled);
(*cb)(JS_TELEMETRY_GC_SCC_SWEEP_TOTAL_MS, t(sccTotal));
(*cb)(JS_TELEMETRY_GC_SCC_SWEEP_MAX_PAUSE_MS, t(sccLongest));
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 zoneCount, int compartmentCount,
JS::gcreason::Reason reason)
{
this->collectedCount = collectedCount;
this->zoneCount = zoneCount;
this->compartmentCount = compartmentCount;
bool first = runtime->gcIncrementalState == gc::NO_INCREMENTAL;
if (first)
beginGC();
SliceData data(reason, PRMJ_Now(), gc::GetPageFaultCount());
(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 == zoneCount;
if (JS::GCSliceCallback cb = runtime->gcSliceCallback)
(*cb)(runtime, first ? JS::GC_CYCLE_BEGIN : JS::GC_SLICE_BEGIN,
JS::GCDescription(!wasFullGC));
}
}
void
Statistics::endSlice()
{
slices.back().end = PRMJ_Now();
slices.back().endFaults = gc::GetPageFaultCount();
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 == zoneCount;
if (JS::GCSliceCallback cb = runtime->gcSliceCallback)
(*cb)(runtime, last ? JS::GC_CYCLE_END : JS::GC_SLICE_END,
JS::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]);
#ifdef DEBUG
JS_ASSERT(phases[phase].index == phase);
Phase parent = phaseNestingDepth ? phaseNesting[phaseNestingDepth - 1] : PHASE_NO_PARENT;
JS_ASSERT(phaseNestingDepth < MAX_NESTING);
JS_ASSERT_IF(gcDepth == 1, phases[phase].parent == parent);
phaseNesting[phaseNestingDepth] = phase;
phaseNestingDepth++;
#endif
phaseStartTimes[phase] = PRMJ_Now();
}
void
Statistics::endPhase(Phase phase)
{
phaseNestingDepth--;
int64_t t = PRMJ_Now() - phaseStartTimes[phase];
slices.back().phaseTimes[phase] += t;
phaseTimes[phase] += t;
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)
{
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;
}