base-target-actor.js (11096B)
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 { Actor } = require("resource://devtools/shared/protocol.js"); 8 const { 9 TYPES: { DOCUMENT_EVENT, NETWORK_EVENT_STACKTRACE, CONSOLE_MESSAGE }, 10 getResourceWatcher, 11 } = require("resource://devtools/server/actors/resources/index.js"); 12 const Targets = require("devtools/server/actors/targets/index"); 13 const { 14 ObjectActorPool, 15 } = require("resource://devtools/server/actors/object/ObjectActorPool.js"); 16 17 const { throttle } = require("resource://devtools/shared/throttle.js"); 18 const RESOURCES_THROTTLING_DELAY = 100; 19 20 loader.lazyRequireGetter( 21 this, 22 "SessionDataProcessors", 23 "resource://devtools/server/actors/targets/session-data-processors/index.js", 24 true 25 ); 26 27 class BaseTargetActor extends Actor { 28 constructor(conn, targetType, spec) { 29 super(conn, spec); 30 31 /** 32 * Type of target, a string of Targets.TYPES. 33 * 34 * @return {string} 35 */ 36 this.targetType = targetType; 37 38 // Lists of resources available/updated/destroyed RDP packet 39 // currently queued which will be emitted after a throttle delay. 40 this.#throttledResources = { 41 available: [], 42 updated: [], 43 destroyed: [], 44 }; 45 46 this.#throttledEmitResources = throttle( 47 this.emitResources.bind(this), 48 RESOURCES_THROTTLING_DELAY 49 ); 50 } 51 52 // Whenever createValueGripForTarget is used, any Object Actor will be added to this pool 53 get objectsPool() { 54 if (this._objectsPool) { 55 return this._objectsPool; 56 } 57 this._objectsPool = new ObjectActorPool(this.threadActor, "target-objects"); 58 this.manage(this._objectsPool); 59 return this._objectsPool; 60 } 61 62 #throttledResources; 63 #throttledEmitResources; 64 65 /** 66 * Process a new data entry, which can be watched resources, breakpoints, ... 67 * 68 * @param string type 69 * The type of data to be added 70 * @param Array<Object> entries 71 * The values to be added to this type of data 72 * @param Boolean isDocumentCreation 73 * Set to true if this function is called just after a new document (and its 74 * associated target) is created. 75 * @param String updateType 76 * "add" will only add the new entries in the existing data set. 77 * "set" will update the data set with the new entries. 78 */ 79 async addOrSetSessionDataEntry( 80 type, 81 entries, 82 isDocumentCreation = false, 83 updateType 84 ) { 85 const processor = SessionDataProcessors[type]; 86 if (processor) { 87 await processor.addOrSetSessionDataEntry( 88 this, 89 entries, 90 isDocumentCreation, 91 updateType 92 ); 93 } 94 } 95 96 /** 97 * Remove data entries that have been previously added via addOrSetSessionDataEntry 98 * 99 * See addOrSetSessionDataEntry for argument description. 100 */ 101 removeSessionDataEntry(type, entries) { 102 const processor = SessionDataProcessors[type]; 103 if (processor) { 104 processor.removeSessionDataEntry(this, entries); 105 } 106 } 107 108 /** 109 * Called by Resource Watchers, when new resources are available, updated or destroyed. 110 * This will only accumulate resource update packets into throttledResources object. 111 * The actualy sending of resources will happen from emitResources. 112 * 113 * @param String updateType 114 * Can be "available", "updated" or "destroyed" 115 * @param String resourceType 116 * The type of resources to be notified about. 117 * @param Array<json|string> resources 118 * For "available", the array will be a list of new resource JSON objects sent as-is to the client. 119 * It can contain actor IDs, actor forms, to be manually marshalled by the client. 120 * For "updated", the array will contain a list of objects with the attributes documented in 121 * `ResourceCommand._onResourceUpdated` jsdoc. 122 * For "destroyed", the array will contain a list of resource IDs (strings). 123 */ 124 notifyResources(updateType, resourceType, resources) { 125 if (resources.length === 0 || this.isDestroyed()) { 126 // Don't try to emit if the resources array is empty or the actor was 127 // destroyed. 128 return; 129 } 130 131 const shouldEmitSynchronously = 132 resourceType == NETWORK_EVENT_STACKTRACE || 133 (resourceType == DOCUMENT_EVENT && 134 resources.some(resource => resource.name == "will-navigate")); 135 136 // If the last throttled resources were of the same resource type, 137 // augment the resources array with the new resources 138 const lastResourceInThrottleCache = 139 this.#throttledResources[updateType].at(-1); 140 if ( 141 lastResourceInThrottleCache && 142 lastResourceInThrottleCache[0] === resourceType 143 ) { 144 lastResourceInThrottleCache[1].push.apply( 145 lastResourceInThrottleCache[1], 146 resources 147 ); 148 } else { 149 // Otherwise, add a new item in the throttle queue with the resource type 150 this.#throttledResources[updateType].push([resourceType, resources]); 151 } 152 153 // Force firing resources immediately when: 154 // * we receive DOCUMENT_EVENT's will-navigate 155 // This will force clearing resources on the client side ASAP. 156 // Otherwise we might emit some other RDP event (outside of resources), 157 // which will be cleared by the throttled/delayed will-navigate. 158 // * we receive NETWORK_EVENT_STACKTRACE which are meant to be dispatched *before* 159 // the related NETWORK_EVENT fired from the parent process which are also throttled. 160 if (shouldEmitSynchronously) { 161 this.emitResources(); 162 } else { 163 this.#throttledEmitResources(); 164 } 165 } 166 167 /** 168 * Flush resources to DevTools transport layer, actually sending all resource update packets 169 */ 170 emitResources() { 171 if (this.isDestroyed()) { 172 return; 173 } 174 for (const updateType of ["available", "updated", "destroyed"]) { 175 const resources = this.#throttledResources[updateType]; 176 if (!resources.length) { 177 continue; 178 } 179 this.#throttledResources[updateType] = []; 180 this.emit(`resources-${updateType}-array`, resources); 181 } 182 } 183 184 // List of actor prefixes (string) which have already been instantiated via getTargetScopedActor method. 185 #instantiatedTargetScopedActors = new Set(); 186 187 /** 188 * Try to return any target scoped actor instance, if it exists. 189 * They are lazily instantiated and so will only be available 190 * if the client called at least one of their method. 191 * 192 * @param {string} prefix 193 * Prefix for the actor we would like to retrieve. 194 * Defined in devtools/server/actors/utils/actor-registry.js 195 */ 196 getTargetScopedActor(prefix) { 197 if (this.isDestroyed()) { 198 return null; 199 } 200 const form = this.form(); 201 this.#instantiatedTargetScopedActors.add(prefix); 202 return this.conn._getOrCreateActor(form[prefix + "Actor"]); 203 } 204 205 /** 206 * Returns true, if the related target scoped actor has already been queried 207 * and instantiated via `getTargetScopedActor` method. 208 * 209 * @param {string} prefix 210 * See getTargetScopedActor definition 211 * @return Boolean 212 * True, if the actor has already been instantiated. 213 */ 214 hasTargetScopedActor(prefix) { 215 return this.#instantiatedTargetScopedActors.has(prefix); 216 } 217 218 /** 219 * Server side boolean to know if the tracer has been enabled by the user. 220 * 221 * By enabled, we mean the feature has been exposed to the user, 222 * not that the tracer is actively tracing executions. 223 */ 224 isTracerFeatureEnabled = false; 225 226 /** 227 * Apply target-specific options. 228 * 229 * This will be called by the watcher when the DevTools target-configuration 230 * is updated, or when a target is created via JSWindowActors. 231 * 232 * @param {JSON} options 233 * Configuration object provided by the client. 234 * See target-configuration actor. 235 * @param {boolean} calledFromDocumentCreate 236 * True, when this is called with initial configuration when the related target 237 * actor is instantiated. 238 */ 239 updateTargetConfiguration(options = {}, calledFromDocumentCreation = false) { 240 if (typeof options.isTracerFeatureEnabled === "boolean") { 241 this.isTracerFeatureEnabled = options.isTracerFeatureEnabled; 242 } 243 // If there is some tracer options, we should start tracing, otherwise we should stop (if we were) 244 if (options.tracerOptions) { 245 // Ignore the SessionData update if the user requested to start the tracer on next page load and: 246 // - we apply it to an already loaded WindowGlobal, 247 // - the target isn't the top level one. 248 if ( 249 options.tracerOptions.traceOnNextLoad && 250 (!calledFromDocumentCreation || !this.isTopLevelTarget) 251 ) { 252 if (this.isTopLevelTarget) { 253 const consoleMessageWatcher = getResourceWatcher( 254 this, 255 CONSOLE_MESSAGE 256 ); 257 if (consoleMessageWatcher) { 258 consoleMessageWatcher.emitMessages([ 259 { 260 arguments: [ 261 "Waiting for next navigation or page reload before starting tracing", 262 ], 263 styles: [], 264 level: "jstracer", 265 chromeContext: false, 266 timeStamp: ChromeUtils.dateNow(), 267 }, 268 ]); 269 } 270 } 271 return; 272 } 273 // Bug 1874204: For now, in the browser toolbox, only frame and workers are traced. 274 // Content process targets are ignored as they would also include each document/frame target. 275 // This would require some work to ignore FRAME targets from here, only in case of browser toolbox, 276 // and also handle all content process documents for DOM Event logging. 277 // 278 // Bug 1874219: Also ignore extensions for now as they are all running in the same process, 279 // whereas we can only spawn one tracer per thread. 280 if ( 281 this.targetType == Targets.TYPES.PROCESS || 282 this.url?.startsWith("moz-extension://") 283 ) { 284 return; 285 } 286 // In the browser toolbox, when debugging the parent process, we should only toggle the tracer in the Parent Process Target Actor. 287 // We have to ignore any frame target which may run in the parent process. 288 // For example DevTools documents or a tab running in the parent process. 289 // (PROCESS_TYPE_DEFAULT refers to the parent process) 290 if ( 291 this.sessionContext.type == "all" && 292 this.targetType === Targets.TYPES.FRAME && 293 this.typeName != "parentProcessTarget" && 294 Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT 295 ) { 296 return; 297 } 298 const tracerActor = this.getTargetScopedActor("tracer"); 299 tracerActor.startTracing(options.tracerOptions); 300 } else if (this.hasTargetScopedActor("tracer")) { 301 const tracerActor = this.getTargetScopedActor("tracer"); 302 tracerActor.stopTracing(); 303 } 304 } 305 } 306 exports.BaseTargetActor = BaseTargetActor;