123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- /* -*- 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 five GCs with the worst
- * max_pause time. Additionally, five 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
- * 5*C/T probablility of being selected (the factor of 5 is because we
- * save 5 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.length; i++) {
- let slice = data.slices[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 5 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 = 25;
- const MAX_SLICES = 4;
- const MAX_SLICE_KEYS = 15;
- 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.length;
- data.slices.sort((a, b) => b.pause - a.pause);
- if (data.slices.length > MAX_SLICES) {
- // Make sure we always keep the first slice since it has the
- // reason the GC was started.
- let firstSliceIndex = data.slices.findIndex(s => s.slice == 0);
- if (firstSliceIndex >= MAX_SLICES) {
- data.slices[MAX_SLICES - 1] = data.slices[firstSliceIndex];
- }
- data.slices.length = MAX_SLICES;
- }
- data.slices.sort((a, b) => a.slice - b.slice);
- limitProperties(data, MAX_GC_KEYS);
- for (let slice of data.slices) {
- 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", false);
- 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;
- },
- };
|