resources.js (14400B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { throttle } = require("resource://devtools/shared/throttle.js"); 8 9 const { makeDebuggeeValue } = require("devtools/server/actors/object/utils"); 10 11 const { 12 TYPES, 13 getResourceWatcher, 14 } = require("resource://devtools/server/actors/resources/index.js"); 15 const { JSTRACER_TRACE } = TYPES; 16 17 const lazy = {}; 18 ChromeUtils.defineESModuleGetters( 19 lazy, 20 { 21 JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs", 22 }, 23 { global: "contextual" } 24 ); 25 26 const { 27 getActorIdForInternalSourceId, 28 } = require("resource://devtools/server/actors/utils/dbg-source.js"); 29 30 const THROTTLING_DELAY = 250; 31 32 class ResourcesTracingListener { 33 constructor({ targetActor, traceValues, traceActor }) { 34 this.targetActor = targetActor; 35 this.traceValues = traceValues; 36 this.sourcesManager = targetActor.sourcesManager; 37 this.traceActor = traceActor; 38 39 // On workers, we don't have access to setTimeout and can't have throttling 40 this.throttleEmitTraces = isWorker 41 ? this.flushTraces.bind(this) 42 : throttle(this.flushTraces.bind(this), THROTTLING_DELAY); 43 } 44 45 // Collect pending data to be sent to the client in various arrays, 46 // each focusing on particular data type. 47 // All these arrays contains arrays as elements. 48 #throttledTraces = []; 49 50 // Index of the next collected frame 51 #frameIndex = 0; 52 // Three level of Maps, ultimately storing frame indexes. 53 // The first level of Map is keyed by source ID, 54 // the second by line number, 55 // the last by column number. 56 // Frame objects are sent to the client and not being held in memory, 57 // we only store their related indexes which are put in trace arrays. 58 #frameMap = new Map(); 59 60 /** 61 * Called when the tracer stops recording JS executions. 62 */ 63 stop() { 64 this.#frameIndex = 0; 65 this.#frameMap.clear(); 66 } 67 68 /** 69 * This method is throttled and will notify all pending traces to be logged in the console 70 * via the console message watcher. 71 */ 72 flushTraces() { 73 const traceWatcher = getResourceWatcher(this.targetActor, JSTRACER_TRACE); 74 // Ignore the request if the frontend isn't listening to traces for that target. 75 if (!traceWatcher) { 76 return; 77 } 78 const traces = this.#throttledTraces; 79 this.#throttledTraces = []; 80 81 traceWatcher.emitTraces(traces); 82 } 83 84 /** 85 * Be notified by the underlying JavaScriptTracer class 86 * in case it stops by itself, instead of being stopped when the Actor's stopTracing 87 * method is called by the user. 88 * 89 * @param {boolean} enabled 90 * True if the tracer starts tracing, false it it stops. 91 * @return {boolean} 92 * Return true, if the JavaScriptTracer should log a message to stdout. 93 */ 94 onTracingToggled(enabled) { 95 if (!enabled) { 96 this.traceActor.stopTracing(); 97 } 98 return false; 99 } 100 101 /** 102 * Called when "trace on next user interaction" is enabled, to notify the user 103 * that the tracer is initialized but waiting for the user first input. 104 */ 105 onTracingPending() { 106 const consoleMessageWatcher = getResourceWatcher( 107 this.targetActor, 108 TYPES.CONSOLE_MESSAGE 109 ); 110 if (consoleMessageWatcher) { 111 consoleMessageWatcher.emitMessages([ 112 { 113 arguments: [lazy.JSTracer.NEXT_INTERACTION_MESSAGE], 114 styles: [], 115 level: "jstracer", 116 chromeContext: false, 117 timeStamp: ChromeUtils.dateNow(), 118 }, 119 ]); 120 } 121 return false; 122 } 123 124 /** 125 * Called by JavaScriptTracer class when a new mutation happened on any DOM Element. 126 * 127 * @param {object} options 128 * @param {number} options.depth 129 * Represents the depth of the frame in the call stack. 130 * @param {string} options.prefix 131 * A string to be displayed as a prefix of any logged frame. 132 * @param {nsIStackFrame} options.caller 133 * The JS Callsite which caused this mutation. 134 * @param {string} options.type 135 * Type of DOM Mutation: 136 * - "add": Node being added, 137 * - "attributes": Node whose attributes changed, 138 * - "remove": Node being removed, 139 * @param {DOMNode} options.element 140 * The DOM Node related to the current mutation. 141 * @return {boolean} 142 * Return true, if the JavaScriptTracer should log a message to stdout. 143 */ 144 onTracingDOMMutation({ depth, prefix, type, caller, element }) { 145 const dbgObj = makeDebuggeeValue(this.targetActor, element); 146 const frameIndex = this.#getFrameIndex( 147 null, 148 null, 149 caller 150 ? getActorIdForInternalSourceId(this.targetActor, caller.sourceId) 151 : null, 152 caller?.lineNumber, 153 caller?.columnNumber, 154 caller?.filename 155 ); 156 this.#throttledTraces.push([ 157 "dom-mutation", 158 prefix, 159 frameIndex, 160 ChromeUtils.dateNow(), 161 depth, 162 type, 163 this.traceActor.createValueGrip(dbgObj), 164 ]); 165 this.throttleEmitTraces(); 166 return false; 167 } 168 169 /** 170 * Called by JavaScriptTracer class on each step of a function call. 171 * 172 * @param {object} options 173 * @param {Debugger.Frame} options.frame 174 * A descriptor object for the JavaScript frame. 175 * @param {number} options.depth 176 * Represents the depth of the frame in the call stack. 177 * @param {string} options.prefix 178 * A string to be displayed as a prefix of any logged frame. 179 * @return {boolean} 180 * Return true, if the JavaScriptTracer should log the step to stdout. 181 */ 182 onTracingFrameStep({ frame, depth, prefix }) { 183 const { script } = frame; 184 const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset); 185 const url = script.source.url; 186 187 // NOTE: Debugger.Script.prototype.getOffsetMetadata returns 188 // columnNumber in 1-based. 189 // Convert to 0-based, while keeping the wasm's column (1) as is. 190 // (bug 1863878) 191 const columnBase = script.format === "wasm" ? 0 : 1; 192 const column = columnNumber - columnBase; 193 194 // Ignore blackboxed sources 195 if (this.sourcesManager.isBlackBoxed(url, lineNumber, column)) { 196 return false; 197 } 198 199 const frameIndex = this.#getFrameIndex( 200 frame.implementation, 201 null, 202 getActorIdForInternalSourceId(this.targetActor, script.source.id), 203 lineNumber, 204 column, 205 url 206 ); 207 this.#throttledTraces.push([ 208 "step", 209 prefix, 210 frameIndex, 211 ChromeUtils.dateNow(), 212 depth, 213 null, 214 ]); 215 this.throttleEmitTraces(); 216 217 return false; 218 } 219 220 #getFrameIndex(implementation, name, sourceId, line, column, url) { 221 let perSourceMap = this.#frameMap.get(sourceId); 222 if (!perSourceMap) { 223 perSourceMap = new Map(); 224 this.#frameMap.set(sourceId, perSourceMap); 225 } 226 let perLineMap = perSourceMap.get(line); 227 if (!perLineMap) { 228 perLineMap = new Map(); 229 perSourceMap.set(line, perLineMap); 230 } 231 let frameIndex = perLineMap.get(column); 232 233 if (frameIndex == undefined) { 234 frameIndex = this.#frameIndex++; 235 236 // Remember updating TRACER_FIELDS_INDEXES when modifying the following array: 237 const frameArray = [ 238 "frame", 239 implementation, 240 name, 241 sourceId, 242 line, 243 column, 244 url, 245 ]; 246 247 perLineMap.set(column, frameIndex); 248 this.#throttledTraces.push(frameArray); 249 } 250 return frameIndex; 251 } 252 253 /** 254 * Called by JavaScriptTracer class when a new JavaScript frame is executed. 255 * 256 * @param {Debugger.Frame} frame 257 * A descriptor object for the JavaScript frame. 258 * @param {number} depth 259 * Represents the depth of the frame in the call stack. 260 * @param {string} formatedDisplayName 261 * A human readable name for the current frame. 262 * @param {string} prefix 263 * A string to be displayed as a prefix of any logged frame. 264 * @param {string} currentDOMEvent 265 * If this is a top level frame (depth==0), and we are currently processing 266 * a DOM Event, this will refer to the name of that DOM Event. 267 * Note that it may also refer to setTimeout and setTimeout callback calls. 268 * @return {boolean} 269 * Return true, if the JavaScriptTracer should log the frame to stdout. 270 */ 271 onTracingFrame({ 272 frame, 273 depth, 274 formatedDisplayName, 275 prefix, 276 currentDOMEvent, 277 }) { 278 const { script } = frame; 279 const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset); 280 const url = script.source.url; 281 282 // NOTE: Debugger.Script.prototype.getOffsetMetadata returns 283 // columnNumber in 1-based. 284 // Convert to 0-based, while keeping the wasm's column (1) as is. 285 // (bug 1863878) 286 const columnBase = script.format === "wasm" ? 0 : 1; 287 const column = columnNumber - columnBase; 288 289 // Ignore blackboxed sources 290 if (this.sourcesManager.isBlackBoxed(url, lineNumber, column)) { 291 return false; 292 } 293 294 // We may receive the currently processed DOM event (if this relates to one). 295 // In this case, log a preliminary message, which looks different to highlight it. 296 if (currentDOMEvent && depth == 0) { 297 // Create a JSTRACER_TRACE resource with a slightly different shape 298 this.#throttledTraces.push([ 299 "event", 300 prefix, 301 null, 302 ChromeUtils.dateNow(), 303 // Events are parent of any subsequent JS call, which has a 0 depth. 304 -1, 305 currentDOMEvent, 306 ]); 307 } 308 309 let args = undefined, 310 argNames = undefined; 311 // Log arguments, but only when this feature is enabled as it introduce 312 // some significant overhead in perf as well as memory as it may hold the objects in memory. 313 // Also prevent trying to log function call arguments if we aren't logging a frame 314 // with arguments (e.g. Debugger evaluation frames, when executing from the console) 315 if (this.traceValues && frame.arguments) { 316 args = []; 317 for (let arg of frame.arguments) { 318 // Debugger.Frame.arguments contains either a Debugger.Object or primitive object 319 if (arg?.unsafeDereference) { 320 arg = arg.unsafeDereference(); 321 } 322 // Instantiate a object actor so that the tools can easily inspect these objects 323 const dbgObj = makeDebuggeeValue(this.targetActor, arg); 324 args.push(this.traceActor.createValueGrip(dbgObj)); 325 } 326 argNames = frame.callee.script.parameterNames; 327 } 328 329 // In order for getActorIdForInternalSourceId to work reliably, 330 // we have to ensure creating a source actor for that source. 331 // It happens on Google Docs that some evaled sources aren't registered? 332 this.sourcesManager.getOrCreateSourceActor(script.source); 333 334 const frameIndex = this.#getFrameIndex( 335 frame.implementation, 336 formatedDisplayName, 337 getActorIdForInternalSourceId(this.targetActor, script.source.id), 338 lineNumber, 339 column, 340 url 341 ); 342 this.#throttledTraces.push([ 343 "enter", 344 prefix, 345 frameIndex, 346 ChromeUtils.dateNow(), 347 depth, 348 args, 349 argNames, 350 ]); 351 this.throttleEmitTraces(); 352 353 return false; 354 } 355 356 /** 357 * Called by JavaScriptTracer class when a JavaScript frame exits (i.e. a function returns or throw). 358 * 359 * @param {object} options 360 * @param {number} options.frameId 361 * Unique identifier for the current frame. 362 * This should match a frame notified via onTracingFrame. 363 * @param {Debugger.Frame} options.frame 364 * A descriptor object for the JavaScript frame. 365 * @param {number} options.depth 366 * Represents the depth of the frame in the call stack. 367 * @param {string} options.formatedDisplayName 368 * A human readable name for the current frame. 369 * @param {string} options.prefix 370 * A string to be displayed as a prefix of any logged frame. 371 * @param {string} options.why 372 * A string to explain why the function stopped. 373 * See tracer.sys.mjs's FRAME_EXIT_REASONS. 374 * @param {Debugger.Object|primitive} options.rv 375 * The returned value. It can be the returned value, or the thrown exception. 376 * It is either a primitive object, otherwise it is a Debugger.Object for any other JS Object type. 377 * @return {boolean} 378 * Return true, if the JavaScriptTracer should log the frame to stdout. 379 */ 380 onTracingFrameExit({ 381 frameId, 382 frame, 383 depth, 384 formatedDisplayName, 385 prefix, 386 why, 387 rv, 388 }) { 389 const { script } = frame; 390 const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset); 391 const url = script.source.url; 392 393 // NOTE: Debugger.Script.prototype.getOffsetMetadata returns 394 // columnNumber in 1-based. 395 // Convert to 0-based, while keeping the wasm's column (1) as is. 396 // (bug 1863878) 397 const columnBase = script.format === "wasm" ? 0 : 1; 398 const column = columnNumber - columnBase; 399 400 // Ignore blackboxed sources 401 if (this.sourcesManager.isBlackBoxed(url, lineNumber, column)) { 402 return false; 403 } 404 405 let returnedValue = undefined; 406 // Log arguments, but only when this feature is enabled as it introduce 407 // some significant overhead in perf as well as memory as it may hold the objects in memory. 408 if (this.traceValues) { 409 // Debugger.Frame.arguments contains either a Debugger.Object or primitive object 410 if (rv?.unsafeDereference) { 411 rv = rv.unsafeDereference(); 412 } 413 // Instantiate a object actor so that the tools can easily inspect these objects 414 const dbgObj = makeDebuggeeValue(this.targetActor, rv); 415 returnedValue = this.traceActor.createValueGrip(dbgObj); 416 } 417 418 const frameIndex = this.#getFrameIndex( 419 frame.implementation, 420 formatedDisplayName, 421 getActorIdForInternalSourceId(this.targetActor, script.source.id), 422 lineNumber, 423 column, 424 url 425 ); 426 this.#throttledTraces.push([ 427 "exit", 428 prefix, 429 frameIndex, 430 ChromeUtils.dateNow(), 431 depth, 432 frameId, 433 returnedValue, 434 why, 435 ]); 436 this.throttleEmitTraces(); 437 438 return false; 439 } 440 } 441 442 exports.ResourcesTracingListener = ResourcesTracingListener;