Commit f0d56e78 authored by Philip Chimento's avatar Philip Chimento 🚮

Merge branch 'heapgraph-scripts' into 'master'

Heapgraph scripts

See merge request !118
parents d1f01763 1f6e68cc
Pipeline #8500 failed with stages
in 14 minutes and 29 seconds
#!/usr/bin/env python3
# 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/.
#
# heapdot.py - DOT Graph output
import re
func_regex = re.compile('Function(?: ([^/]+)(?:/([<|\w]+))?)?')
gobj_regex = re.compile('([^ ]+) (\(nil\)|0x[a-fA-F0-9]+$)')
###############################################################################
# DOT Graph Output
###############################################################################
dot_graph_paths = []
def add_dot_graph_path(path):
dot_graph_paths.append(path)
def output_dot_file(args, graph, targs, fname):
# build the set of nodes
nodes = set([])
for p in dot_graph_paths:
for x in p:
nodes.add(x)
# build the edge map
edges = {}
for p in dot_graph_paths:
prevNode = None
for x in p:
if prevNode:
edges.setdefault(prevNode, set([])).add(x)
prevNode = x
# Write out the DOT graph
outf = open(fname, 'w')
outf.write('digraph {\n')
# Nodes
for addr in nodes:
label = graph.node_labels.get(addr, '')
color = 'black'
style = 'solid'
shape = 'rect'
native = ''
if label.endswith('<no private>'):
label = label[:-13]
# Lookup the edge label for this node
elabel = ''
for origin in graph.edge_labels.values():
if addr in origin:
elabels = origin[addr]
elabel = elabels[0]
break
# GObject or something else with a native address
gm = gobj_regex.match(label)
if gm:
label = gm.group(1)
color = 'orange'
style = 'bold'
if not args.no_addr:
native = gm.group(2)
# Some kind of GObject
if label.startswith('GObject_'):
shape = 'circle'
if elabel in ['prototype', 'group_proto']:
style += ',dashed'
# Another object native to Gjs
elif label.startswith('Gjs') or label.startswith('GIR'):
shape = 'octagon'
elif label.startswith('Function'):
fm = func_regex.match(label)
if fm.group(2) == '<':
label = 'Function via {}()'.format(fm.group(1))
elif fm.group(2):
label = 'Function {} in {}'.format(fm.group(2), fm.group(1))
else:
if len(label) > 10:
label = label[9:]
label += '()'
color = 'green'
style = 'bold,rounded'
# A function context
elif label == 'Call' or label == 'LexicalEnvironment':
color = 'green'
style = 'bold,dashed'
# A file/script reference
elif label.startswith('script'):
label = label[7:].split('/')[-1]
shape = 'note'
color = 'blue'
# A WeakMap
elif label.startswith('WeakMap'):
label = 'WeakMap'
style = 'dashed'
# Mostly uninteresting objects
elif label in ['base_shape', 'object_group', 'type_object']:
style = 'dotted'
if label == 'base_shape':
label = 'shape'
elif label == 'type_object':
label = 'type'
# Only mark the target if it's a single match
if addr == targs[0] and len(targs) == 1:
color = 'red'
style = 'bold'
if args.no_addr:
outf.write(' node [label="{0}", color={1}, shape={2}, style="{3}"] q{4};\n'.format(label, color, shape, style, addr))
else:
if native:
outf.write(' node [label="{0}\\njsobj@{4}\\nnative@{5}", color={1}, shape={2}, style="{3}"] q{4};\n'.format(label, color, shape, style, addr, native))
else:
outf.write(' node [label="{0}\\njsobj@{4}", color={1}, shape={2}, style="{3}"] q{4};\n'.format(label, color, shape, style, addr))
# Edges (relationships)
for origin, destinations in edges.items():
for destination in destinations:
labels = graph.edge_labels.get(origin, {}).get(destination, [])
ll = []
for l in labels:
if len(l) == 2:
l = l[0]
if l.startswith('**UNKNOWN SLOT '):
continue
ll.append(l)
label = ''
style = 'solid'
color = 'black'
if len(ll) == 1:
label = ll[0]
# Object children
if label.startswith('objects['):
label = label[7:]
# Array elements
elif label.startswith('objectElements['):
label = label[14:]
# prototype/constructor function
elif label in ['prototype', 'group_proto']:
color = 'orange'
style = 'bold,dashed'
# fun_environment
elif label == 'fun_environment':
label = ''
color = 'green'
style = 'bold,dashed'
elif label == 'script':
label = ''
color = 'blue'
# Signals
# TODO: better heap label via gi/closure.cpp & gi/object.cpp
elif label == 'signal connection':
color = 'red'
style = 'bold,dashed'
if len(label) > 18:
label = label[:8] + '...' + label[-8:]
else:
label = ',\\n'.join(ll)
outf.write(' q{0} -> q{1} [label="{2}", color={3}, style="{4}"];\n'.format(origin, destination, label, color, style))
outf.write('}\n')
outf.close()
# gjs-heapgraph
A heap analyzer for Gjs based on https://github.com/amccreight/heapgraph to aid
in debugging and plugging memory leaks.
## Resource Usage
Be aware that parsing a heap can take a fair amount of RAM depending on the
heap size and time depending on the amount of target objects and path length.
Examples of approximate memory and time required to build DOT graphs on an
IvyBridge i7:
| Heap Size | RAM | Targets | Time |
|-----------|-------|---------|-------------|
| 5MB | 80MB | 1500 | 1.5 Minutes |
| 30MB | 425MB | 7700 | 40 Minutes |
## Basic Usage
### Getting a Heap Dump
The more convenient way to dump a heap is to send `SIGUSR1` to a GJS process
with the env variable `GJS_DEBUG_HEAP_OUTPUT` set:
```sh
$ GJS_DEBUG_HEAP_OUTPUT=myApp.heap gjs myApp.js &
$ kill -USR1 <gjs-pid>
```
It's also possible to dump a heap from within a script via the `System` import:
```js
const System = imports.system;
// Dumping the heap before the "leak" has happened
System.dumpHeap('/home/user/myApp1.heap.');
// Code presumably resulting in a leak...
// Running the garbage collector before dumping can avoid some false positives
System.gc();
// Dumping the heap after the "leak" has happened
System.dumpHeap('/home/user/myApp2.heap.');
```
### Output
The default output of `./heapgraph.py` is a tiered tree of paths from root to
rooted objects. If the output is being sent to a terminal (TTY) some minimal
ANSI styling is used to make the output more readable. Additionally, anything
that isn't part of the graph will be sent to `stderr` so the output can be
directed to a file as plain text. Below is a snippet:
```sh
$ ./heapgraph.py myApp2.heap Object > myApp2.tree
Parsing file.heap...done
Found 343 targets with type "Object"
$ cat file.tree
├─[vm_stack[1]]─➤ [Object jsobj@0x7fce60683440]
├─[vm_stack[1]]─➤ [Object jsobj@0x7fce606833c0]
├─[exact-Object]─➤ [Object jsobj@0x7fce60683380]
├─[exact-Object]─➤ [GjsGlobal jsobj@0x7fce60680060]
│ ├─[Debugger]─➤ [Function Debugger jsobj@0x7fce606a4540]
│ │ ╰─[Object]─➤ [Function Object jsobj@0x7fce606a9cc0]
│ │ ╰─[prototype]─➤ [Object (nil) jsobj@0x7fce60681160]
│ │
...and so on
```
`heapgraph.py` can also output DOT graphs that can be a useful way to visualize
the heap graph, especially if you don't know exactly what you're looking for.
Passing the `--dot-graph` option will output a DOT graph to `<input-file>.dot`
in the current working directory.
There are a few choices for viewing dot graphs, and many utilities for
converting them to other formats like PDF, Tex or GraphML. For Gnome desktops
[`xdot`](https://github.com/jrfonseca/xdot.py) is a nice lightweight
Python/Cairo viewer available on PyPi and in most distributions.
```sh
$ ./heapgraph.py --dot-graph /home/user/myApp2.heap Object
Parsing file.heap...done
Found 343 targets with type "Object"
$ xdot myApp2.heap.dot
```
### Excluding Nodes from the Graph
The exclusion switch you are most likely to use is `--diff-heap` which will
exclude all nodes in the graph common to that heap, allowing you to easily
see what's not being collected between two states.
```sh
$ ./heapgraph --diff-heap myApp1.heap myApp2.heap GObject
```
You can also exclude Gray Roots, WeakMaps, nodes with a heap address or nodes
with labels containing a string. Because GObject addresses are part of the node
label, these can be excluded with `--hide-node` as well.
By default the global object (GjsGlobal aka `window`), imports (GjsModule,
GjsFileImporter), and namespaces (GIRepositoryNamespace) aren't shown in the
graph since these are less useful and can't be garbage collected anyways.
```sh
$ ./heapgraph.py --hide-addr 0x7f6ef022c060 \
--hide-node 'self-hosting-global' \
--no-gray-roots \
/home/user/myApp2.heap Object
$ ./heapgraph.py --hide-node 0x55e93cf5deb0 /home/user/myApp2.heap Object
```
### Command-Line Arguments
> **NOTE:** Command line arguments are subject to change; Check
> `./heapgraph.py --help` before running.
```
usage: heapgraph.py [-h] [--edge | --function | --string] [--count]
[--dot-graph] [--no-addr] [--diff-heap FILE]
[--no-gray-roots] [--no-weak-maps] [--show-global]
[--show-imports] [--hide-addr ADDR] [--hide-node LABEL]
FILE TARGET
Find what is rooting or preventing an object from being collected in a GJS
heap using a shortest-path breadth-first algorithm.
positional arguments:
FILE Garbage collector heap from System.dumpHeap()
TARGET Heap address (eg. 0x7fa814054d00) or type prefix (eg.
Array, Object, GObject, Function...)
optional arguments:
-h, --help show this help message and exit
--edge, -e Treat TARGET as a function name
--function, -f Treat TARGET as a function name
--string, -s Treat TARGET as a string literal or String()
Output Options:
--count, -c Only count the matches for TARGET
--dot-graph, -d Output a DOT graph to FILE.dot
--no-addr, -na Don't show addresses
Node/Root Filtering:
--diff-heap FILE, -dh FILE
Don't show roots common to the heap FILE
--no-gray-roots, -ng Don't show gray roots (marked to be collected)
--no-weak-maps, -nwm Don't show WeakMaps
--show-global, -g Show the global object (eg. window/GjsGlobal)
--show-imports, -i Show import and module nodes (eg. imports.foo)
--hide-addr ADDR, -ha ADDR
Don't show roots with the heap address ADDR
--hide-node LABEL, -hn LABEL
Don't show nodes with labels containing LABEL
```
## See Also
Below are some links to information relevant to SpiderMonkey garbage collection
and heap parsing:
* [GC.cpp Comments](https://searchfox.org/mozilla-central/source/js/src/gc/GC.cpp)
* [How JavaScript Objects Are Implemented](https://www.infoq.com/presentations/javascript-objects-spidermonkey)
* [Tracing garbage collection](https://en.wikipedia.org/wiki/Tracing_garbage_collection#Tri-color_marking) on Wikipedia
* [SpiderMonkey Memory](https://gitlab.gnome.org/GNOME/gjs/blob/master/doc/SpiderMonkey_Memory.md) via GJS Repo
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment