gecko-dev/toolkit/components/telemetry/other/GCTelemetry.jsm
Victor Porof 4a06c925ac Bug 1561435 - Format toolkit/components/, a=automatic-formatting
# ignore-this-changeset

Differential Revision: https://phabricator.services.mozilla.com/D36052

--HG--
extra : source : b5be5b4f4b47c256e28a29f665dc754f6407ee7f
2019-07-05 11:14:05 +02:00

255 lines
7.5 KiB
JavaScript

/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
/* 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/. */
"use strict";
/**
* This module records detailed timing information about selected
* GCs. The data is sent back in the telemetry session ping. To avoid
* bloating the ping, only a few GCs are included. There are two
* selection strategies. We always save the two GCs with the worst
* max_pause time. Additionally, two collections are selected at
* random. If a GC runs for C milliseconds and the total time for all
* GCs since the session began is T milliseconds, then the GC has a
* 2*C/T probablility of being selected (the factor of 2 is because we
* save two of them).
*
* GCs from both the main process and all content processes are
* recorded. The data is cleared for each new subsession.
*/
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
var EXPORTED_SYMBOLS = ["GCTelemetry"];
// Names of processes where we record GCs.
const PROCESS_NAMES = ["main", "content"];
// Should be the time we started up in milliseconds since the epoch.
const BASE_TIME = Date.now() - Services.telemetry.msSinceProcessStart();
// Records selected GCs. There is one instance per process type.
class GCData {
constructor(kind) {
let numRandom = { main: 0, content: 2 };
let numWorst = { main: 2, content: 2 };
this.totalGCTime = 0;
this.randomlySelected = Array(numRandom[kind]).fill(null);
this.worst = Array(numWorst[kind]).fill(null);
}
// Turn absolute timestamps (in microseconds since the epoch) into
// milliseconds since startup.
rebaseTimes(data) {
function fixup(t) {
return t / 1000.0 - BASE_TIME;
}
data.timestamp = fixup(data.timestamp);
for (let i = 0; i < data.slices_list.length; i++) {
let slice = data.slices_list[i];
slice.start_timestamp = fixup(slice.start_timestamp);
// Slices have no end_timestamp, instead they have a duration.
}
}
// Records a GC (represented by |data|) in the randomlySelected or
// worst batches depending on the criteria above.
record(data) {
this.rebaseTimes(data);
let time = data.total_time;
this.totalGCTime += time;
// Probability that we will replace any one of our
// current randomlySelected GCs with |data|.
let prob = time / this.totalGCTime;
// Note that we may replace multiple GCs in
// randomlySelected. It's easier to reason about the
// probabilities this way, and it's unlikely to have any effect in
// practice.
for (let i = 0; i < this.randomlySelected.length; i++) {
let r = Math.random();
if (r <= prob) {
this.randomlySelected[i] = data;
}
}
// Save the 2 worst GCs based on max_pause. A GC may appear in
// both worst and randomlySelected.
for (let i = 0; i < this.worst.length; i++) {
if (!this.worst[i]) {
this.worst[i] = data;
break;
}
if (this.worst[i].max_pause < data.max_pause) {
this.worst.splice(i, 0, data);
this.worst.length--;
break;
}
}
}
entries() {
return {
random: this.randomlySelected.filter(e => e !== null),
worst: this.worst.filter(e => e !== null),
};
}
}
// NOTE: If you adjust any of the constants here (slice limit, number of
// keys, etc.) make sure to update the JSON schema at:
// https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/dev/templates/include/telemetry/gcItem.1.schema.json
// You should also adjust browser_TelemetryGC.js.
const MAX_GC_KEYS = 24;
const MAX_SLICES = 4;
const MAX_SLICE_KEYS = 12;
// Phases are 0-based, so we need one more than the maximum reported in
// StatsPhasesGenerated.inc. Phase 0 is probably never used by telemetry,
// but lets allow it anyway.
const MAX_PHASES = 73;
const LOGGER_NAME = "Toolkit.Telemetry";
function limitProperties(name, obj, count, log) {
// If there are too many properties delete all/most of them. We don't
// expect this ever to happen.
let num_properties = Object.keys(obj).length;
if (num_properties > count) {
for (let key of Object.keys(obj)) {
// If this is the main GC object then save some of the critical
// properties.
if (
name === "data" &&
(key === "max_pause" ||
key === "slices" ||
key === "slices_list" ||
key === "status" ||
key === "timestamp" ||
key === "total_time" ||
key === "totals")
) {
continue;
}
delete obj[key];
}
let log_fn;
if (name === "slice.times" || name === "data.totals") {
// This is a bit more likely, but is mostly-okay.
log_fn = s => log.info(s);
} else {
log_fn = s => log.warn(s);
}
log_fn(
`Number of properties exceeded in the GC telemetry ${name} ping, expected ${count} got ${num_properties}`
);
}
}
/*
* Reduce the size of the object by limiting the number of slices or times
* etc.
*/
function limitSize(data, log) {
data.slices_list.sort((a, b) => b.pause - a.pause);
if (data.slices_list.length > MAX_SLICES) {
// Make sure we always keep the first slice since it has the
// reason the GC was started.
let firstSliceIndex = data.slices_list.findIndex(s => s.slice == 0);
if (firstSliceIndex >= MAX_SLICES) {
data.slices_list[MAX_SLICES - 1] = data.slices_list[firstSliceIndex];
}
data.slices_list.length = MAX_SLICES;
}
data.slices_list.sort((a, b) => a.slice - b.slice);
limitProperties("data", data, MAX_GC_KEYS, log);
for (let slice of data.slices_list) {
limitProperties("slice", slice, MAX_SLICE_KEYS, log);
limitProperties("slice.times", slice.times, MAX_PHASES, log);
}
limitProperties("data.totals", data.totals, MAX_PHASES, log);
}
let processData = new Map();
for (let name of PROCESS_NAMES) {
processData.set(name, new GCData(name));
}
var GCTelemetry = {
initialized: false,
init() {
if (this.initialized) {
return false;
}
this.initialized = true;
this._log = Log.repository.getLoggerWithMessagePrefix(
LOGGER_NAME,
"GCTelemetry::"
);
Services.obs.addObserver(this, "garbage-collection-statistics");
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
Services.ppmm.addMessageListener("Telemetry:GCStatistics", this);
}
return true;
},
shutdown() {
if (!this.initialized) {
return;
}
Services.obs.removeObserver(this, "garbage-collection-statistics");
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
Services.ppmm.removeMessageListener("Telemetry:GCStatistics", this);
}
this.initialized = false;
},
observe(subject, topic, arg) {
this.observeRaw(JSON.parse(arg));
},
// We expose this method so unit tests can call it, no need to test JSON
// parsing.
observeRaw(data) {
limitSize(data, this._log);
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
processData.get("main").record(data);
} else {
Services.cpmm.sendAsyncMessage("Telemetry:GCStatistics", data);
}
},
receiveMessage(msg) {
processData.get("content").record(msg.data);
},
entries(kind, clear) {
let result = processData.get(kind).entries();
if (clear) {
processData.set(kind, new GCData(kind));
}
return result;
},
};