mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1136945 - Convert GC events from memory actor to be emitted as pseudo-markers from the TimelineActor. Pull out the core of the MemoryActor into a bridge, so it does not have to be used over RDP. r=vp,fitzgen
This commit is contained in:
parent
b6c103248f
commit
e61d1fbb02
@ -11,7 +11,7 @@ support-files =
|
||||
# that need to be moved over to performance tool
|
||||
|
||||
[browser_perf-aaa-run-first-leaktest.js]
|
||||
|
||||
[browser_markers-gc.js]
|
||||
[browser_markers-parse-html.js]
|
||||
[browser_perf-allocations-to-samples.js]
|
||||
[browser_perf-compatibility-01.js]
|
||||
|
47
browser/devtools/performance/test/browser_markers-gc.js
Normal file
47
browser/devtools/performance/test/browser_markers-gc.js
Normal file
@ -0,0 +1,47 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Test that we get a "GarbageCollection" marker.
|
||||
*/
|
||||
|
||||
const TIME_CLOSE_TO = 10000;
|
||||
|
||||
function* spawnTest () {
|
||||
let { target, front } = yield initBackend(SIMPLE_URL);
|
||||
let markers;
|
||||
|
||||
front.on("timeline-data", handler);
|
||||
let model = yield front.startRecording({ withTicks: true });
|
||||
|
||||
// Check async for markers found while GC/CCing between
|
||||
yield waitUntil(() => {
|
||||
forceCC();
|
||||
return !!markers;
|
||||
}, 100);
|
||||
|
||||
front.off("timeline-data", handler);
|
||||
yield front.stopRecording(model);
|
||||
|
||||
info(`Got ${markers.length} markers.`);
|
||||
|
||||
let maxMarkerTime = model._timelineStartTime + model.getDuration() + TIME_CLOSE_TO;
|
||||
|
||||
ok(markers.every(({name}) => name === "GarbageCollection"), "All markers found are GC markers");
|
||||
ok(markers.length > 0, "found atleast one GC marker");
|
||||
ok(markers.every(({start}) => typeof start === "number" && start > 0 && start < maxMarkerTime),
|
||||
"All markers have a start time between the valid range.");
|
||||
ok(markers.every(({end}) => typeof end === "number" && end > 0 && end < maxMarkerTime),
|
||||
"All markers have an end time between the valid range.");
|
||||
ok(markers.every(({causeName}) => typeof causeName === "string"),
|
||||
"All markers have a causeName.");
|
||||
|
||||
yield removeTab(target.tab);
|
||||
finish();
|
||||
|
||||
function handler (_, name, m) {
|
||||
if (name === "markers" && m[0].name === "GarbageCollection") {
|
||||
markers = m;
|
||||
}
|
||||
}
|
||||
}
|
@ -497,3 +497,13 @@ function reload (aTarget, aEvent = "navigate") {
|
||||
aTarget.activeTab.reload();
|
||||
return once(aTarget, aEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces cycle collection and GC, used in AudioNode destruction tests.
|
||||
*/
|
||||
function forceCC () {
|
||||
info("Triggering GC/CC...");
|
||||
SpecialPowers.DOMWindowUtils.cycleCollect();
|
||||
SpecialPowers.DOMWindowUtils.garbageCollect();
|
||||
SpecialPowers.DOMWindowUtils.garbageCollect();
|
||||
}
|
||||
|
@ -21,7 +21,8 @@ const L10N = new ViewHelpers.L10N(STRINGS_URI);
|
||||
* - label: the label used in the waterfall to identify the marker
|
||||
* - colorName: the name of the DevTools color used for this marker. If adding
|
||||
* a new color, be sure to check that there's an entry for
|
||||
* `.marker-details-bullet.{COLORNAME}` for the equivilent entry.
|
||||
* `.marker-details-bullet.{COLORNAME}` for the equivilent entry
|
||||
* in ./browser/themes/shared/devtools/performance.inc.css
|
||||
* https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
|
||||
*
|
||||
* Whenever this is changed, browser_timeline_waterfall-styles.js *must* be
|
||||
@ -68,6 +69,11 @@ const TIMELINE_BLUEPRINT = {
|
||||
colorName: "highlight-bluegrey",
|
||||
label: L10N.getStr("timeline.label.consoleTime")
|
||||
},
|
||||
"GarbageCollection": {
|
||||
group: 1,
|
||||
colorName: "highlight-red",
|
||||
label: L10N.getStr("timeline.label.garbageCollection")
|
||||
},
|
||||
};
|
||||
|
||||
// Exported symbols.
|
||||
|
@ -44,6 +44,7 @@ timeline.label.parseHTML=Parse HTML
|
||||
timeline.label.parseXML=Parse XML
|
||||
timeline.label.domevent=DOM Event
|
||||
timeline.label.consoleTime=Console
|
||||
timeline.label.garbageCollection=GC Event
|
||||
|
||||
# LOCALIZATION NOTE (graphs.memory):
|
||||
# This string is displayed in the memory graph of the Performance tool,
|
||||
|
@ -447,6 +447,13 @@
|
||||
.waterfall-marker-bullet.highlight-lightorange {
|
||||
background-color: var(--theme-highlight-lightorange);
|
||||
}
|
||||
#performance-filter-menupopup > menuitem.highlight-red:before,
|
||||
.marker-details-bullet.highlight-red,
|
||||
.waterfall-marker-bar.highlight-red,
|
||||
.waterfall-marker-bullet.highlight-red {
|
||||
background-color: var(--theme-highlight-red);
|
||||
}
|
||||
|
||||
|
||||
#waterfall-details > * {
|
||||
padding-top: 3px;
|
||||
|
@ -4,41 +4,22 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
let protocol = require("devtools/server/protocol");
|
||||
let { method, RetVal, Arg, types } = protocol;
|
||||
const { reportException } = require("devtools/toolkit/DevToolsUtils");
|
||||
const protocol = require("devtools/server/protocol");
|
||||
const { method, RetVal, Arg, types } = protocol;
|
||||
const { MemoryBridge } = require("./utils/memory-bridge");
|
||||
loader.lazyRequireGetter(this, "events", "sdk/event/core");
|
||||
loader.lazyRequireGetter(this, "StackFrameCache",
|
||||
"devtools/server/actors/utils/stack", true);
|
||||
|
||||
/**
|
||||
* A method decorator that ensures the actor is in the expected state before
|
||||
* proceeding. If the actor is not in the expected state, the decorated method
|
||||
* returns a rejected promise.
|
||||
*
|
||||
* @param String expectedState
|
||||
* The expected state.
|
||||
* @param String activity
|
||||
* Additional info about what's going on.
|
||||
* @param Function method
|
||||
* The actor method to proceed with when the actor is in the expected
|
||||
* state.
|
||||
*
|
||||
* @returns Function
|
||||
* The decorated method.
|
||||
* Proxies a call to the MemoryActor to the underlying MemoryBridge,
|
||||
* allowing access to MemoryBridge features by defining the RDP
|
||||
* request/response signature.
|
||||
*/
|
||||
function expectState(expectedState, method, activity) {
|
||||
return function(...args) {
|
||||
if (this.state !== expectedState) {
|
||||
const msg = `Wrong state while ${activity}:` +
|
||||
`Expected '${expectedState}',` +
|
||||
`but current state is '${this.state}'.`;
|
||||
return Promise.reject(new Error(msg));
|
||||
}
|
||||
|
||||
return method.apply(this, args);
|
||||
};
|
||||
function linkBridge (methodName, definition) {
|
||||
return method(function () {
|
||||
return this.bridge[methodName].apply(this.bridge, arguments);
|
||||
}, definition);
|
||||
}
|
||||
|
||||
types.addDictType("AllocationsRecordingOptions", {
|
||||
@ -66,6 +47,7 @@ let MemoryActor = protocol.ActorClass({
|
||||
* The set of unsolicited events the MemoryActor emits that will be sent over
|
||||
* the RDP (by protocol.js).
|
||||
*/
|
||||
|
||||
events: {
|
||||
// Same format as the data passed to the
|
||||
// `Debugger.Memory.prototype.onGarbageCollection` hook. See
|
||||
@ -76,37 +58,17 @@ let MemoryActor = protocol.ActorClass({
|
||||
},
|
||||
},
|
||||
|
||||
get dbg() {
|
||||
if (!this._dbg) {
|
||||
this._dbg = this.parent.makeDebugger();
|
||||
}
|
||||
return this._dbg;
|
||||
},
|
||||
|
||||
initialize: function(conn, parent, frameCache = new StackFrameCache()) {
|
||||
protocol.Actor.prototype.initialize.call(this, conn);
|
||||
this.parent = parent;
|
||||
this._mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
|
||||
.getService(Ci.nsIMemoryReporterManager);
|
||||
this.state = "detached";
|
||||
this._dbg = null;
|
||||
this._frameCache = frameCache;
|
||||
|
||||
this._onGarbageCollection = data =>
|
||||
events.emit(this, "garbage-collection", data);
|
||||
|
||||
this._onWindowReady = this._onWindowReady.bind(this);
|
||||
|
||||
events.on(this.parent, "window-ready", this._onWindowReady);
|
||||
this._onGarbageCollection = this._onGarbageCollection.bind(this);
|
||||
this.bridge = new MemoryBridge(parent, frameCache);
|
||||
this.bridge.on("garbage-collection", this._onGarbageCollection);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
events.off(this.parent, "window-ready", this._onWindowReady);
|
||||
|
||||
this._mgr = null;
|
||||
if (this.state === "attached") {
|
||||
this.detach();
|
||||
}
|
||||
this.bridge.off("garbage-collection", this._onGarbageCollection);
|
||||
this.bridge.destroy();
|
||||
protocol.Actor.prototype.destroy.call(this);
|
||||
},
|
||||
|
||||
@ -117,12 +79,7 @@ let MemoryActor = protocol.ActorClass({
|
||||
* recording allocations or take a census of the heap. In addition, the
|
||||
* MemoryActor will start emitting GC events.
|
||||
*/
|
||||
attach: method(expectState("detached", function() {
|
||||
this.dbg.addDebuggees();
|
||||
this.dbg.memory.onGarbageCollection = this._onGarbageCollection;
|
||||
this.state = "attached";
|
||||
},
|
||||
`attaching to the debugger`), {
|
||||
attach: linkBridge("attach", {
|
||||
request: {},
|
||||
response: {
|
||||
type: "attached"
|
||||
@ -132,13 +89,7 @@ let MemoryActor = protocol.ActorClass({
|
||||
/**
|
||||
* Detach from this MemoryActor.
|
||||
*/
|
||||
detach: method(expectState("attached", function() {
|
||||
this._clearDebuggees();
|
||||
this.dbg.enabled = false;
|
||||
this._dbg = null;
|
||||
this.state = "detached";
|
||||
},
|
||||
`detaching from the debugger`), {
|
||||
detach: linkBridge("detach", {
|
||||
request: {},
|
||||
response: {
|
||||
type: "detached"
|
||||
@ -148,51 +99,17 @@ let MemoryActor = protocol.ActorClass({
|
||||
/**
|
||||
* Gets the current MemoryActor attach/detach state.
|
||||
*/
|
||||
getState: method(function() {
|
||||
return this.state;
|
||||
}, {
|
||||
getState: linkBridge("getState", {
|
||||
response: {
|
||||
state: RetVal(0, "string")
|
||||
}
|
||||
}),
|
||||
|
||||
_clearDebuggees: function() {
|
||||
if (this._dbg) {
|
||||
if (this.dbg.memory.trackingAllocationSites) {
|
||||
this.dbg.memory.drainAllocationsLog();
|
||||
}
|
||||
this._clearFrames();
|
||||
this.dbg.removeAllDebuggees();
|
||||
}
|
||||
},
|
||||
|
||||
_clearFrames: function() {
|
||||
if (this.dbg.memory.trackingAllocationSites) {
|
||||
this._frameCache.clearFrames();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler for the parent actor's "window-ready" event.
|
||||
*/
|
||||
_onWindowReady: function({ isTopLevel }) {
|
||||
if (this.state == "attached") {
|
||||
if (isTopLevel && this.dbg.memory.trackingAllocationSites) {
|
||||
this._clearDebuggees();
|
||||
this._frameCache.initFrames();
|
||||
}
|
||||
this.dbg.addDebuggees();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Take a census of the heap. See js/src/doc/Debugger/Debugger.Memory.md for
|
||||
* more information.
|
||||
*/
|
||||
takeCensus: method(expectState("attached", function() {
|
||||
return this.dbg.memory.takeCensus();
|
||||
},
|
||||
`taking census`), {
|
||||
takeCensus: linkBridge("takeCensus", {
|
||||
request: {},
|
||||
response: RetVal("json")
|
||||
}),
|
||||
@ -203,24 +120,7 @@ let MemoryActor = protocol.ActorClass({
|
||||
* @param AllocationsRecordingOptions options
|
||||
* See the protocol.js definition of AllocationsRecordingOptions above.
|
||||
*/
|
||||
startRecordingAllocations: method(expectState("attached", function(options = {}) {
|
||||
if (this.dbg.memory.trackingAllocationSites) {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
this._frameCache.initFrames();
|
||||
|
||||
this.dbg.memory.allocationSamplingProbability = options.probability != null
|
||||
? options.probability
|
||||
: 1.0;
|
||||
if (options.maxLogLength != null) {
|
||||
this.dbg.memory.maxAllocationsLogLength = options.maxLogLength;
|
||||
}
|
||||
this.dbg.memory.trackingAllocationSites = true;
|
||||
|
||||
return Date.now();
|
||||
},
|
||||
`starting recording allocations`), {
|
||||
startRecordingAllocations: linkBridge("startRecordingAllocations", {
|
||||
request: {
|
||||
options: Arg(0, "nullable:AllocationsRecordingOptions")
|
||||
},
|
||||
@ -233,13 +133,7 @@ let MemoryActor = protocol.ActorClass({
|
||||
/**
|
||||
* Stop recording allocation sites.
|
||||
*/
|
||||
stopRecordingAllocations: method(expectState("attached", function() {
|
||||
this.dbg.memory.trackingAllocationSites = false;
|
||||
this._clearFrames();
|
||||
|
||||
return Date.now();
|
||||
},
|
||||
`stopping recording allocations`), {
|
||||
stopRecordingAllocations: linkBridge("stopRecordingAllocations", {
|
||||
request: {},
|
||||
response: {
|
||||
// Accept `nullable` in the case of server Gecko <= 37, handled on the front
|
||||
@ -251,115 +145,14 @@ let MemoryActor = protocol.ActorClass({
|
||||
* Return settings used in `startRecordingAllocations` for `probability`
|
||||
* and `maxLogLength`. Currently only uses in tests.
|
||||
*/
|
||||
getAllocationsSettings: method(expectState("attached", function() {
|
||||
return {
|
||||
maxLogLength: this.dbg.memory.maxAllocationsLogLength,
|
||||
probability: this.dbg.memory.allocationSamplingProbability
|
||||
};
|
||||
},
|
||||
`getting allocations settings`), {
|
||||
getAllocationsSettings: linkBridge("getAllocationsSettings", {
|
||||
request: {},
|
||||
response: {
|
||||
options: RetVal(0, "json")
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get a list of the most recent allocations since the last time we got
|
||||
* allocations, as well as a summary of all allocations since we've been
|
||||
* recording.
|
||||
*
|
||||
* @returns Object
|
||||
* An object of the form:
|
||||
*
|
||||
* {
|
||||
* allocations: [<index into "frames" below>, ...],
|
||||
* allocationsTimestamps: [
|
||||
* <timestamp for allocations[0]>,
|
||||
* <timestamp for allocations[1]>,
|
||||
* ...
|
||||
* ],
|
||||
* frames: [
|
||||
* {
|
||||
* line: <line number for this frame>,
|
||||
* column: <column number for this frame>,
|
||||
* source: <filename string for this frame>,
|
||||
* functionDisplayName: <this frame's inferred function name function or null>,
|
||||
* parent: <index into "frames">
|
||||
* },
|
||||
* ...
|
||||
* ],
|
||||
* counts: [
|
||||
* <number of allocations in frames[0]>,
|
||||
* <number of allocations in frames[1]>,
|
||||
* <number of allocations in frames[2]>,
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* The timestamps' unit is microseconds since the epoch.
|
||||
*
|
||||
* Subsequent `getAllocations` request within the same recording and
|
||||
* tab navigation will always place the same stack frames at the same
|
||||
* indices as previous `getAllocations` requests in the same
|
||||
* recording. In other words, it is safe to use the index as a
|
||||
* unique, persistent id for its frame.
|
||||
*
|
||||
* Additionally, the root node (null) is always at index 0.
|
||||
*
|
||||
* Note that the allocation counts include "self" allocations only,
|
||||
* and don't account for allocations in child frames.
|
||||
*
|
||||
* We use the indices into the "frames" array to avoid repeating the
|
||||
* description of duplicate stack frames both when listing
|
||||
* allocations, and when many stacks share the same tail of older
|
||||
* frames. There shouldn't be any duplicates in the "frames" array,
|
||||
* as that would defeat the purpose of this compression trick.
|
||||
*
|
||||
* In the future, we might want to split out a frame's "source" and
|
||||
* "functionDisplayName" properties out the same way we have split
|
||||
* frames out with the "frames" array. While this would further
|
||||
* compress the size of the response packet, it would increase CPU
|
||||
* usage to build the packet, and it should, of course, be guided by
|
||||
* profiling and done only when necessary.
|
||||
*/
|
||||
getAllocations: method(expectState("attached", function() {
|
||||
if (this.dbg.memory.allocationsLogOverflowed) {
|
||||
// Since the last time we drained the allocations log, there have been
|
||||
// more allocations than the log's capacity, and we lost some data. There
|
||||
// isn't anything actionable we can do about this, but put a message in
|
||||
// the browser console so we at least know that it occurred.
|
||||
reportException("MemoryActor.prototype.getAllocations",
|
||||
"Warning: allocations log overflowed and lost some data.");
|
||||
}
|
||||
|
||||
const allocations = this.dbg.memory.drainAllocationsLog()
|
||||
const packet = {
|
||||
allocations: [],
|
||||
allocationsTimestamps: []
|
||||
};
|
||||
|
||||
for (let { frame: stack, timestamp } of allocations) {
|
||||
if (stack && Cu.isDeadWrapper(stack)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Safe because SavedFrames are frozen/immutable.
|
||||
let waived = Cu.waiveXrays(stack);
|
||||
|
||||
// Ensure that we have a form, count, and index for new allocations
|
||||
// because we potentially haven't seen some or all of them yet. After this
|
||||
// loop, we can rely on the fact that every frame we deal with already has
|
||||
// its metadata stored.
|
||||
let index = this._frameCache.addFrame(waived);
|
||||
|
||||
packet.allocations.push(index);
|
||||
packet.allocationsTimestamps.push(timestamp);
|
||||
}
|
||||
|
||||
return this._frameCache.updateFramePacket(packet);
|
||||
},
|
||||
`getting allocations`), {
|
||||
getAllocations: linkBridge("getAllocations", {
|
||||
request: {},
|
||||
response: RetVal("json")
|
||||
}),
|
||||
@ -367,11 +160,7 @@ let MemoryActor = protocol.ActorClass({
|
||||
/*
|
||||
* Force a browser-wide GC.
|
||||
*/
|
||||
forceGarbageCollection: method(function() {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
Cu.forceGC();
|
||||
}
|
||||
}, {
|
||||
forceGarbageCollection: linkBridge("forceGarbageCollection", {
|
||||
request: {},
|
||||
response: {}
|
||||
}),
|
||||
@ -381,9 +170,7 @@ let MemoryActor = protocol.ActorClass({
|
||||
* collection, see
|
||||
* https://developer.mozilla.org/en-US/docs/Interfacing_with_the_XPCOM_cycle_collector#What_the_cycle_collector_does
|
||||
*/
|
||||
forceCycleCollection: method(function() {
|
||||
Cu.forceCC();
|
||||
}, {
|
||||
forceCycleCollection: linkBridge("forceCycleCollection", {
|
||||
request: {},
|
||||
response: {}
|
||||
}),
|
||||
@ -394,47 +181,23 @@ let MemoryActor = protocol.ActorClass({
|
||||
*
|
||||
* @returns object
|
||||
*/
|
||||
measure: method(function() {
|
||||
let result = {};
|
||||
|
||||
let jsObjectsSize = {};
|
||||
let jsStringsSize = {};
|
||||
let jsOtherSize = {};
|
||||
let domSize = {};
|
||||
let styleSize = {};
|
||||
let otherSize = {};
|
||||
let totalSize = {};
|
||||
let jsMilliseconds = {};
|
||||
let nonJSMilliseconds = {};
|
||||
|
||||
try {
|
||||
this._mgr.sizeOfTab(this.parent.window, jsObjectsSize, jsStringsSize, jsOtherSize,
|
||||
domSize, styleSize, otherSize, totalSize, jsMilliseconds, nonJSMilliseconds);
|
||||
result.total = totalSize.value;
|
||||
result.domSize = domSize.value;
|
||||
result.styleSize = styleSize.value;
|
||||
result.jsObjectsSize = jsObjectsSize.value;
|
||||
result.jsStringsSize = jsStringsSize.value;
|
||||
result.jsOtherSize = jsOtherSize.value;
|
||||
result.otherSize = otherSize.value;
|
||||
result.jsMilliseconds = jsMilliseconds.value.toFixed(1);
|
||||
result.nonJSMilliseconds = nonJSMilliseconds.value.toFixed(1);
|
||||
} catch (e) {
|
||||
reportException("MemoryActor.prototype.measure", e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {
|
||||
measure: linkBridge("measure", {
|
||||
request: {},
|
||||
response: RetVal("json"),
|
||||
}),
|
||||
|
||||
residentUnique: method(function() {
|
||||
return this._mgr.residentUnique;
|
||||
}, {
|
||||
residentUnique: linkBridge("residentUnique", {
|
||||
request: {},
|
||||
response: { value: RetVal("number") }
|
||||
})
|
||||
}),
|
||||
|
||||
/**
|
||||
* Called when the underlying MemoryBridge fires a "garbage-collection" events.
|
||||
* Propagates over RDP.
|
||||
*/
|
||||
_onGarbageCollection: function (data) {
|
||||
events.emit(this, "garbage-collection", data);
|
||||
},
|
||||
});
|
||||
|
||||
exports.MemoryActor = MemoryActor;
|
||||
|
@ -24,8 +24,9 @@ const protocol = require("devtools/server/protocol");
|
||||
const {method, Arg, RetVal, Option} = protocol;
|
||||
const events = require("sdk/event/core");
|
||||
const {setTimeout, clearTimeout} = require("sdk/timers");
|
||||
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
|
||||
const {MemoryActor} = require("devtools/server/actors/memory");
|
||||
const {MemoryBridge} = require("devtools/server/actors/utils/memory-bridge");
|
||||
const {FramerateActor} = require("devtools/server/actors/framerate");
|
||||
const {StackFrameCache} = require("devtools/server/actors/utils/stack");
|
||||
|
||||
@ -109,9 +110,11 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
|
||||
this._isRecording = false;
|
||||
this._stackFrames = null;
|
||||
this._memoryBridge = null;
|
||||
|
||||
// Make sure to get markers from new windows as they become available
|
||||
this._onWindowReady = this._onWindowReady.bind(this);
|
||||
this._onGarbageCollection = this._onGarbageCollection.bind(this);
|
||||
events.on(this.tabActor, "window-ready", this._onWindowReady);
|
||||
},
|
||||
|
||||
@ -132,6 +135,7 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
|
||||
events.off(this.tabActor, "window-ready", this._onWindowReady);
|
||||
this.tabActor = null;
|
||||
this._memoryBridge = null;
|
||||
|
||||
protocol.Actor.prototype.destroy.call(this);
|
||||
},
|
||||
@ -173,10 +177,7 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
* markers, memory, tick and frames events, if any.
|
||||
*/
|
||||
_pullTimelineData: function() {
|
||||
if (!this._isRecording) {
|
||||
return;
|
||||
}
|
||||
if (!this.docShells.length) {
|
||||
if (!this._isRecording || !this.docShells.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -209,10 +210,10 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
if (markers.length > 0) {
|
||||
events.emit(this, "markers", markers, endTime);
|
||||
}
|
||||
if (this._memoryActor) {
|
||||
events.emit(this, "memory", endTime, this._memoryActor.measure());
|
||||
if (this._withMemory) {
|
||||
events.emit(this, "memory", endTime, this._memoryBridge.measure());
|
||||
}
|
||||
if (this._framerateActor) {
|
||||
if (this._withTicks) {
|
||||
events.emit(this, "ticks", endTime, this._framerateActor.getPendingTicks());
|
||||
}
|
||||
|
||||
@ -235,9 +236,20 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
|
||||
/**
|
||||
* Start recording profile markers.
|
||||
*
|
||||
* @option {boolean} withMemory
|
||||
* Boolean indiciating whether we want memory measurements sampled. A memory actor
|
||||
* will be created regardless (to hook into GC events), but this determines
|
||||
* whether or not a `memory` event gets fired.
|
||||
* @option {boolean} withTicks
|
||||
* Boolean indicating whether a `ticks` event is fired and a FramerateActor
|
||||
* is created.
|
||||
*/
|
||||
start: method(function({ withMemory, withTicks }) {
|
||||
var startTime = this.docShells[0].now();
|
||||
start: method(Task.async(function *({ withMemory, withTicks }) {
|
||||
var startTime = this._startTime = this.docShells[0].now();
|
||||
// Store the start time from unix epoch so we can normalize
|
||||
// markers from the memory actor
|
||||
this._unixStartTime = Date.now();
|
||||
|
||||
if (this._isRecording) {
|
||||
return startTime;
|
||||
@ -246,14 +258,16 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
this._isRecording = true;
|
||||
this._stackFrames = new StackFrameCache();
|
||||
this._stackFrames.initFrames();
|
||||
this._withMemory = withMemory;
|
||||
this._withTicks = withTicks;
|
||||
|
||||
for (let docShell of this.docShells) {
|
||||
docShell.recordProfileTimelineMarkers = true;
|
||||
}
|
||||
|
||||
if (withMemory) {
|
||||
this._memoryActor = new MemoryActor(this.conn, this.tabActor, this._stackFrames);
|
||||
}
|
||||
this._memoryBridge = new MemoryBridge(this.tabActor, this._stackFrames);
|
||||
this._memoryBridge.attach();
|
||||
events.on(this._memoryBridge, "garbage-collection", this._onGarbageCollection);
|
||||
|
||||
if (withTicks) {
|
||||
this._framerateActor = new FramerateActor(this.conn, this.tabActor);
|
||||
@ -262,7 +276,7 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
|
||||
this._pullTimelineData();
|
||||
return startTime;
|
||||
}, {
|
||||
}), {
|
||||
request: {
|
||||
withMemory: Option(0, "boolean"),
|
||||
withTicks: Option(0, "boolean")
|
||||
@ -275,16 +289,15 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
/**
|
||||
* Stop recording profile markers.
|
||||
*/
|
||||
stop: method(function() {
|
||||
stop: method(Task.async(function *() {
|
||||
if (!this._isRecording) {
|
||||
return;
|
||||
}
|
||||
this._isRecording = false;
|
||||
this._stackFrames = null;
|
||||
|
||||
if (this._memoryActor) {
|
||||
this._memoryActor = null;
|
||||
}
|
||||
events.off(this._memoryBridge, "garbage-collection", this._onGarbageCollection);
|
||||
this._memoryBridge.detach();
|
||||
|
||||
if (this._framerateActor) {
|
||||
this._framerateActor.stopRecording();
|
||||
@ -297,7 +310,7 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
|
||||
clearTimeout(this._dataPullTimeout);
|
||||
return this.docShells[0].now();
|
||||
}, {
|
||||
}), {
|
||||
response: {
|
||||
// Set as possibly nullable due to the end time possibly being
|
||||
// undefined during destruction
|
||||
@ -316,7 +329,38 @@ let TimelineActor = exports.TimelineActor = protocol.ActorClass({
|
||||
.QueryInterface(Ci.nsIDocShell);
|
||||
docShell.recordProfileTimelineMarkers = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fired when the MemoryActor emits a `garbage-collection` event. Used to
|
||||
* emit the data to the front end and in similar format to other markers.
|
||||
*
|
||||
* A GC "marker" here represents a full GC cycle, which may contain several incremental
|
||||
* events within its `collection` array. The marker contains a `reason` field, indicating
|
||||
* why there was a GC, and may contain a `nonincrementalReason` when SpiderMonkey could
|
||||
* not incrementally collect garbage.
|
||||
*/
|
||||
_onGarbageCollection: function ({ collections, reason, nonincrementalReason }) {
|
||||
if (!this._isRecording || !this.docShells.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize the start time to docshell start time, and convert it
|
||||
// to microseconds.
|
||||
let startTime = (this._unixStartTime - this._startTime) * 1000;
|
||||
let endTime = this.docShells[0].now();
|
||||
|
||||
events.emit(this, "markers", collections.map(({ startTimestamp: start, endTimestamp: end }) => {
|
||||
return {
|
||||
name: "GarbageCollection",
|
||||
causeName: reason,
|
||||
nonincrementalReason: nonincrementalReason,
|
||||
// Both timestamps are in microseconds -- convert to milliseconds to match other markers
|
||||
start: (start - startTime) / 1000,
|
||||
end: (end - startTime) / 1000
|
||||
};
|
||||
}), endTime);
|
||||
},
|
||||
});
|
||||
|
||||
exports.TimelineFront = protocol.FrontClass(TimelineActor, {
|
||||
|
367
toolkit/devtools/server/actors/utils/memory-bridge.js
Normal file
367
toolkit/devtools/server/actors/utils/memory-bridge.js
Normal file
@ -0,0 +1,367 @@
|
||||
/* 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";
|
||||
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { reportException } = require("devtools/toolkit/DevToolsUtils");
|
||||
const { Class } = require("sdk/core/heritage");
|
||||
loader.lazyRequireGetter(this, "events", "sdk/event/core");
|
||||
loader.lazyRequireGetter(this, "EventTarget", "sdk/event/target", true);
|
||||
loader.lazyRequireGetter(this, "StackFrameCache",
|
||||
"devtools/server/actors/utils/stack", true);
|
||||
|
||||
/**
|
||||
* A method decorator that ensures the actor is in the expected state before
|
||||
* proceeding. If the actor is not in the expected state, the decorated method
|
||||
* returns a rejected promise.
|
||||
*
|
||||
* @param String expectedState
|
||||
* The expected state.
|
||||
* @param String activity
|
||||
* Additional info about what's going on.
|
||||
* @param Function method
|
||||
* The actor method to proceed with when the actor is in the expected
|
||||
* state.
|
||||
*
|
||||
* @returns Function
|
||||
* The decorated method.
|
||||
*/
|
||||
function expectState(expectedState, method, activity) {
|
||||
return function(...args) {
|
||||
if (this.state !== expectedState) {
|
||||
const msg = `Wrong state while ${activity}:` +
|
||||
`Expected '${expectedState}',` +
|
||||
`but current state is '${this.state}'.`;
|
||||
return Promise.reject(new Error(msg));
|
||||
}
|
||||
|
||||
return method.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that returns memory data for a parent actor's window.
|
||||
* Using a tab-scoped actor with this instance will measure the memory footprint of its
|
||||
* parent tab. Using a global-scoped actor instance however, will measure the memory
|
||||
* footprint of the chrome window referenced by its root actor.
|
||||
*
|
||||
* To be consumed by actor's, like MemoryActor using MemoryBridge to
|
||||
* send information over RDP, and TimelineActor for using more light-weight
|
||||
* utilities like GC events and measuring memory consumption.
|
||||
*/
|
||||
let MemoryBridge = Class({
|
||||
extends: EventTarget,
|
||||
|
||||
/**
|
||||
* Requires a root actor and a StackFrameCache.
|
||||
*/
|
||||
initialize: function (parent, frameCache = new StackFrameCache()) {
|
||||
this.parent = parent;
|
||||
this._mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
|
||||
.getService(Ci.nsIMemoryReporterManager);
|
||||
this.state = "detached";
|
||||
this._dbg = null;
|
||||
this._frameCache = frameCache;
|
||||
|
||||
this._onGarbageCollection = this._onGarbageCollection.bind(this);
|
||||
this._onWindowReady = this._onWindowReady.bind(this);
|
||||
|
||||
events.on(this.parent, "window-ready", this._onWindowReady);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
events.off(this.parent, "window-ready", this._onWindowReady);
|
||||
|
||||
this._mgr = null;
|
||||
if (this.state === "attached") {
|
||||
this.detach();
|
||||
}
|
||||
},
|
||||
|
||||
get dbg() {
|
||||
if (!this._dbg) {
|
||||
this._dbg = this.parent.makeDebugger();
|
||||
}
|
||||
return this._dbg;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Attach to this MemoryBridge.
|
||||
*
|
||||
* This attaches the MemoryBridge's Debugger instance so that you can start
|
||||
* recording allocations or take a census of the heap. In addition, the
|
||||
* MemoryBridge will start emitting GC events.
|
||||
*/
|
||||
attach: expectState("detached", function() {
|
||||
this.dbg.addDebuggees();
|
||||
this.dbg.memory.onGarbageCollection = this._onGarbageCollection.bind(this);
|
||||
this.state = "attached";
|
||||
}, `attaching to the debugger`),
|
||||
|
||||
/**
|
||||
* Detach from this MemoryBridge.
|
||||
*/
|
||||
detach: expectState("attached", function() {
|
||||
this._clearDebuggees();
|
||||
this.dbg.enabled = false;
|
||||
this._dbg = null;
|
||||
this.state = "detached";
|
||||
}, `detaching from the debugger`),
|
||||
|
||||
/**
|
||||
* Gets the current MemoryBridge attach/detach state.
|
||||
*/
|
||||
getState: function () {
|
||||
return this.state;
|
||||
},
|
||||
|
||||
_clearDebuggees: function() {
|
||||
if (this._dbg) {
|
||||
if (this.dbg.memory.trackingAllocationSites) {
|
||||
this.dbg.memory.drainAllocationsLog();
|
||||
}
|
||||
this._clearFrames();
|
||||
this.dbg.removeAllDebuggees();
|
||||
}
|
||||
},
|
||||
|
||||
_clearFrames: function() {
|
||||
if (this.dbg.memory.trackingAllocationSites) {
|
||||
this._frameCache.clearFrames();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler for the parent actor's "window-ready" event.
|
||||
*/
|
||||
_onWindowReady: function({ isTopLevel }) {
|
||||
if (this.state == "attached") {
|
||||
if (isTopLevel && this.dbg.memory.trackingAllocationSites) {
|
||||
this._clearDebuggees();
|
||||
this._frameCache.initFrames();
|
||||
}
|
||||
this.dbg.addDebuggees();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler for GC events on the Debugger.Memory instance.
|
||||
*/
|
||||
_onGarbageCollection: function (data) {
|
||||
events.emit(this, "garbage-collection", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Take a census of the heap. See js/src/doc/Debugger/Debugger.Memory.md for
|
||||
* more information.
|
||||
*/
|
||||
takeCensus: expectState("attached", function() {
|
||||
return this.dbg.memory.takeCensus();
|
||||
}, `taking census`),
|
||||
|
||||
/**
|
||||
* Start recording allocation sites.
|
||||
*
|
||||
* @param AllocationsRecordingOptions options
|
||||
* See the protocol.js definition of AllocationsRecordingOptions above.
|
||||
*/
|
||||
startRecordingAllocations: expectState("attached", function(options = {}) {
|
||||
if (this.dbg.memory.trackingAllocationSites) {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
this._frameCache.initFrames();
|
||||
|
||||
this.dbg.memory.allocationSamplingProbability = options.probability != null
|
||||
? options.probability
|
||||
: 1.0;
|
||||
if (options.maxLogLength != null) {
|
||||
this.dbg.memory.maxAllocationsLogLength = options.maxLogLength;
|
||||
}
|
||||
this.dbg.memory.trackingAllocationSites = true;
|
||||
|
||||
return Date.now();
|
||||
}, `starting recording allocations`),
|
||||
|
||||
/**
|
||||
* Stop recording allocation sites.
|
||||
*/
|
||||
stopRecordingAllocations: expectState("attached", function() {
|
||||
this.dbg.memory.trackingAllocationSites = false;
|
||||
this._clearFrames();
|
||||
|
||||
return Date.now();
|
||||
}, `stopping recording allocations`),
|
||||
|
||||
/**
|
||||
* Return settings used in `startRecordingAllocations` for `probability`
|
||||
* and `maxLogLength`. Currently only uses in tests.
|
||||
*/
|
||||
getAllocationsSettings: expectState("attached", function() {
|
||||
return {
|
||||
maxLogLength: this.dbg.memory.maxAllocationsLogLength,
|
||||
probability: this.dbg.memory.allocationSamplingProbability
|
||||
};
|
||||
}, `getting allocations settings`),
|
||||
|
||||
/**
|
||||
* Get a list of the most recent allocations since the last time we got
|
||||
* allocations, as well as a summary of all allocations since we've been
|
||||
* recording.
|
||||
*
|
||||
* @returns Object
|
||||
* An object of the form:
|
||||
*
|
||||
* {
|
||||
* allocations: [<index into "frames" below>, ...],
|
||||
* allocationsTimestamps: [
|
||||
* <timestamp for allocations[0]>,
|
||||
* <timestamp for allocations[1]>,
|
||||
* ...
|
||||
* ],
|
||||
* frames: [
|
||||
* {
|
||||
* line: <line number for this frame>,
|
||||
* column: <column number for this frame>,
|
||||
* source: <filename string for this frame>,
|
||||
* functionDisplayName: <this frame's inferred function name function or null>,
|
||||
* parent: <index into "frames">
|
||||
* },
|
||||
* ...
|
||||
* ],
|
||||
* counts: [
|
||||
* <number of allocations in frames[0]>,
|
||||
* <number of allocations in frames[1]>,
|
||||
* <number of allocations in frames[2]>,
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* The timestamps' unit is microseconds since the epoch.
|
||||
*
|
||||
* Subsequent `getAllocations` request within the same recording and
|
||||
* tab navigation will always place the same stack frames at the same
|
||||
* indices as previous `getAllocations` requests in the same
|
||||
* recording. In other words, it is safe to use the index as a
|
||||
* unique, persistent id for its frame.
|
||||
*
|
||||
* Additionally, the root node (null) is always at index 0.
|
||||
*
|
||||
* Note that the allocation counts include "self" allocations only,
|
||||
* and don't account for allocations in child frames.
|
||||
*
|
||||
* We use the indices into the "frames" array to avoid repeating the
|
||||
* description of duplicate stack frames both when listing
|
||||
* allocations, and when many stacks share the same tail of older
|
||||
* frames. There shouldn't be any duplicates in the "frames" array,
|
||||
* as that would defeat the purpose of this compression trick.
|
||||
*
|
||||
* In the future, we might want to split out a frame's "source" and
|
||||
* "functionDisplayName" properties out the same way we have split
|
||||
* frames out with the "frames" array. While this would further
|
||||
* compress the size of the response packet, it would increase CPU
|
||||
* usage to build the packet, and it should, of course, be guided by
|
||||
* profiling and done only when necessary.
|
||||
*/
|
||||
getAllocations: expectState("attached", function() {
|
||||
if (this.dbg.memory.allocationsLogOverflowed) {
|
||||
// Since the last time we drained the allocations log, there have been
|
||||
// more allocations than the log's capacity, and we lost some data. There
|
||||
// isn't anything actionable we can do about this, but put a message in
|
||||
// the browser console so we at least know that it occurred.
|
||||
reportException("MemoryBridge.prototype.getAllocations",
|
||||
"Warning: allocations log overflowed and lost some data.");
|
||||
}
|
||||
|
||||
const allocations = this.dbg.memory.drainAllocationsLog()
|
||||
const packet = {
|
||||
allocations: [],
|
||||
allocationsTimestamps: []
|
||||
};
|
||||
|
||||
for (let { frame: stack, timestamp } of allocations) {
|
||||
if (stack && Cu.isDeadWrapper(stack)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Safe because SavedFrames are frozen/immutable.
|
||||
let waived = Cu.waiveXrays(stack);
|
||||
|
||||
// Ensure that we have a form, count, and index for new allocations
|
||||
// because we potentially haven't seen some or all of them yet. After this
|
||||
// loop, we can rely on the fact that every frame we deal with already has
|
||||
// its metadata stored.
|
||||
let index = this._frameCache.addFrame(waived);
|
||||
|
||||
packet.allocations.push(index);
|
||||
packet.allocationsTimestamps.push(timestamp);
|
||||
}
|
||||
|
||||
return this._frameCache.updateFramePacket(packet);
|
||||
}, `getting allocations`),
|
||||
|
||||
/*
|
||||
* Force a browser-wide GC.
|
||||
*/
|
||||
forceGarbageCollection: function () {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
Cu.forceGC();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Force an XPCOM cycle collection. For more information on XPCOM cycle
|
||||
* collection, see
|
||||
* https://developer.mozilla.org/en-US/docs/Interfacing_with_the_XPCOM_cycle_collector#What_the_cycle_collector_does
|
||||
*/
|
||||
forceCycleCollection: function () {
|
||||
Cu.forceCC();
|
||||
},
|
||||
|
||||
/**
|
||||
* A method that returns a detailed breakdown of the memory consumption of the
|
||||
* associated window.
|
||||
*
|
||||
* @returns object
|
||||
*/
|
||||
measure: function () {
|
||||
let result = {};
|
||||
|
||||
let jsObjectsSize = {};
|
||||
let jsStringsSize = {};
|
||||
let jsOtherSize = {};
|
||||
let domSize = {};
|
||||
let styleSize = {};
|
||||
let otherSize = {};
|
||||
let totalSize = {};
|
||||
let jsMilliseconds = {};
|
||||
let nonJSMilliseconds = {};
|
||||
|
||||
try {
|
||||
this._mgr.sizeOfTab(this.parent.window, jsObjectsSize, jsStringsSize, jsOtherSize,
|
||||
domSize, styleSize, otherSize, totalSize, jsMilliseconds, nonJSMilliseconds);
|
||||
result.total = totalSize.value;
|
||||
result.domSize = domSize.value;
|
||||
result.styleSize = styleSize.value;
|
||||
result.jsObjectsSize = jsObjectsSize.value;
|
||||
result.jsStringsSize = jsStringsSize.value;
|
||||
result.jsOtherSize = jsOtherSize.value;
|
||||
result.otherSize = otherSize.value;
|
||||
result.jsMilliseconds = jsMilliseconds.value.toFixed(1);
|
||||
result.nonJSMilliseconds = nonJSMilliseconds.value.toFixed(1);
|
||||
} catch (e) {
|
||||
reportException("MemoryBridge.prototype.measure", e);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
residentUnique: function () {
|
||||
return this._mgr.residentUnique;
|
||||
}
|
||||
});
|
||||
|
||||
exports.MemoryBridge = MemoryBridge;
|
@ -79,6 +79,7 @@ EXTRA_JS_MODULES.devtools.server.actors.utils += [
|
||||
'actors/utils/automation-timeline.js',
|
||||
'actors/utils/make-debugger.js',
|
||||
'actors/utils/map-uri-to-addon-id.js',
|
||||
'actors/utils/memory-bridge.js',
|
||||
'actors/utils/ScriptStore.js',
|
||||
'actors/utils/stack.js',
|
||||
'actors/utils/TabSources.js'
|
||||
|
Loading…
Reference in New Issue
Block a user