diff options
Diffstat (limited to 'toolkit/components/telemetry/GCTelemetry.jsm')
-rw-r--r-- | toolkit/components/telemetry/GCTelemetry.jsm | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/GCTelemetry.jsm b/toolkit/components/telemetry/GCTelemetry.jsm new file mode 100644 index 000000000..43a4ea9ca --- /dev/null +++ b/toolkit/components/telemetry/GCTelemetry.jsm @@ -0,0 +1,216 @@ +/* -*- 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; + }, +}; |