зеркало из https://github.com/mozilla/gecko-dev.git
217 строки
6.2 KiB
JavaScript
217 строки
6.2 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.
|
|
*/
|
|
|
|
const Cu = Components.utils;
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm", this);
|
|
|
|
this.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);
|
|
slice.end_timestamp = fixup(slice.end_timestamp);
|
|
}
|
|
}
|
|
|
|
// 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),
|
|
};
|
|
}
|
|
}
|
|
|
|
// 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/master/telemetry/main.schema.json
|
|
// You should also adjust browser_TelemetryGC.js.
|
|
const MAX_GC_KEYS = 24;
|
|
const MAX_SLICES = 4;
|
|
const MAX_SLICE_KEYS = 12;
|
|
const MAX_PHASES = 65;
|
|
|
|
function limitProperties(obj, count) {
|
|
// If there are too many properties, just delete them all. We don't
|
|
// expect this ever to happen.
|
|
if (Object.keys(obj).length > count) {
|
|
for (let key of Object.keys(obj)) {
|
|
delete obj[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
function limitSize(data) {
|
|
// Store the number of slices so we know if we lost any at the end.
|
|
data.num_slices = data.slices_list.length;
|
|
|
|
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, MAX_GC_KEYS);
|
|
|
|
for (let slice of data.slices_list) {
|
|
limitProperties(slice, MAX_SLICE_KEYS);
|
|
limitProperties(slice.times, MAX_PHASES);
|
|
}
|
|
|
|
limitProperties(data.totals, MAX_PHASES);
|
|
}
|
|
|
|
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;
|
|
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) {
|
|
let data = JSON.parse(arg);
|
|
|
|
limitSize(data);
|
|
|
|
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;
|
|
},
|
|
};
|