summaryrefslogtreecommitdiffstats
path: root/devtools/shared/test-helpers/thread-helpers.sys.mjs
blob: b99fc16f0089bb1205261e51111e3e4489481516 (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
/* 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/. */

/**
 * Helper code to play with the javascript thread
 **/

function getSandboxWithDebuggerSymbol() {
  // Bug 1835268 - Changing this to an ES module import currently throws an
  // assertion in test_javascript_tracer.js in debug builds.
  const { addDebuggerToGlobal } = ChromeUtils.import(
    "resource://gre/modules/jsdebugger.jsm"
  );
  const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();

  const debuggerSandbox = Cu.Sandbox(systemPrincipal, {
    // This sandbox is also reused for ChromeDebugger implementation.
    // As we want to load the `Debugger` API for debugging chrome contexts,
    // we have to ensure loading it in a distinct compartment from its debuggee.
    freshCompartment: true,
    invisibleToDebugger: true,
  });
  addDebuggerToGlobal(debuggerSandbox);

  return debuggerSandbox;
}

/**
 * Implementation of a Javascript tracer logging traces to stdout.
 *
 * To be used like this:

  const { traceAllJSCalls } = ChromeUtils.importESModule(
    "resource://devtools/shared/test-helpers/thread-helpers.sys.mjs"
  );
  const jsTracer = traceAllJSCalls();
  [... execute some code to tracer ...]
  jsTracer.stop();

 * @param prefix String
 *        Optional, if passed, this will be displayed in front of each
 *        line reporting a new frame execution.
 * @param pause Number
 *        Optional, if passed, hold off each frame for `pause` ms,
 *        by letting the other event loops run in between.
 *        Be careful that it can introduce unexpected race conditions
 *        that can't necessarily be reproduced without this.
 */
export function traceAllJSCalls({ prefix = "", pause } = {}) {
  const debuggerSandbox = getSandboxWithDebuggerSymbol();

  debuggerSandbox.Services = Services;
  const f = Cu.evalInSandbox(
    "(" +
      function (pauseInMs, prefixString) {
        const dbg = new Debugger();
        // Add absolutely all the globals...
        dbg.addAllGlobalsAsDebuggees();
        // ...but avoid tracing this sandbox code
        const global = Cu.getGlobalForObject(this);
        dbg.removeDebuggee(global);

        // Add all globals created later on
        dbg.onNewGlobalObject = g => dbg.addDebuggee(g);

        function formatDisplayName(frame) {
          if (frame.type === "call") {
            const callee = frame.callee;
            return callee.name || callee.userDisplayName || callee.displayName;
          }

          return `(${frame.type})`;
        }

        function stop() {
          dbg.onEnterFrame = undefined;
          dbg.removeAllDebuggees();
        }
        global.stop = stop;

        let depth = 0;
        dbg.onEnterFrame = frame => {
          if (depth == 100) {
            dump(
              "Looks like an infinite loop? We stop the js tracer, but code may still be running!\n"
            );
            stop();
            return;
          }

          const { script } = frame;
          const { lineNumber, columnNumber } = script.getOffsetMetadata(
            frame.offset
          );
          const padding = new Array(depth).join(" ");
          dump(
            `${prefixString}${padding}--[${frame.implementation}]--> ${
              script.source.url
            } @ ${lineNumber}:${columnNumber} - ${formatDisplayName(frame)}\n`
          );

          depth++;
          frame.onPop = () => {
            depth--;
          };

          // Optionaly pause the frame execute by letting the other event loop to run in between.
          if (typeof pauseInMs == "number") {
            let freeze = true;
            const timer = Cc["@mozilla.org/timer;1"].createInstance(
              Ci.nsITimer
            );
            timer.initWithCallback(
              () => {
                freeze = false;
              },
              pauseInMs,
              Ci.nsITimer.TYPE_ONE_SHOT
            );
            Services.tm.spinEventLoopUntil("debugger-slow-motion", function () {
              return !freeze;
            });
          }
        };

        return { stop };
      } +
      ")",
    debuggerSandbox,
    undefined,
    "debugger-javascript-tracer",
    1,
    /* enforceFilenameRestrictions */ false
  );
  f(pause, prefix);

  return {
    stop() {
      debuggerSandbox.stop();
    },
  };
}