mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-09 04:39:03 +02:00
# ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D36052 --HG-- extra : source : b5be5b4f4b47c256e28a29f665dc754f6407ee7f
255 lines
7.5 KiB
JavaScript
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;
|
|
},
|
|
};
|