# HG changeset patch # User Jason Laster # Date 1517561820 -7200 # Node ID 2fdf70b7c76829533719e8d1136a0df88f110b81 # Parent c52ba88c0c62fc46bc4f86e496185453548c49a8 Bug 1435187 - Refactor the script actor. r=jdescottes - Extract paused scoped objects. - Extract event loop stack. r=jdescottes - Extract actor stores. r=jdescottes - Move script.js to actor.js. r=jdescottes diff --git a/devtools/server/actors/addon.js b/devtools/server/actors/addon.js --- a/devtools/server/actors/addon.js +++ b/devtools/server/actors/addon.js @@ -8,18 +8,18 @@ var { Ci, Cu } = require("chrome"); var Services = require("Services"); var { ActorPool } = require("devtools/server/actors/common"); var { TabSources } = require("./utils/TabSources"); var makeDebugger = require("./utils/make-debugger"); var { ConsoleAPIListener } = require("devtools/server/actors/webconsole/listeners"); var DevToolsUtils = require("devtools/shared/DevToolsUtils"); var { assert, update } = DevToolsUtils; -loader.lazyRequireGetter(this, "AddonThreadActor", "devtools/server/actors/script", true); -loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true); +loader.lazyRequireGetter(this, "AddonThreadActor", "devtools/server/actors/thread", true); +loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/thread", true); loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id"); loader.lazyRequireGetter(this, "WebConsoleActor", "devtools/server/actors/webconsole", true); loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); function BrowserAddonActor(connection, addon) { this.conn = connection; this._addon = addon; diff --git a/devtools/server/actors/child-process.js b/devtools/server/actors/child-process.js --- a/devtools/server/actors/child-process.js +++ b/devtools/server/actors/child-process.js @@ -2,17 +2,17 @@ * 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 Services = require("Services"); -const { ChromeDebuggerActor } = require("devtools/server/actors/script"); +const { ChromeDebuggerActor } = require("devtools/server/actors/thread"); const { WebConsoleActor } = require("devtools/server/actors/webconsole"); const makeDebugger = require("devtools/server/actors/utils/make-debugger"); const { ActorPool } = require("devtools/server/main"); const { assert } = require("devtools/shared/DevToolsUtils"); const { TabSources } = require("./utils/TabSources"); loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker-list", true); diff --git a/devtools/server/actors/moz.build b/devtools/server/actors/moz.build --- a/devtools/server/actors/moz.build +++ b/devtools/server/actors/moz.build @@ -35,32 +35,33 @@ DevToolsModules( 'framerate.js', 'gcli.js', 'heap-snapshot-file.js', 'highlighters.css', 'highlighters.js', 'layout.js', 'memory.js', 'object.js', + 'pause-scoped.js', 'perf.js', 'performance-recording.js', 'performance.js', 'preference.js', 'pretty-print-worker.js', 'process.js', 'promises.js', 'reflow.js', 'root.js', - 'script.js', 'source.js', 'storage.js', 'string.js', 'styles.js', 'stylesheets.js', 'tab.js', + 'thread.js', 'timeline.js', 'webaudio.js', 'webbrowser.js', 'webconsole.js', 'webextension-inspected-window.js', 'webextension-parent.js', 'webextension.js', 'webgl.js', diff --git a/devtools/server/actors/pause-scoped.js b/devtools/server/actors/pause-scoped.js new file mode 100644 --- /dev/null +++ b/devtools/server/actors/pause-scoped.js @@ -0,0 +1,128 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 { ObjectActor } = require("devtools/server/actors/object"); + +/** + * A base actor for any actors that should only respond receive messages in the + * paused state. Subclasses may expose a `threadActor` which is used to help + * determine when we are in a paused state. Subclasses should set their own + * "constructor" property if they want better error messages. You should never + * instantiate a PauseScopedActor directly, only through subclasses. + */ +function PauseScopedActor() { +} + +/** + * A function decorator for creating methods to handle protocol messages that + * should only be received while in the paused state. + * + * @param method Function + * The function we are decorating. + */ +PauseScopedActor.withPaused = function (method) { + return function () { + if (this.isPaused()) { + return method.apply(this, arguments); + } + return this._wrongState(); + }; +}; + +PauseScopedActor.prototype = { + + /** + * Returns true if we are in the paused state. + */ + isPaused: function () { + // When there is not a ThreadActor available (like in the webconsole) we + // have to be optimistic and assume that we are paused so that we can + // respond to requests. + return this.threadActor ? this.threadActor.state === "paused" : true; + }, + + /** + * Returns the wrongState response packet for this actor. + */ + _wrongState: function () { + return { + error: "wrongState", + message: this.constructor.name + + " actors can only be accessed while the thread is paused." + }; + } +}; + +/** + * Creates a pause-scoped actor for the specified object. + * @see ObjectActor + */ +function PauseScopedObjectActor(obj, hooks) { + ObjectActor.call(this, obj, hooks); + this.hooks.promote = hooks.promote; + this.hooks.isThreadLifetimePool = hooks.isThreadLifetimePool; +} + +PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype); + +Object.assign(PauseScopedObjectActor.prototype, ObjectActor.prototype); + +Object.assign(PauseScopedObjectActor.prototype, { + constructor: PauseScopedObjectActor, + actorPrefix: "pausedobj", + + onOwnPropertyNames: + PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames), + + onPrototypeAndProperties: + PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties), + + onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype), + onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty), + onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile), + + onDisplayString: + PauseScopedActor.withPaused(ObjectActor.prototype.onDisplayString), + + onParameterNames: + PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames), + + /** + * Handle a protocol request to promote a pause-lifetime grip to a + * thread-lifetime grip. + * + * @param request object + * The protocol request object. + */ + onThreadGrip: PauseScopedActor.withPaused(function (request) { + this.hooks.promote(); + return {}; + }), + + /** + * Handle a protocol request to release a thread-lifetime grip. + * + * @param request object + * The protocol request object. + */ + onRelease: PauseScopedActor.withPaused(function (request) { + if (this.hooks.isThreadLifetimePool()) { + return { error: "notReleasable", + message: "Only thread-lifetime actors can be released." }; + } + + this.release(); + return {}; + }), +}); + +Object.assign(PauseScopedObjectActor.prototype.requestTypes, { + "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip, +}); + +exports.PauseScopedObjectActor = PauseScopedObjectActor; diff --git a/devtools/server/actors/tab.js b/devtools/server/actors/tab.js --- a/devtools/server/actors/tab.js +++ b/devtools/server/actors/tab.js @@ -22,18 +22,18 @@ var { DebuggerServer } = require("devtoo var DevToolsUtils = require("devtools/shared/DevToolsUtils"); var { assert } = DevToolsUtils; var { TabSources } = require("./utils/TabSources"); var makeDebugger = require("./utils/make-debugger"); const EventEmitter = require("devtools/shared/event-emitter"); const EXTENSION_CONTENT_JSM = "resource://gre/modules/ExtensionContent.jsm"; -loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true); -loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true); +loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/thread", true); +loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/thread", true); loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker-list", true); loader.lazyImporter(this, "ExtensionContent", EXTENSION_CONTENT_JSM); loader.lazyRequireGetter(this, "StyleSheetActor", "devtools/server/actors/stylesheets", true); function getWindowID(window) { return window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) diff --git a/devtools/server/actors/script.js b/devtools/server/actors/thread.js rename from devtools/server/actors/script.js rename to devtools/server/actors/thread.js --- a/devtools/server/actors/script.js +++ b/devtools/server/actors/thread.js @@ -3,401 +3,45 @@ /* 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 Services = require("Services"); const { Cc, Ci, Cr } = require("chrome"); -const { ActorPool, OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common"); -const { ObjectActor, createValueGrip, longStringGrip } = require("devtools/server/actors/object"); +const { ActorPool, GeneratedLocation } = require("devtools/server/actors/common"); +const { createValueGrip, longStringGrip } = require("devtools/server/actors/object"); const { ActorClassWithSpec } = require("devtools/shared/protocol"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const flags = require("devtools/shared/flags"); const { assert, dumpn } = DevToolsUtils; const promise = require("promise"); -const xpcInspector = require("xpcInspector"); const { DevToolsWorker } = require("devtools/shared/worker/worker"); const { threadSpec } = require("devtools/shared/specs/script"); const { resolve, reject, all } = promise; loader.lazyGetter(this, "Debugger", () => { let Debugger = require("Debugger"); hackDebugger(Debugger); return Debugger; }); -loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true); loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true); -loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id"); loader.lazyRequireGetter(this, "BreakpointActor", "devtools/server/actors/breakpoint", true); loader.lazyRequireGetter(this, "setBreakpointAtEntryPoints", "devtools/server/actors/breakpoint", true); -loader.lazyRequireGetter(this, "getSourceURL", "devtools/server/actors/source", true); loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true); +loader.lazyRequireGetter(this, "SourceActorStore", "devtools/server/actors/utils/source-actor-store", true); +loader.lazyRequireGetter(this, "BreakpointActorMap", "devtools/server/actors/utils/breakpoint-actor-map", true); +loader.lazyRequireGetter(this, "PauseScopedObjectActor", "devtools/server/actors/pause-scoped", true); +loader.lazyRequireGetter(this, "EventLoopStack", "devtools/server/actors/utils/event-loop", true); loader.lazyRequireGetter(this, "FrameActor", "devtools/server/actors/frame", true); loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter"); /** - * A BreakpointActorMap is a map from locations to instances of BreakpointActor. - */ -function BreakpointActorMap() { - this._size = 0; - this._actors = {}; -} - -BreakpointActorMap.prototype = { - /** - * Return the number of BreakpointActors in this BreakpointActorMap. - * - * @returns Number - * The number of BreakpointActor in this BreakpointActorMap. - */ - get size() { - return this._size; - }, - - /** - * Generate all BreakpointActors that match the given location in - * this BreakpointActorMap. - * - * @param OriginalLocation location - * The location for which matching BreakpointActors should be generated. - */ - findActors: function* (location = new OriginalLocation()) { - // Fast shortcut for when we know we won't find any actors. Surprisingly - // enough, this speeds up refreshing when there are no breakpoints set by - // about 2x! - if (this.size === 0) { - return; - } - - function* findKeys(obj, key) { - if (key !== undefined) { - if (key in obj) { - yield key; - } - } else { - for (key of Object.keys(obj)) { - yield key; - } - } - } - - let query = { - sourceActorID: location.originalSourceActor - ? location.originalSourceActor.actorID - : undefined, - line: location.originalLine, - }; - - // If location contains a line, assume we are searching for a whole line - // breakpoint, and set begin/endColumn accordingly. Otherwise, we are - // searching for all breakpoints, so begin/endColumn should be left unset. - if (location.originalLine) { - query.beginColumn = location.originalColumn ? location.originalColumn : 0; - query.endColumn = location.originalColumn ? location.originalColumn + 1 : Infinity; - } else { - query.beginColumn = location.originalColumn ? query.originalColumn : undefined; - query.endColumn = location.originalColumn ? query.originalColumn + 1 : undefined; - } - - for (let sourceActorID of findKeys(this._actors, query.sourceActorID)) { - let actor = this._actors[sourceActorID]; - for (let line of findKeys(actor, query.line)) { - for (let beginColumn of findKeys(actor[line], query.beginColumn)) { - for (let endColumn of findKeys(actor[line][beginColumn], - query.endColumn)) { - yield actor[line][beginColumn][endColumn]; - } - } - } - } - }, - - /** - * Return the BreakpointActor at the given location in this - * BreakpointActorMap. - * - * @param OriginalLocation location - * The location for which the BreakpointActor should be returned. - * - * @returns BreakpointActor actor - * The BreakpointActor at the given location. - */ - getActor: function (originalLocation) { - for (let actor of this.findActors(originalLocation)) { - return actor; - } - - return null; - }, - - /** - * Set the given BreakpointActor to the given location in this - * BreakpointActorMap. - * - * @param OriginalLocation location - * The location to which the given BreakpointActor should be set. - * - * @param BreakpointActor actor - * The BreakpointActor to be set to the given location. - */ - setActor: function (location, actor) { - let { originalSourceActor, originalLine, originalColumn } = location; - - let sourceActorID = originalSourceActor.actorID; - let line = originalLine; - let beginColumn = originalColumn ? originalColumn : 0; - let endColumn = originalColumn ? originalColumn + 1 : Infinity; - - if (!this._actors[sourceActorID]) { - this._actors[sourceActorID] = []; - } - if (!this._actors[sourceActorID][line]) { - this._actors[sourceActorID][line] = []; - } - if (!this._actors[sourceActorID][line][beginColumn]) { - this._actors[sourceActorID][line][beginColumn] = []; - } - if (!this._actors[sourceActorID][line][beginColumn][endColumn]) { - ++this._size; - } - this._actors[sourceActorID][line][beginColumn][endColumn] = actor; - }, - - /** - * Delete the BreakpointActor from the given location in this - * BreakpointActorMap. - * - * @param OriginalLocation location - * The location from which the BreakpointActor should be deleted. - */ - deleteActor: function (location) { - let { originalSourceActor, originalLine, originalColumn } = location; - - let sourceActorID = originalSourceActor.actorID; - let line = originalLine; - let beginColumn = originalColumn ? originalColumn : 0; - let endColumn = originalColumn ? originalColumn + 1 : Infinity; - - if (this._actors[sourceActorID]) { - if (this._actors[sourceActorID][line]) { - if (this._actors[sourceActorID][line][beginColumn]) { - if (this._actors[sourceActorID][line][beginColumn][endColumn]) { - --this._size; - } - delete this._actors[sourceActorID][line][beginColumn][endColumn]; - if (Object.keys(this._actors[sourceActorID][line][beginColumn]).length === 0) { - delete this._actors[sourceActorID][line][beginColumn]; - } - } - if (Object.keys(this._actors[sourceActorID][line]).length === 0) { - delete this._actors[sourceActorID][line]; - } - } - } - } -}; - -exports.BreakpointActorMap = BreakpointActorMap; - -/** - * Keeps track of persistent sources across reloads and ties different - * source instances to the same actor id so that things like - * breakpoints survive reloads. ThreadSources uses this to force the - * same actorID on a SourceActor. - */ -function SourceActorStore() { - // source identifier --> actor id - this._sourceActorIds = Object.create(null); -} - -SourceActorStore.prototype = { - /** - * Lookup an existing actor id that represents this source, if available. - */ - getReusableActorId: function (source, originalUrl) { - let url = this.getUniqueKey(source, originalUrl); - if (url && url in this._sourceActorIds) { - return this._sourceActorIds[url]; - } - return null; - }, - - /** - * Update a source with an actorID. - */ - setReusableActorId: function (source, originalUrl, actorID) { - let url = this.getUniqueKey(source, originalUrl); - if (url) { - this._sourceActorIds[url] = actorID; - } - }, - - /** - * Make a unique URL from a source that identifies it across reloads. - */ - getUniqueKey: function (source, originalUrl) { - if (originalUrl) { - // Original source from a sourcemap. - return originalUrl; - } - - return getSourceURL(source); - } -}; - -exports.SourceActorStore = SourceActorStore; - -/** - * Manages pushing event loops and automatically pops and exits them in the - * correct order as they are resolved. - * - * @param ThreadActor thread - * The thread actor instance that owns this EventLoopStack. - * @param DebuggerServerConnection connection - * The remote protocol connection associated with this event loop stack. - * @param Object hooks - * An object with the following properties: - * - url: The URL string of the debuggee we are spinning an event loop - * for. - * - preNest: function called before entering a nested event loop - * - postNest: function called after exiting a nested event loop - */ -function EventLoopStack({ thread, connection, hooks }) { - this._hooks = hooks; - this._thread = thread; - this._connection = connection; -} - -EventLoopStack.prototype = { - /** - * The number of nested event loops on the stack. - */ - get size() { - return xpcInspector.eventLoopNestLevel; - }, - - /** - * The URL of the debuggee who pushed the event loop on top of the stack. - */ - get lastPausedUrl() { - let url = null; - if (this.size > 0) { - try { - url = xpcInspector.lastNestRequestor.url; - } catch (e) { - // The tab's URL getter may throw if the tab is destroyed by the time - // this code runs, but we don't really care at this point. - dumpn(e); - } - } - return url; - }, - - /** - * The DebuggerServerConnection of the debugger who pushed the event loop on - * top of the stack - */ - get lastConnection() { - return xpcInspector.lastNestRequestor._connection; - }, - - /** - * Push a new nested event loop onto the stack. - * - * @returns EventLoop - */ - push: function () { - return new EventLoop({ - thread: this._thread, - connection: this._connection, - hooks: this._hooks - }); - } -}; - -/** - * An object that represents a nested event loop. It is used as the nest - * requestor with nsIJSInspector instances. - * - * @param ThreadActor thread - * The thread actor that is creating this nested event loop. - * @param DebuggerServerConnection connection - * The remote protocol connection associated with this event loop. - * @param Object hooks - * The same hooks object passed into EventLoopStack during its - * initialization. - */ -function EventLoop({ thread, connection, hooks }) { - this._thread = thread; - this._hooks = hooks; - this._connection = connection; - - this.enter = this.enter.bind(this); - this.resolve = this.resolve.bind(this); -} - -EventLoop.prototype = { - entered: false, - resolved: false, - get url() { - return this._hooks.url; - }, - - /** - * Enter this nested event loop. - */ - enter: function () { - let nestData = this._hooks.preNest - ? this._hooks.preNest() - : null; - - this.entered = true; - xpcInspector.enterNestedEventLoop(this); - - // Keep exiting nested event loops while the last requestor is resolved. - if (xpcInspector.eventLoopNestLevel > 0) { - const { resolved } = xpcInspector.lastNestRequestor; - if (resolved) { - xpcInspector.exitNestedEventLoop(); - } - } - - if (this._hooks.postNest) { - this._hooks.postNest(nestData); - } - }, - - /** - * Resolve this nested event loop. - * - * @returns boolean - * True if we exited this nested event loop because it was on top of - * the stack, false if there is another nested event loop above this - * one that hasn't resolved yet. - */ - resolve: function () { - if (!this.entered) { - throw new Error("Can't resolve an event loop before it has been entered!"); - } - if (this.resolved) { - throw new Error("Already resolved this nested event loop!"); - } - this.resolved = true; - if (this === xpcInspector.lastNestRequestor) { - xpcInspector.exitNestedEventLoop(); - return true; - } - return false; - }, -}; - -/** * JSD2 actors. */ /** * Creates a ThreadActor. * * ThreadActors manage a JSInspector object and manage execution/inspection * of debuggees. @@ -2087,133 +1731,16 @@ exports.ThreadActor = ThreadActor; function PauseActor(pool) { this.pool = pool; } PauseActor.prototype = { actorPrefix: "pause" }; -/** - * A base actor for any actors that should only respond receive messages in the - * paused state. Subclasses may expose a `threadActor` which is used to help - * determine when we are in a paused state. Subclasses should set their own - * "constructor" property if they want better error messages. You should never - * instantiate a PauseScopedActor directly, only through subclasses. - */ -function PauseScopedActor() { -} - -/** - * A function decorator for creating methods to handle protocol messages that - * should only be received while in the paused state. - * - * @param method Function - * The function we are decorating. - */ -PauseScopedActor.withPaused = function (method) { - return function () { - if (this.isPaused()) { - return method.apply(this, arguments); - } - return this._wrongState(); - }; -}; - -PauseScopedActor.prototype = { - - /** - * Returns true if we are in the paused state. - */ - isPaused: function () { - // When there is not a ThreadActor available (like in the webconsole) we - // have to be optimistic and assume that we are paused so that we can - // respond to requests. - return this.threadActor ? this.threadActor.state === "paused" : true; - }, - - /** - * Returns the wrongState response packet for this actor. - */ - _wrongState: function () { - return { - error: "wrongState", - message: this.constructor.name + - " actors can only be accessed while the thread is paused." - }; - } -}; - -/** - * Creates a pause-scoped actor for the specified object. - * @see ObjectActor - */ -function PauseScopedObjectActor(obj, hooks) { - ObjectActor.call(this, obj, hooks); - this.hooks.promote = hooks.promote; - this.hooks.isThreadLifetimePool = hooks.isThreadLifetimePool; -} - -PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype); - -Object.assign(PauseScopedObjectActor.prototype, ObjectActor.prototype); - -Object.assign(PauseScopedObjectActor.prototype, { - constructor: PauseScopedObjectActor, - actorPrefix: "pausedobj", - - onOwnPropertyNames: - PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames), - - onPrototypeAndProperties: - PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties), - - onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype), - onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty), - onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile), - - onDisplayString: - PauseScopedActor.withPaused(ObjectActor.prototype.onDisplayString), - - onParameterNames: - PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames), - - /** - * Handle a protocol request to promote a pause-lifetime grip to a - * thread-lifetime grip. - * - * @param request object - * The protocol request object. - */ - onThreadGrip: PauseScopedActor.withPaused(function (request) { - this.hooks.promote(); - return {}; - }), - - /** - * Handle a protocol request to release a thread-lifetime grip. - * - * @param request object - * The protocol request object. - */ - onRelease: PauseScopedActor.withPaused(function (request) { - if (this.hooks.isThreadLifetimePool()) { - return { error: "notReleasable", - message: "Only thread-lifetime actors can be released." }; - } - - this.release(); - return {}; - }), -}); - -Object.assign(PauseScopedObjectActor.prototype.requestTypes, { - "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip, -}); - function hackDebugger(Debugger) { // TODO: Improve native code instead of hacking on top of it /** * Override the toString method in order to get more meaningful script output * for debugging the debugger. */ Debugger.Script.prototype.toString = function () { diff --git a/devtools/server/actors/utils/breakpoint-actor-map.js b/devtools/server/actors/utils/breakpoint-actor-map.js new file mode 100644 --- /dev/null +++ b/devtools/server/actors/utils/breakpoint-actor-map.js @@ -0,0 +1,173 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 { OriginalLocation } = require("devtools/server/actors/common"); + +/** + * A BreakpointActorMap is a map from locations to instances of BreakpointActor. + */ +function BreakpointActorMap() { + this._size = 0; + this._actors = {}; +} + +BreakpointActorMap.prototype = { + /** + * Return the number of BreakpointActors in this BreakpointActorMap. + * + * @returns Number + * The number of BreakpointActor in this BreakpointActorMap. + */ + get size() { + return this._size; + }, + + /** + * Generate all BreakpointActors that match the given location in + * this BreakpointActorMap. + * + * @param OriginalLocation location + * The location for which matching BreakpointActors should be generated. + */ + findActors: function* (location = new OriginalLocation()) { + // Fast shortcut for when we know we won't find any actors. Surprisingly + // enough, this speeds up refreshing when there are no breakpoints set by + // about 2x! + if (this.size === 0) { + return; + } + + function* findKeys(obj, key) { + if (key !== undefined) { + if (key in obj) { + yield key; + } + } else { + for (key of Object.keys(obj)) { + yield key; + } + } + } + + let query = { + sourceActorID: location.originalSourceActor + ? location.originalSourceActor.actorID + : undefined, + line: location.originalLine, + }; + + // If location contains a line, assume we are searching for a whole line + // breakpoint, and set begin/endColumn accordingly. Otherwise, we are + // searching for all breakpoints, so begin/endColumn should be left unset. + if (location.originalLine) { + query.beginColumn = location.originalColumn ? location.originalColumn : 0; + query.endColumn = location.originalColumn ? location.originalColumn + 1 : Infinity; + } else { + query.beginColumn = location.originalColumn ? query.originalColumn : undefined; + query.endColumn = location.originalColumn ? query.originalColumn + 1 : undefined; + } + + for (let sourceActorID of findKeys(this._actors, query.sourceActorID)) { + let actor = this._actors[sourceActorID]; + for (let line of findKeys(actor, query.line)) { + for (let beginColumn of findKeys(actor[line], query.beginColumn)) { + for (let endColumn of findKeys(actor[line][beginColumn], + query.endColumn)) { + yield actor[line][beginColumn][endColumn]; + } + } + } + } + }, + + /** + * Return the BreakpointActor at the given location in this + * BreakpointActorMap. + * + * @param OriginalLocation location + * The location for which the BreakpointActor should be returned. + * + * @returns BreakpointActor actor + * The BreakpointActor at the given location. + */ + getActor: function (originalLocation) { + for (let actor of this.findActors(originalLocation)) { + return actor; + } + + return null; + }, + + /** + * Set the given BreakpointActor to the given location in this + * BreakpointActorMap. + * + * @param OriginalLocation location + * The location to which the given BreakpointActor should be set. + * + * @param BreakpointActor actor + * The BreakpointActor to be set to the given location. + */ + setActor: function (location, actor) { + let { originalSourceActor, originalLine, originalColumn } = location; + + let sourceActorID = originalSourceActor.actorID; + let line = originalLine; + let beginColumn = originalColumn ? originalColumn : 0; + let endColumn = originalColumn ? originalColumn + 1 : Infinity; + + if (!this._actors[sourceActorID]) { + this._actors[sourceActorID] = []; + } + if (!this._actors[sourceActorID][line]) { + this._actors[sourceActorID][line] = []; + } + if (!this._actors[sourceActorID][line][beginColumn]) { + this._actors[sourceActorID][line][beginColumn] = []; + } + if (!this._actors[sourceActorID][line][beginColumn][endColumn]) { + ++this._size; + } + this._actors[sourceActorID][line][beginColumn][endColumn] = actor; + }, + + /** + * Delete the BreakpointActor from the given location in this + * BreakpointActorMap. + * + * @param OriginalLocation location + * The location from which the BreakpointActor should be deleted. + */ + deleteActor: function (location) { + let { originalSourceActor, originalLine, originalColumn } = location; + + let sourceActorID = originalSourceActor.actorID; + let line = originalLine; + let beginColumn = originalColumn ? originalColumn : 0; + let endColumn = originalColumn ? originalColumn + 1 : Infinity; + + if (this._actors[sourceActorID]) { + if (this._actors[sourceActorID][line]) { + if (this._actors[sourceActorID][line][beginColumn]) { + if (this._actors[sourceActorID][line][beginColumn][endColumn]) { + --this._size; + } + delete this._actors[sourceActorID][line][beginColumn][endColumn]; + if (Object.keys(this._actors[sourceActorID][line][beginColumn]).length === 0) { + delete this._actors[sourceActorID][line][beginColumn]; + } + } + if (Object.keys(this._actors[sourceActorID][line]).length === 0) { + delete this._actors[sourceActorID][line]; + } + } + } + } +}; + +exports.BreakpointActorMap = BreakpointActorMap; diff --git a/devtools/server/actors/utils/event-loop.js b/devtools/server/actors/utils/event-loop.js new file mode 100644 --- /dev/null +++ b/devtools/server/actors/utils/event-loop.js @@ -0,0 +1,157 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 xpcInspector = require("xpcInspector"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const { dumpn } = DevToolsUtils; + +/** + * Manages pushing event loops and automatically pops and exits them in the + * correct order as they are resolved. + * + * @param ThreadActor thread + * The thread actor instance that owns this EventLoopStack. + * @param DebuggerServerConnection connection + * The remote protocol connection associated with this event loop stack. + * @param Object hooks + * An object with the following properties: + * - url: The URL string of the debuggee we are spinning an event loop + * for. + * - preNest: function called before entering a nested event loop + * - postNest: function called after exiting a nested event loop + */ +function EventLoopStack({ thread, connection, hooks }) { + this._hooks = hooks; + this._thread = thread; + this._connection = connection; +} + +EventLoopStack.prototype = { + /** + * The number of nested event loops on the stack. + */ + get size() { + return xpcInspector.eventLoopNestLevel; + }, + + /** + * The URL of the debuggee who pushed the event loop on top of the stack. + */ + get lastPausedUrl() { + let url = null; + if (this.size > 0) { + try { + url = xpcInspector.lastNestRequestor.url; + } catch (e) { + // The tab's URL getter may throw if the tab is destroyed by the time + // this code runs, but we don't really care at this point. + dumpn(e); + } + } + return url; + }, + + /** + * The DebuggerServerConnection of the debugger who pushed the event loop on + * top of the stack + */ + get lastConnection() { + return xpcInspector.lastNestRequestor._connection; + }, + + /** + * Push a new nested event loop onto the stack. + * + * @returns EventLoop + */ + push: function () { + return new EventLoop({ + thread: this._thread, + connection: this._connection, + hooks: this._hooks + }); + } +}; + +/** + * An object that represents a nested event loop. It is used as the nest + * requestor with nsIJSInspector instances. + * + * @param ThreadActor thread + * The thread actor that is creating this nested event loop. + * @param DebuggerServerConnection connection + * The remote protocol connection associated with this event loop. + * @param Object hooks + * The same hooks object passed into EventLoopStack during its + * initialization. + */ +function EventLoop({ thread, connection, hooks }) { + this._thread = thread; + this._hooks = hooks; + this._connection = connection; + + this.enter = this.enter.bind(this); + this.resolve = this.resolve.bind(this); +} + +EventLoop.prototype = { + entered: false, + resolved: false, + get url() { + return this._hooks.url; + }, + + /** + * Enter this nested event loop. + */ + enter: function () { + let nestData = this._hooks.preNest + ? this._hooks.preNest() + : null; + + this.entered = true; + xpcInspector.enterNestedEventLoop(this); + + // Keep exiting nested event loops while the last requestor is resolved. + if (xpcInspector.eventLoopNestLevel > 0) { + const { resolved } = xpcInspector.lastNestRequestor; + if (resolved) { + xpcInspector.exitNestedEventLoop(); + } + } + + if (this._hooks.postNest) { + this._hooks.postNest(nestData); + } + }, + + /** + * Resolve this nested event loop. + * + * @returns boolean + * True if we exited this nested event loop because it was on top of + * the stack, false if there is another nested event loop above this + * one that hasn't resolved yet. + */ + resolve: function () { + if (!this.entered) { + throw new Error("Can't resolve an event loop before it has been entered!"); + } + if (this.resolved) { + throw new Error("Already resolved this nested event loop!"); + } + this.resolved = true; + if (this === xpcInspector.lastNestRequestor) { + xpcInspector.exitNestedEventLoop(); + return true; + } + return false; + }, +}; + +exports.EventLoopStack = EventLoopStack; diff --git a/devtools/server/actors/utils/moz.build b/devtools/server/actors/utils/moz.build --- a/devtools/server/actors/utils/moz.build +++ b/devtools/server/actors/utils/moz.build @@ -3,16 +3,19 @@ # 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/. DevToolsModules( 'actor-registry-utils.js', 'audionodes.json', 'automation-timeline.js', + 'breakpoint-actor-map.js', 'css-grid-utils.js', + 'event-loop.js', 'make-debugger.js', 'map-uri-to-addon-id.js', 'shapes-utils.js', + 'source-actor-store.js', 'stack.js', 'TabSources.js', 'walker-search.js', ) diff --git a/devtools/server/actors/utils/source-actor-store.js b/devtools/server/actors/utils/source-actor-store.js new file mode 100644 --- /dev/null +++ b/devtools/server/actors/utils/source-actor-store.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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"; + +loader.lazyRequireGetter(this, "getSourceURL", "devtools/server/actors/source", true); + +/** + * Keeps track of persistent sources across reloads and ties different + * source instances to the same actor id so that things like + * breakpoints survive reloads. ThreadSources uses this to force the + * same actorID on a SourceActor. + */ +function SourceActorStore() { + // source identifier --> actor id + this._sourceActorIds = Object.create(null); +} + +SourceActorStore.prototype = { + /** + * Lookup an existing actor id that represents this source, if available. + */ + getReusableActorId: function (source, originalUrl) { + let url = this.getUniqueKey(source, originalUrl); + if (url && url in this._sourceActorIds) { + return this._sourceActorIds[url]; + } + return null; + }, + + /** + * Update a source with an actorID. + */ + setReusableActorId: function (source, originalUrl, actorID) { + let url = this.getUniqueKey(source, originalUrl); + if (url) { + this._sourceActorIds[url] = actorID; + } + }, + + /** + * Make a unique URL from a source that identifies it across reloads. + */ + getUniqueKey: function (source, originalUrl) { + if (originalUrl) { + // Original source from a sourcemap. + return originalUrl; + } + + return getSourceURL(source); + } +}; + +exports.SourceActorStore = SourceActorStore; diff --git a/devtools/server/actors/webconsole.js b/devtools/server/actors/webconsole.js --- a/devtools/server/actors/webconsole.js +++ b/devtools/server/actors/webconsole.js @@ -6,17 +6,17 @@ "use strict"; /* global XPCNativeWrapper */ const Services = require("Services"); const { Cc, Ci, Cu } = require("chrome"); const { DebuggerServer, ActorPool } = require("devtools/server/main"); -const { ThreadActor } = require("devtools/server/actors/script"); +const { ThreadActor } = require("devtools/server/actors/thread"); const { ObjectActor, LongStringActor, createValueGrip, stringIsLong } = require("devtools/server/actors/object"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const ErrorDocs = require("devtools/server/actors/errordocs"); loader.lazyRequireGetter(this, "NetworkMonitor", "devtools/shared/webconsole/network-monitor", true); loader.lazyRequireGetter(this, "NetworkMonitorChild", "devtools/shared/webconsole/network-monitor", true); loader.lazyRequireGetter(this, "ConsoleProgressListener", "devtools/shared/webconsole/network-monitor", true); loader.lazyRequireGetter(this, "StackTraceCollector", "devtools/shared/webconsole/network-monitor", true); diff --git a/devtools/server/actors/webextension.js b/devtools/server/actors/webextension.js --- a/devtools/server/actors/webextension.js +++ b/devtools/server/actors/webextension.js @@ -6,17 +6,17 @@ const { Ci, Cu, Cc } = require("chrome"); const Services = require("Services"); const { ChromeActor } = require("./chrome"); const makeDebugger = require("./utils/make-debugger"); loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id"); -loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true); +loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/thread", true); const FALLBACK_DOC_MESSAGE = "Your addon does not have any document opened yet."; /** * Creates a TabActor for debugging all the contexts associated to a target WebExtensions * add-on running in a child extension process. * Most of the implementation is inherited from ChromeActor (which inherits most of its * implementation from TabActor). diff --git a/devtools/server/tests/unit/test_breakpoint-actor-map.js b/devtools/server/tests/unit/test_breakpoint-actor-map.js --- a/devtools/server/tests/unit/test_breakpoint-actor-map.js +++ b/devtools/server/tests/unit/test_breakpoint-actor-map.js @@ -1,17 +1,17 @@ /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Test the functionality of the BreakpointActorMap object. -const { BreakpointActorMap } = require("devtools/server/actors/script"); +const { BreakpointActorMap } = require("devtools/server/actors/utils/breakpoint-actor-map"); function run_test() { test_get_actor(); test_set_actor(); test_delete_actor(); test_find_actors(); test_duplicate_actors(); } diff --git a/devtools/server/tests/unit/testactors.js b/devtools/server/tests/unit/testactors.js --- a/devtools/server/tests/unit/testactors.js +++ b/devtools/server/tests/unit/testactors.js @@ -1,16 +1,16 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { ActorPool, appendExtraActors, createExtraActors } = require("devtools/server/actors/common"); const { RootActor } = require("devtools/server/actors/root"); -const { ThreadActor } = require("devtools/server/actors/script"); +const { ThreadActor } = require("devtools/server/actors/thread"); const { DebuggerServer } = require("devtools/server/main"); const { TabSources } = require("devtools/server/actors/utils/TabSources"); const makeDebugger = require("devtools/server/actors/utils/make-debugger"); var gTestGlobals = []; DebuggerServer.addTestGlobal = function (global) { gTestGlobals.push(global); }; diff --git a/devtools/server/worker.js b/devtools/server/worker.js --- a/devtools/server/worker.js +++ b/devtools/server/worker.js @@ -25,17 +25,17 @@ this.rpc = function (method, ...params) rpcDeferreds[id] = deferred; return deferred.promise; }; loadSubScript("resource://devtools/shared/worker/loader.js"); var defer = worker.require("devtools/shared/defer"); var { ActorPool } = worker.require("devtools/server/actors/common"); -var { ThreadActor } = worker.require("devtools/server/actors/script"); +var { ThreadActor } = worker.require("devtools/server/actors/thread"); var { WebConsoleActor } = worker.require("devtools/server/actors/webconsole"); var { TabSources } = worker.require("devtools/server/actors/utils/TabSources"); var makeDebugger = worker.require("devtools/server/actors/utils/make-debugger"); var { DebuggerServer } = worker.require("devtools/server/main"); DebuggerServer.init(); DebuggerServer.createRootActor = function () { throw new Error("Should never get here!"); diff --git a/devtools/shared/security/tests/unit/testactors.js b/devtools/shared/security/tests/unit/testactors.js --- a/devtools/shared/security/tests/unit/testactors.js +++ b/devtools/shared/security/tests/unit/testactors.js @@ -1,17 +1,17 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { ActorPool, appendExtraActors, createExtraActors } = require("devtools/server/actors/common"); const { RootActor } = require("devtools/server/actors/root"); -const { ThreadActor } = require("devtools/server/actors/script"); +const { ThreadActor } = require("devtools/server/actors/thread"); const { DebuggerServer } = require("devtools/server/main"); const promise = require("promise"); var gTestGlobals = []; DebuggerServer.addTestGlobal = function (global) { gTestGlobals.push(global); }; diff --git a/devtools/shared/transport/tests/unit/head_dbg.js b/devtools/shared/transport/tests/unit/head_dbg.js --- a/devtools/shared/transport/tests/unit/head_dbg.js +++ b/devtools/shared/transport/tests/unit/head_dbg.js @@ -89,17 +89,17 @@ var listener = { var consoleService = Cc["@mozilla.org/consoleservice;1"] .getService(Ci.nsIConsoleService); consoleService.registerListener(listener); /** * Initialize the testing debugger server. */ function initTestDebuggerServer() { - DebuggerServer.registerModule("devtools/server/actors/script", { + DebuggerServer.registerModule("devtools/server/actors/thread", { prefix: "script", constructor: "ScriptActor", type: { global: true, tab: true } }); DebuggerServer.registerModule("xpcshell-test/testactors"); // Allow incoming connections. DebuggerServer.init(); } diff --git a/devtools/shared/transport/tests/unit/testactors.js b/devtools/shared/transport/tests/unit/testactors.js --- a/devtools/shared/transport/tests/unit/testactors.js +++ b/devtools/shared/transport/tests/unit/testactors.js @@ -1,16 +1,16 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { ActorPool, appendExtraActors, createExtraActors } = require("devtools/server/actors/common"); const { RootActor } = require("devtools/server/actors/root"); -const { ThreadActor } = require("devtools/server/actors/script"); +const { ThreadActor } = require("devtools/server/actors/thread"); const { DebuggerServer } = require("devtools/server/main"); const promise = require("promise"); var gTestGlobals = []; DebuggerServer.addTestGlobal = function (global) { gTestGlobals.push(global); };