summaryrefslogtreecommitdiffstats
path: root/devtools/shared/test-helpers/test-line-tracer.js
blob: a53b94bb489f65b96d9a7d9e8cdbdf6920f4ba1d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/* this source code form is subject to the terms of the mozilla public
 * license, v. 2.0. if a copy of the mpl was not distributed with this
 * file, you can obtain one at http://mozilla.org/mpl/2.0/. */

"use strict";

const { JSTracer } = ChromeUtils.importESModule(
  "resource://devtools/server/tracer/tracer.sys.mjs",
  { global: "shared" }
);

let lineToTrace;

const fileContents = new Map();

function getFileContent(url) {
  let content = fileContents.get(url);
  if (content) {
    return content;
  }
  content = readURI(url).split("\n");
  fileContents.set(url, content);
  return content;
}

function isNestedFrame(frame, topFrame) {
  if (frame.older) {
    // older will be a Debugger.Frame
    while ((frame = frame.older)) {
      if (frame == topFrame) {
        return true;
      }
    }
  } else if (frame.olderSavedFrame) {
    // olderSavedFrame will be a SavedStack object
    frame = frame.olderSavedFrame;
    const { lineNumber, columnNumber } = topFrame.script.getOffsetMetadata(
      top.offset
    );
    while ((frame = frame.parent || frame.asyncParent)) {
      if (
        frame.source == topFrame.script.source.url &&
        frame.line == lineNumber &&
        frame.column == columnNumber
      ) {
        return true;
      }
    }
  }
  return false;
}

// Store the top most frame running at `lineToTrace` line.
// We will then log all frames which are children of this top one.
let initialFrame = null;
let previousSourceUrl = null;

function traceFrame({ frame }) {
  const { script } = frame;
  const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
  if (lineToTrace) {
    if (lineNumber == lineToTrace) {
      // Stop the first tracer started from `exports.start()` which was only waiting for the particular test script line to run
      JSTracer.stopTracing();

      const { url } = script.source;
      const filename = url.substr(url.lastIndexOf("/") + 1);
      const line = getFileContent(url)[lineNumber - 1];
      logStep(`Start tracing ${filename} @ ${lineNumber} :: ${line}`);
      previousSourceUrl = url;
      // Restart a new tracer which would go track all the globals and not restrict to the test script.
      const tracerOptions = {
        // Ensure tracing all globals in this thread
        traceAllGlobals: true,
        // Ensure tracing each execution within functions (and not only function calls)
        traceSteps: true,
      };
      lineToTrace = null;
      JSTracer.startTracing(tracerOptions);
    }
    return false;
  }
  // We executed the test line we wanted to trace and now log all frames via a second tracer instance

  // First pick up the very first executed frame, so that we can trace all nested frame from this one.
  if (!initialFrame) {
    initialFrame = frame;
  } else if (initialFrame.terminated) {
    // If the traced top frame completed its execution, stop tracing.
    // Note that terminated will only be true once any possibly asynchronous work of the traced function
    // is done executing.
    logStep("End of execution");
    exports.stop();
    return false;
  } else if (!initialFrame.onStack) {
    // If we are done executing the traced Frame, it will be declared out of the stack.
    // By we should keep tracing as, if the traced Frame involves async work, it may be later restored onto the stack.
    return false;
  } else if (frame != initialFrame && !isNestedFrame(frame, initialFrame)) {
    // Then, only log frame which ultimately related to this first frame we picked.
    // Because of asynchronous calls and concurrent event loops, we may have in-between frames
    // that we ignore which relates to another event loop and another top frame.
    //
    // Note that the tracer may notify us about the exact same Frame object multiple times.
    // (its offset/location will change, but the object will be the same)
    return false;
  }

  const { url } = script.source;

  // Print the full source URL each time we start tracing a new source
  if (previousSourceUrl && previousSourceUrl !== url) {
    logStep("");
    logStep(url);
    // Log a grey line separator
    logStep(`\x1b[2m` + `\u2500`.repeat(url.length) + `\x1b[0m`);
    previousSourceUrl = url;
  }

  const line = getFileContent(url)[lineNumber - 1];
  // Grey out the beginning of the line, before frame's column,
  // and display an arrow before displaying the rest of the line.
  const code =
    "\x1b[2m" +
    line.substr(0, columnNumber - 1) +
    "\x1b[0m" +
    "\u21A6 " +
    line.substr(columnNumber - 1);

  const position = (lineNumber + ":" + columnNumber).padEnd(7);
  logStep(`${position} \u007C ${code}`);

  // Disable builtin tracer logging
  return false;
}

function logStep(message) {
  dump(` \x1b[2m[STEP]\x1b[0m ${message}\n`);
}

const tracingListener = {
  onTracingFrame: traceFrame,
  onTracingFrameStep: traceFrame,
};

exports.start = function (testGlobal, testUrl, line) {
  lineToTrace = line;
  const tracerOptions = {
    global: testGlobal,
    // Ensure tracing each execution within functions (and not only function calls)
    traceSteps: true,
    // Only trace the running test and nothing else
    filterFrameSourceUrl: testUrl,
  };
  JSTracer.startTracing(tracerOptions);
  JSTracer.addTracingListener(tracingListener);
};

exports.stop = function () {
  JSTracer.stopTracing();
  JSTracer.removeTracingListener(tracingListener);
};

function readURI(uri) {
  const { NetUtil } = ChromeUtils.importESModule(
    "resource://gre/modules/NetUtil.sys.mjs",
    { global: "contextual" }
  );
  const stream = NetUtil.newChannel({
    uri: NetUtil.newURI(uri, "UTF-8"),
    loadUsingSystemPrincipal: true,
  }).open();
  const count = stream.available();
  const data = NetUtil.readInputStreamToString(stream, count, {
    charset: "UTF-8",
  });

  stream.close();
  return data;
}