test-line-tracer.js (5846B)
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 { JSTracer } = ChromeUtils.importESModule( 8 "resource://devtools/server/tracer/tracer.sys.mjs", 9 { global: "shared" } 10 ); 11 12 let lineToTrace; 13 14 const fileContents = new Map(); 15 16 function getFileContent(url) { 17 let content = fileContents.get(url); 18 if (content) { 19 return content; 20 } 21 content = readURI(url).split("\n"); 22 fileContents.set(url, content); 23 return content; 24 } 25 26 function isNestedFrame(frame, topFrame) { 27 if (frame.older) { 28 // older will be a Debugger.Frame 29 while ((frame = frame.older)) { 30 if (frame == topFrame) { 31 return true; 32 } 33 } 34 } else if (frame.olderSavedFrame) { 35 // olderSavedFrame will be a SavedStack object 36 frame = frame.olderSavedFrame; 37 const { lineNumber, columnNumber } = topFrame.script.getOffsetMetadata( 38 top.offset 39 ); 40 while ((frame = frame.parent || frame.asyncParent)) { 41 if ( 42 frame.source == topFrame.script.source.url && 43 frame.line == lineNumber && 44 frame.column == columnNumber 45 ) { 46 return true; 47 } 48 } 49 } 50 return false; 51 } 52 53 // Store the top most frame running at `lineToTrace` line. 54 // We will then log all frames which are children of this top one. 55 let initialFrame = null; 56 let previousSourceUrl = null; 57 58 function traceFrame({ frame }) { 59 const { script } = frame; 60 const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset); 61 if (lineToTrace) { 62 if (lineNumber == lineToTrace) { 63 // Stop the first tracer started from `exports.start()` which was only waiting for the particular test script line to run 64 JSTracer.stopTracing(); 65 66 const { url } = script.source; 67 const filename = url.substr(url.lastIndexOf("/") + 1); 68 const line = getFileContent(url)[lineNumber - 1]; 69 logStep(`Start tracing ${filename} @ ${lineNumber} :: ${line}`); 70 previousSourceUrl = url; 71 // Restart a new tracer which would go track all the globals and not restrict to the test script. 72 const tracerOptions = { 73 // Ensure tracing all globals in this thread 74 traceAllGlobals: true, 75 // Ensure tracing each execution within functions (and not only function calls) 76 traceSteps: true, 77 }; 78 lineToTrace = null; 79 JSTracer.startTracing(tracerOptions); 80 } 81 return false; 82 } 83 // We executed the test line we wanted to trace and now log all frames via a second tracer instance 84 85 // First pick up the very first executed frame, so that we can trace all nested frame from this one. 86 if (!initialFrame) { 87 initialFrame = frame; 88 } else if (initialFrame.terminated) { 89 // If the traced top frame completed its execution, stop tracing. 90 // Note that terminated will only be true once any possibly asynchronous work of the traced function 91 // is done executing. 92 logStep("End of execution"); 93 exports.stop(); 94 return false; 95 } else if (!initialFrame.onStack) { 96 // If we are done executing the traced Frame, it will be declared out of the stack. 97 // By we should keep tracing as, if the traced Frame involves async work, it may be later restored onto the stack. 98 return false; 99 } else if (frame != initialFrame && !isNestedFrame(frame, initialFrame)) { 100 // Then, only log frame which ultimately related to this first frame we picked. 101 // Because of asynchronous calls and concurrent event loops, we may have in-between frames 102 // that we ignore which relates to another event loop and another top frame. 103 // 104 // Note that the tracer may notify us about the exact same Frame object multiple times. 105 // (its offset/location will change, but the object will be the same) 106 return false; 107 } 108 109 const { url } = script.source; 110 111 // Print the full source URL each time we start tracing a new source 112 if (previousSourceUrl && previousSourceUrl !== url) { 113 logStep(""); 114 logStep(url); 115 // Log a grey line separator 116 logStep(`\x1b[2m` + `\u2500`.repeat(url.length) + `\x1b[0m`); 117 previousSourceUrl = url; 118 } 119 120 const line = getFileContent(url)[lineNumber - 1]; 121 // Grey out the beginning of the line, before frame's column, 122 // and display an arrow before displaying the rest of the line. 123 const code = 124 "\x1b[2m" + 125 line.substr(0, columnNumber - 1) + 126 "\x1b[0m" + 127 "\u21A6 " + 128 line.substr(columnNumber - 1); 129 130 const position = (lineNumber + ":" + columnNumber).padEnd(7); 131 logStep(`${position} \u007C ${code}`); 132 133 // Disable builtin tracer logging 134 return false; 135 } 136 137 function logStep(message) { 138 dump(` \x1b[2m[STEP]\x1b[0m ${message}\n`); 139 } 140 141 const tracingListener = { 142 onTracingFrame: traceFrame, 143 onTracingFrameStep: traceFrame, 144 }; 145 146 exports.start = function (testGlobal, testUrl, line) { 147 lineToTrace = line; 148 const tracerOptions = { 149 global: testGlobal, 150 // Ensure tracing each execution within functions (and not only function calls) 151 traceSteps: true, 152 // Only trace the running test and nothing else 153 filterFrameSourceUrl: testUrl, 154 }; 155 JSTracer.startTracing(tracerOptions); 156 JSTracer.addTracingListener(tracingListener); 157 }; 158 159 exports.stop = function () { 160 JSTracer.stopTracing(); 161 JSTracer.removeTracingListener(tracingListener); 162 }; 163 164 function readURI(uri) { 165 const { NetUtil } = ChromeUtils.importESModule( 166 "resource://gre/modules/NetUtil.sys.mjs", 167 { global: "contextual" } 168 ); 169 const stream = NetUtil.newChannel({ 170 uri: NetUtil.newURI(uri, "UTF-8"), 171 loadUsingSystemPrincipal: true, 172 }).open(); 173 const count = stream.available(); 174 const data = NetUtil.readInputStreamToString(stream, count, { 175 charset: "UTF-8", 176 }); 177 178 stream.close(); 179 return data; 180 }