tracer.js (8697B)
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 lazy = {}; 8 ChromeUtils.defineESModuleGetters( 9 lazy, 10 { 11 JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs", 12 }, 13 { global: "contextual" } 14 ); 15 16 const { Actor } = require("resource://devtools/shared/protocol.js"); 17 const { createValueGrip } = require("devtools/server/actors/object/utils"); 18 const { 19 ObjectActorPool, 20 } = require("resource://devtools/server/actors/object/ObjectActorPool.js"); 21 const { 22 tracerSpec, 23 TRACER_LOG_METHODS, 24 } = require("resource://devtools/shared/specs/tracer.js"); 25 26 loader.lazyRequireGetter( 27 this, 28 "StdoutTracingListener", 29 "resource://devtools/server/actors/tracer/stdout.js", 30 true 31 ); 32 loader.lazyRequireGetter( 33 this, 34 "ResourcesTracingListener", 35 "resource://devtools/server/actors/tracer/resources.js", 36 true 37 ); 38 loader.lazyRequireGetter( 39 this, 40 "ProfilerTracingListener", 41 "resource://devtools/server/actors/tracer/profiler.js", 42 true 43 ); 44 45 // Indexes of each data type within the array describing a trace 46 exports.TRACER_FIELDS_INDEXES = { 47 // This is shared with all the data types 48 TYPE: 0, 49 50 // Frame traces are slightly special and do not share any field with the other data types 51 FRAME_IMPLEMENTATION: 1, 52 FRAME_NAME: 2, 53 FRAME_SOURCEID: 3, 54 FRAME_LINE: 4, 55 FRAME_COLUMN: 5, 56 FRAME_URL: 6, 57 58 // These fields are shared with all but frame data types 59 PREFIX: 1, 60 FRAME_INDEX: 2, 61 TIMESTAMP: 3, 62 DEPTH: 4, 63 64 EVENT_NAME: 5, 65 66 ENTER_ARGS: 5, 67 ENTER_ARG_NAMES: 6, 68 69 EXIT_PARENT_FRAME_ID: 5, 70 EXIT_RETURNED_VALUE: 6, 71 EXIT_WHY: 7, 72 73 DOM_MUTATION_TYPE: 5, 74 DOM_MUTATION_ELEMENT: 6, 75 }; 76 77 const VALID_LOG_METHODS = Object.values(TRACER_LOG_METHODS); 78 79 class TracerActor extends Actor { 80 constructor(conn, targetActor) { 81 super(conn, tracerSpec); 82 this.targetActor = targetActor; 83 } 84 85 // When the tracer is stopped, save the result of the Listener Class. 86 // This is used by the profiler log method and the getProfile method. 87 #stopResult = null; 88 89 // A Pool for all JS values emitted by the Tracer Actor. 90 // This helps instantiate a unique Object Actor per JS Object communicated to the client. 91 // This also helps share the same Object Actor instances when evaluating JS via 92 // the console actor. 93 // This pool is created lazily, only once we start a new trace. 94 // We also clear the pool before starting the trace. 95 #tracerPool = null; 96 97 destroy() { 98 this.stopTracing(); 99 } 100 101 getLogMethod() { 102 return this.logMethod; 103 } 104 105 /** 106 * Toggle tracing JavaScript. 107 * Meant for the WebConsole command in order to pass advanced 108 * configuration directly to JavaScriptTracer class. 109 * 110 * @param {object} options 111 * Options used to configure JavaScriptTracer. 112 * See `JavaScriptTracer.startTracing`. 113 * @return {boolean} 114 * True if the tracer starts, or false if it was stopped. 115 */ 116 toggleTracing(options) { 117 if (!this.tracingListener) { 118 this.startTracing(options); 119 return true; 120 } 121 this.stopTracing(); 122 return false; 123 } 124 125 /** 126 * Start tracing. 127 * 128 * @param {object} options 129 * Options used to configure JavaScriptTracer. 130 * See `JavaScriptTracer.startTracing`. 131 */ 132 // eslint-disable-next-line complexity 133 startTracing(options = {}) { 134 if (options.logMethod && !VALID_LOG_METHODS.includes(options.logMethod)) { 135 throw new Error( 136 `Invalid log method '${options.logMethod}'. Only supports: ${VALID_LOG_METHODS}` 137 ); 138 } 139 if (options.prefix && typeof options.prefix != "string") { 140 throw new Error("Invalid prefix, only support string type"); 141 } 142 if (options.maxDepth && typeof options.maxDepth != "number") { 143 throw new Error("Invalid max-depth, only support numbers"); 144 } 145 if (options.maxRecords && typeof options.maxRecords != "number") { 146 throw new Error("Invalid max-records, only support numbers"); 147 } 148 149 // When tracing on next user interaction is enabled, 150 // disable logging from workers as this makes the tracer work 151 // against visible documents and is actived per document thread. 152 if (options.traceOnNextInteraction && isWorker) { 153 return; 154 } 155 156 // Ignore WindowGlobal target actors for WindowGlobal of iframes running in the same process and thread as their parent document. 157 // isProcessRoot will be true for each WindowGlobal being the top parent within a given process. 158 // It will typically be true for WindowGlobal of iframe running in a distinct origin and process, 159 // but only for the top iframe document. It will also be true for the top level tab document. 160 if ( 161 this.targetActor.window && 162 !this.targetActor.window.windowGlobalChild?.isProcessRoot 163 ) { 164 return; 165 } 166 167 // Flush any previous recorded data only when we start a new tracer 168 // as we may still analyse trace data after stopping the trace. 169 // The pool will then be re-created on demand from createValueGrip. 170 if (this.#tracerPool) { 171 this.#tracerPool.destroy(); 172 this.#tracerPool = null; 173 } 174 175 this.logMethod = options.logMethod || TRACER_LOG_METHODS.STDOUT; 176 177 let ListenerClass = null; 178 // Currently only the profiler output is supported with the native tracer. 179 let useNativeTracing = false; 180 switch (this.logMethod) { 181 case TRACER_LOG_METHODS.STDOUT: 182 ListenerClass = StdoutTracingListener; 183 break; 184 case TRACER_LOG_METHODS.CONSOLE: 185 case TRACER_LOG_METHODS.DEBUGGER_SIDEBAR: 186 // Console and debugger sidebar are both using JSTRACE_STATE/JSTRACE_TRACE resources 187 // to receive tracing data. 188 ListenerClass = ResourcesTracingListener; 189 break; 190 case TRACER_LOG_METHODS.PROFILER: 191 ListenerClass = ProfilerTracingListener; 192 // Recording function returns is mandatory when recording profiler output. 193 // Otherwise frames are not closed and mixed up in the profiler frontend. 194 options.traceFunctionReturn = true; 195 useNativeTracing = true; 196 break; 197 } 198 this.tracingListener = new ListenerClass({ 199 targetActor: this.targetActor, 200 traceValues: !!options.traceValues, 201 traceActor: this, 202 }); 203 lazy.JSTracer.addTracingListener(this.tracingListener); 204 205 this.traceValues = !!options.traceValues; 206 try { 207 lazy.JSTracer.startTracing({ 208 global: this.targetActor.targetGlobal, 209 prefix: options.prefix || "", 210 // Enable receiving the `currentDOMEvent` being passed to `onTracingFrame` 211 traceDOMEvents: true, 212 // Enable tracing DOM Mutations 213 traceDOMMutations: options.traceDOMMutations, 214 // Enable tracing function arguments as well as returned values 215 traceValues: !!options.traceValues, 216 // Enable tracing only on next user interaction 217 traceOnNextInteraction: !!options.traceOnNextInteraction, 218 // Notify about frame exit / function call returning 219 traceFunctionReturn: !!options.traceFunctionReturn, 220 // Use the native tracing implementation 221 useNativeTracing, 222 // Ignore frames beyond the given depth 223 maxDepth: options.maxDepth, 224 // Stop the tracing after a number of top level frames 225 maxRecords: options.maxRecords, 226 }); 227 } catch (e) { 228 // If startTracing throws, it probably rejected one of its options and we should 229 // unregister the tracing listener. 230 this.stopTracing(); 231 throw e; 232 } 233 } 234 235 async stopTracing() { 236 if (!this.tracingListener) { 237 return; 238 } 239 // Remove before stopping to prevent receiving the stop notification 240 lazy.JSTracer.removeTracingListener(this.tracingListener); 241 // Save the result of the stop request for the profiler and the getProfile RDP method 242 this.#stopResult = this.tracingListener.stop(); 243 this.tracingListener = null; 244 245 lazy.JSTracer.stopTracing(); 246 this.logMethod = null; 247 } 248 249 /** 250 * Queried by THREAD_STATE watcher to send the gecko profiler data 251 * as part of THREAD STATE "stop" resource. 252 * 253 * @return {object} Gecko profiler profile object. 254 */ 255 async getProfile() { 256 // #stopResult is a promise 257 return this.#stopResult; 258 } 259 260 createValueGrip(value) { 261 if (!this.#tracerPool) { 262 this.#tracerPool = new ObjectActorPool( 263 this.targetActor.threadActor, 264 "tracer", 265 true 266 ); 267 this.manage(this.#tracerPool); 268 } 269 return createValueGrip(this, value, this.#tracerPool); 270 } 271 } 272 exports.TracerActor = TracerActor;