summaryrefslogtreecommitdiffstats
path: root/js/src/doc/Debugger/Tutorial-Alloc-Log-Tree.md
blob: 635b8eca1b95930f1233c9056fd1e98a1e8f89a7 (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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# Tutorial: Show Allocations Per Call Path

This page shows how to use the [`Debugger` API][debugger] to show how many
objects a web page allocates, sorted by the function call path that allocated
them.

1.  Visit the URL `about:config`, and set the `devtools.chrome.enabled`
    preference to `true`:

    ![Setting the 'devtools.chrome.enabled' preference][img-chrome-pref]

2.  Open a developer Scratchpad (Menu button > Developer > Scratchpad), and
    select "Browser" from the "Environment" menu. (This menu will not be
    present unless you have changed the preference as explained above.)

    ![Selecting the 'browser' context in the Scratchpad][img-scratchpad-browser]

3.  Enter the following code in the Scratchpad:

    ```js
    // This simply defines the 'Debugger' constructor in this
    // Scratchpad; it doesn't actually start debugging anything.
    const { addDebuggerToGlobal } = ChromeUtils.importESModule(
      'resource://gre/modules/jsdebugger.sys.mjs'
    );
    addDebuggerToGlobal(window);

    (function () {
      // The debugger we'll use to observe a tab's allocation.
      var dbg;

      // Start measuring the selected tab's main window's memory
      // consumption. This function is available in the browser
      // console.
      window.demoTrackAllocations = function() {
        dbg = new Debugger;

        // This makes hacking on the demo *much* more
        // pleasant.
        dbg.uncaughtExceptionHook = handleUncaughtException;

        // Find the current tab's main content window.
        var w = gBrowser.selectedBrowser.contentWindow;
        console.log("Tracking allocations in page: " +
                    w.location.href);

        // Make that window a debuggee of our Debugger.
        dbg.addDebuggee(w.wrappedJSObject);

        // Enable allocation tracking in dbg's debuggees.
        dbg.memory.trackingAllocationSites = true;
      }

      window.demoPlotAllocations = function() {
        // Grab the allocation log.
        var log = dbg.memory.drainAllocationsLog();

        // Neutralize the Debugger, and drop it on the floor
        // for the GC to collect.
        console.log("Stopping allocation tracking.");
        dbg.removeAllDebuggees();
        dbg = undefined;

        // Analyze and display the allocation log.
        plot(log);
      }

      function handleUncaughtException(ex) {
        console.log('Debugger hook threw:');
        console.log(ex.toString());
        console.log('Stack:');
        console.log(ex.stack);
      };

      function plot(log) {
        // Given the log, compute a map from allocation sites to
        // allocation counts. Note that stack entries are '===' if
        // they represent the same site with the same callers.
        var counts = new Map;
        for (let site of log) {
          // This is a kludge, necessary for now. The saved stacks
          // are new, and Firefox doesn't yet understand that they
          // are safe for chrome code to use, so we must tell it
          // so explicitly.
          site = Components.utils.waiveXrays(site.frame);

          if (!counts.has(site))
            counts.set(site, 0);
          counts.set(site, counts.get(site) + 1);
        }

        // Walk from each site that allocated something up to the
        // root, computing allocation totals that include
        // children. Remember that 'null' is a valid site,
        // representing the root.
        var totals = new Map;
        for (let [site, count] of counts) {
          for(;;) {
            if (!totals.has(site))
              totals.set(site, 0);
            totals.set(site, totals.get(site) + count);
            if (!site)
              break;
            site = site.parent;
          }
        }

        // Compute parent-to-child links, since saved stack frames
        // have only parent links.
        var rootChildren = new Map;
        function childMapFor(site) {
          if (!site)
            return rootChildren;

          let parentMap = childMapFor(site.parent);
          if (parentMap.has(site))
            return parentMap.get(site);

          var m = new Map;
          parentMap.set(site, m);
          return m;
        }
        for (let [site, total] of totals) {
          childMapFor(site);
        }

        // Print the allocation count for |site|. Print
        // |children|'s entries as |site|'s child nodes. Indent
        // the whole thing by |indent|.
        function walk(site, children, indent) {
          var name, place;
          if (site) {
            name = site.functionDisplayName;
            place = '  ' + site.source + ':' + site.line + ':' + site.column;
          } else {
            name = '(root)';
            place = '';
          }
          console.log(indent + totals.get(site) + ': ' + name + place);
          for (let [child, grandchildren] of children)
            walk(child, grandchildren, indent + '   ');
        }
        walk(null, rootChildren, '');
      }
    })();
    ```

4.  In the Scratchpad, ensure that no text is selected, and press the "Run"
    button. (If you get an error complaining that `Components.utils` is not
    defined, be sure you've selected `Browser` from the scratchpad's
    `Environment` menu, as described in step 2.)

5.  Save the following HTML text to a file, and visit the file in your browser.
    Make sure the current browser tab is displaying this page.

    ```html
    <div onclick="doDivsAndSpans()">
      Click here to make the page do some allocations.
    </div>

    <script>
      function makeFactory(type) {
        return function factory(content) {
          var elt = document.createElement(type);
          elt.textContent = content;
          return elt;
        };
      }

      var divFactory = makeFactory('div');
      var spanFactory = makeFactory('span');

      function divsAndSpans() {
        for (i = 0; i < 10; i++) {
          var div = divFactory('div #' + i);
          div.appendChild(spanFactory('span #' + i));
          document.body.appendChild(div);
        }
      }

      function doDivsAndSpans() { divsAndSpans(); }
    </script>
    ```

6.  Open the browser console (Menu Button > Developer > Browser Console), and
    then evaluate the expression `demoTrackAllocations()` in the browser
    console. This begins logging allocations in the current browser tab.

7.  In the browser tab, click on the text that says "Click here...". The event
    handler should add some text to the end of the page.

8.  Back in the browser console, evaluate the expression
    `demoPlotAllocations()`. This stops logging allocations, and displays a tree
    of allocations:

    ![An allocation plot, displayed in the console][img-alloc-plot]

    The numbers at the left edge of each line show the total number of objects
    allocated at that site or at sites called from there. After the count, we
    see the function name, and the source code location of the call site or
    allocation.

    The `(root)` node's count includes objects allocated in the content page by
    the web browser, like DOM events. Indeed, this display shows that
    `popup.xml` and `content.js`, which are internal components of Firefox,
    allocated more objects in the page's compartment than the page itself. (We
    will probably revise the allocation log to present such allocations in a way
    that is more informative, and that exposes less of Firefox's internal
    structure.)

    As expected, the `onclick` handler is responsible for all allocation done by
    the page's own code. (The line number for the onclick handler is `1`,
    indicating that the allocating call is located on line one of the handler
    text itself. We will probably change this to be the line number within
    `page.html`, not the line number within the handler code.)

    The `onclick` handler calls `doDivsAndSpans`, which calls `divsAndSpans`,
    which invokes closures of `factory` to do all the actual allocation. (It is
    unclear why `spanFactory` allocated thirteen objects, despite being called
    only ten times.)


[debugger]: Debugger-API.md
[img-chrome-pref]: enable-chrome-devtools.png
[img-scratchpad-browser]: scratchpad-browser-environment.png
[img-alloc-plot]: alloc-plot-console.png