network-events-stacktraces.js (7285B)
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 loader.lazyRequireGetter( 8 this, 9 "ChannelEventSinkFactory", 10 "resource://devtools/server/actors/network-monitor/channel-event-sink.js", 11 true 12 ); 13 14 const lazy = {}; 15 16 ChromeUtils.defineESModuleGetters( 17 lazy, 18 { 19 NetworkUtils: 20 "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", 21 }, 22 { global: "contextual" } 23 ); 24 25 class NetworkEventStackTracesWatcher { 26 /** 27 * Start watching for all network event's stack traces related to a given Target actor. 28 * 29 * @param TargetActor targetActor 30 * The target actor from which we should observe the strack traces 31 * @param Object options 32 * Dictionary object with following attributes: 33 * - onAvailable: mandatory 34 * This will be called for each resource. 35 */ 36 async watch(targetActor, { onAvailable }) { 37 this.stacktraces = new Map(); 38 this.onStackTraceAvailable = onAvailable; 39 this.targetActor = targetActor; 40 41 Services.obs.addObserver(this, "http-on-opening-request"); 42 Services.obs.addObserver(this, "document-on-opening-request"); 43 Services.obs.addObserver(this, "network-monitor-alternate-stack"); 44 ChannelEventSinkFactory.getService().registerCollector(this); 45 } 46 47 /** 48 * Allows clearing of network stacktrace resources 49 */ 50 clear() { 51 this.stacktraces.clear(); 52 } 53 54 /** 55 * Stop watching for network event's strack traces related to a given Target Actor. 56 */ 57 destroy() { 58 this.clear(); 59 Services.obs.removeObserver(this, "http-on-opening-request"); 60 Services.obs.removeObserver(this, "document-on-opening-request"); 61 Services.obs.removeObserver(this, "network-monitor-alternate-stack"); 62 ChannelEventSinkFactory.getService().unregisterCollector(this); 63 } 64 65 onChannelRedirect(oldChannel, newChannel) { 66 // We can be called with any nsIChannel, but are interested only in HTTP channels 67 try { 68 oldChannel.QueryInterface(Ci.nsIHttpChannel); 69 newChannel.QueryInterface(Ci.nsIHttpChannel); 70 } catch (ex) { 71 return; 72 } 73 74 const oldId = oldChannel.channelId; 75 const stacktrace = this.stacktraces.get(oldId); 76 if (stacktrace) { 77 this._setStackTrace(newChannel.channelId, stacktrace); 78 } 79 } 80 81 observe(subject, topic, data) { 82 let channel, id; 83 try { 84 // We need to QI nsIHttpChannel in order to load the interface's 85 // methods / attributes for later code that could assume we are dealing 86 // with a nsIHttpChannel. 87 channel = subject.QueryInterface(Ci.nsIHttpChannel); 88 id = channel.channelId; 89 } catch (e1) { 90 try { 91 channel = subject.QueryInterface(Ci.nsIIdentChannel); 92 id = channel.channelId; 93 } catch (e2) { 94 // WebSocketChannels do not have IDs, so use the serial. When a WebSocket is 95 // opened in a content process, a channel is created locally but the HTTP 96 // channel for the connection lives entirely in the parent process. When 97 // the server code running in the parent sees that HTTP channel, it will 98 // look for the creation stack using the websocket's serial. 99 try { 100 channel = subject.QueryInterface(Ci.nsIWebSocketChannel); 101 id = channel.serial; 102 } catch (e3) { 103 // Try if the channel is a nsIWorkerChannelInfo which is the substitute 104 // of the channel in the parent process. 105 try { 106 channel = subject.QueryInterface(Ci.nsIWorkerChannelInfo); 107 id = channel.channelId; 108 } catch (e4) { 109 // Channels which don't implement the above interfaces can appear here, 110 // such as nsIFileChannel. Ignore these channels. 111 return; 112 } 113 } 114 } 115 } 116 117 if ( 118 !lazy.NetworkUtils.matchRequest(channel, { 119 targetActor: this.targetActor, 120 }) 121 ) { 122 return; 123 } 124 125 if (this.stacktraces.has(id)) { 126 // We can get up to two stack traces for the same channel: one each from 127 // the two observer topics we are listening to. Use the first stack trace 128 // which is specified, and ignore any later one. 129 return; 130 } 131 132 const stacktrace = []; 133 switch (topic) { 134 case "http-on-opening-request": 135 case "document-on-opening-request": { 136 // The channel is being opened on the main thread, associate the current 137 // stack with it. 138 // 139 // Convert the nsIStackFrame XPCOM objects to a nice JSON that can be 140 // passed around through message managers etc. 141 let frame = Components.stack; 142 if (frame?.caller) { 143 frame = frame.caller; 144 while (frame) { 145 stacktrace.push({ 146 filename: frame.filename, 147 lineNumber: frame.lineNumber, 148 columnNumber: frame.columnNumber, 149 functionName: frame.name, 150 asyncCause: frame.asyncCause, 151 }); 152 frame = frame.caller || frame.asyncCaller; 153 } 154 } 155 break; 156 } 157 case "network-monitor-alternate-stack": { 158 // An alternate stack trace is being specified for this channel. 159 // The topic data is the JSON for the saved frame stack we should use, 160 // so convert this into the expected format. 161 // 162 // This topic is used in the following cases: 163 // 164 // - The HTTP channel is opened asynchronously or on a different thread 165 // from the code which triggered its creation, in which case the stack 166 // from Components.stack will be empty. The alternate stack will be 167 // for the point we want to associate with the channel. 168 // 169 // - The channel is not a nsIHttpChannel, and we will receive no 170 // opening request notification for it. 171 let frame = JSON.parse(data); 172 while (frame) { 173 stacktrace.push({ 174 filename: frame.source, 175 lineNumber: frame.line, 176 columnNumber: frame.column, 177 functionName: frame.functionDisplayName, 178 asyncCause: frame.asyncCause, 179 }); 180 frame = frame.parent || frame.asyncParent; 181 } 182 break; 183 } 184 default: 185 throw new Error("Unexpected observe() topic"); 186 } 187 188 this._setStackTrace(id, stacktrace); 189 } 190 191 _setStackTrace(resourceId, stacktrace) { 192 const isBrowserToolBox = this.targetActor.sessionContext.type == "all"; 193 194 const filteredStacktrace = isBrowserToolBox 195 ? stacktrace 196 : lazy.NetworkUtils.removeChromeFrames(stacktrace); 197 198 this.stacktraces.set(resourceId, filteredStacktrace); 199 200 const stacktraceAvailable = 201 filteredStacktrace && !!filteredStacktrace.length; 202 203 this.onStackTraceAvailable([ 204 { 205 resourceId, 206 stacktraceAvailable, 207 lastFrame: stacktraceAvailable ? filteredStacktrace[0] : undefined, 208 }, 209 ]); 210 } 211 212 getStackTrace(id) { 213 let stacktrace = []; 214 if (this.stacktraces.has(id)) { 215 stacktrace = this.stacktraces.get(id); 216 } 217 return stacktrace; 218 } 219 } 220 module.exports = NetworkEventStackTracesWatcher;