...
 
Commits (23)
......@@ -70,7 +70,7 @@ fedora:
<<: *build
when: always
stage: test
image: claudioandre/spidermonkey:fedora.dev.gcc
image: claudioandre/spidermonkey:job-400.5 # temporarily pinned to old tag
variables:
CC: gcc
DEV: devel
......@@ -110,7 +110,7 @@ fedora_clang:
no_profiler:
<<: *build
stage: thorough_tests
image: claudioandre/spidermonkey:fedora.dev.gcc
image: claudioandre/spidermonkey:job-400.5 # temporarily pinned to old tag
variables:
CC: gcc
DEV: devel
......@@ -140,7 +140,7 @@ coverage:
sanitizer_gcc:
<<: *build
stage: thorough_tests
image: claudioandre/spidermonkey:fedora.dev.gcc
image: claudioandre/spidermonkey:job-400.5 # temporarily pinned to old tag
variables:
CC: gcc
DEV: devel
......@@ -223,7 +223,7 @@ pages:
valgrind:
<<: *build
stage: thorough_tests
image: claudioandre/spidermonkey:fedora.dev.gcc
image: claudioandre/spidermonkey:job-400.5 # temporarily pinned to old tag
variables:
CC: gcc
DEV: devel
......@@ -234,7 +234,7 @@ valgrind:
no_graphics:
<<: *build
stage: thorough_tests
image: claudioandre/spidermonkey:fedora.dev.gcc
image: claudioandre/spidermonkey:job-400.5 # temporarily pinned to old tag
variables:
CC: gcc
DEV: devel
......
Version 1.52.1
--------------
- This version has more changes than would normally be expected from a stable
version. The intention of 1.52.1 is to deliver a version that runs cleaner
under performance tools, in time for the upcoming GNOME Shell performance
hackfest. We also wanted to deliver a stable CI pipeline before branching
GNOME 3.28 off of master.
- Claudio André's work on the CI pipeline deserves a spotlight. We now have
test jobs that run linters, sanitizers, Valgrind, and more; the tests are
run on up-to-date Docker images; and the reliability errors that were plaguing
the test runs are solved.
- In addition to System.dumpHeap(), you can now dump a heap from a running
Javascript program by starting it with the environment variable
GJS_DEBUG_HEAP_OUTPUT=some_name, and sending it SIGUSR1.
- heapgraph.py is a tool in the repository (not installed in distributions) for
analyzing and graphing heap dumps, to aid with tracking down memory leaks.
- The linter CI jobs will compare your branch against GNOME/gjs@master, and fail
if your branch added any new linter errors. There may be false positives, and
the rules configuration is not perfect. If that's the case on your merge
request, you can skip the appropriate linter job by adding the text
"[skip (linter)]" in your commit message: e.g., "[skip cpplint]".
- We welcomed first merge requests from several new contributors for this
release.
- Closed bugs and merge requests:
* Crash when resolving promises if exception is pending [#18, !95, Philip
Chimento]
* gjs_byte_array_get_proto(JSContext*): assertion failed: (((void) "gjs_"
"byte_array" "_define_proto() must be called before " "gjs_" "byte_array"
"_get_proto()", !v_proto.isUndefined())) [#39, !92, Philip Chimento]
* Tools for examining heap graph [#116, !61, !118, Andy Holmes, Tommi
Komulainen, Philip Chimento]
* Run analysis tools to prepare for release [#120, !88, Philip Chimento]
* Add support for passing flags to Gio.DBusProxy in makeProxyWrapper [#122,
!81, Florian Müllner]
* Cannot instantiate Cairo.Context [#126, !91, Philip Chimento]
* GISCAN GjsPrivate-1.0.gir fails [#128, !90, Philip Chimento]
* Invalid read of g_object_finalized flag [#129, !117, Philip Chimento]
* Fix race condition in coverage file test [#130, !99, Philip Chimento]
* Linter jobs should only fail if new lint errors were added [#133, !94,
Philip Chimento]
* Disable all tests that depends on X if there is no XServer [#135, !109,
Claudio André]
* Pick a different C++ linter [#137, !102, Philip Chimento]
* Create a CI test that builds using autotools only [!74, Claudio André]
* CI: enable ASAN [!89, Claudio André]
* CI: disable static analysis jobs using the commit message [!93, Claudio
André]
* profiler: Don't assume layout of struct sigaction [!96, James Cowgill]
* Valgrind [!98, Claudio André]
* Robustness of CI [!103, Claudio André]
* CI: make a separate job for installed tests [!106, Claudio André]
* Corrected Markdown format and added links to JHBuild in setup guide for GJS
[!111, Avi Zajac]
* Update tweener.js -- 48 eslint errors fixed [!112, Karen Medina]
* Various maintenance [!100, !104, !105, !107, !110, !113, !116, Claudio
André, Philip Chimento]
Version 1.52.0
--------------
......
[![Build Status](https://gitlab.gnome.org/GNOME/gjs/badges/master/build.svg)](https://gitlab.gnome.org/GNOME/gjs/pipelines)
[![coverage report](https://gitlab.gnome.org/GNOME/gjs/badges/master/coverage.svg)](https://gitlab.gnome.org/GNOME/gjs/-/jobs)
[![coverage report](https://gitlab.gnome.org/GNOME/gjs/badges/master/coverage.svg)](https://gnome.pages.gitlab.gnome.org/gjs/)
[![License](https://img.shields.io/badge/License-LGPL%20v2%2B-blue.svg)](https://gitlab.gnome.org/GNOME/gjs/blob/master/COPYING)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitlab.gnome.org/GNOME/gjs/blob/master/COPYING)
......
......@@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
m4_define(pkg_major_version, 1)
m4_define(pkg_minor_version, 52)
m4_define(pkg_minor_version, 53)
m4_define(pkg_micro_version, 1)
m4_define(pkg_version, pkg_major_version.pkg_minor_version.pkg_micro_version)
m4_define(pkg_int_version, (pkg_major_version * 100 + pkg_minor_version) * 100 + pkg_micro_version)
......
......@@ -945,7 +945,6 @@ object_instance_props_to_g_parameters(JSContext *context,
return true;
}
#define DEBUG_DISPOSE 0
static void
wrapped_gobj_dispose_notify(gpointer data,
GObject *where_the_object_was)
......@@ -954,9 +953,8 @@ wrapped_gobj_dispose_notify(gpointer data,
priv->g_object_finalized = true;
wrapped_gobject_list.erase(priv);
#if DEBUG_DISPOSE
gjs_debug(GJS_DEBUG_GOBJECT, "Wrapped GObject %p disposed", where_the_object_was);
#endif
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Wrapped GObject %p disposed",
where_the_object_was);
}
static void
......@@ -967,9 +965,10 @@ gobj_no_longer_kept_alive_func(JS::HandleObject obj,
priv = (ObjectInstance *) data;
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
"GObject wrapper %p will no longer be kept alive, eligible for collection",
obj.get());
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "GObject wrapper %p for GObject "
"%p (%s) was rooted but is now unrooted due to "
"GjsContext dispose", obj.get(), priv->gobj,
G_OBJECT_TYPE_NAME(priv->gobj));
priv->keep_alive.reset();
wrapped_gobject_list.erase(priv);
......@@ -980,15 +979,15 @@ handle_toggle_down(GObject *gobj)
{
ObjectInstance *priv = get_object_qdata(gobj);
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
"Toggle notify gobj %p obj %p is_last_ref true",
gobj, priv->keep_alive.get());
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Toggle notify DOWN for GObject "
"%p (%s), JS obj %p", gobj, G_OBJECT_TYPE_NAME(gobj),
priv->keep_alive.get());
/* Change to weak ref so the wrapper-wrappee pair can be
* collected by the GC
*/
if (priv->keep_alive.rooted()) {
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Removing object from keep alive");
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Unrooting object");
priv->keep_alive.switch_to_unrooted();
}
}
......@@ -1005,9 +1004,9 @@ handle_toggle_up(GObject *gobj)
if (!priv->keep_alive) /* Object already GC'd */
return;
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
"Toggle notify gobj %p obj %p is_last_ref false",
gobj, priv->keep_alive.get());
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Toggle notify UP for GObject "
"%p (%s), JS obj %p", gobj, G_OBJECT_TYPE_NAME(gobj),
priv->keep_alive.get());
/* Change to strong ref so the wrappee keeps the wrapper alive
* in case the wrapper has data in it that the app cares about
......@@ -1016,7 +1015,7 @@ handle_toggle_up(GObject *gobj)
/* FIXME: thread the context through somehow. Maybe by looking up
* the compartment that obj belongs to. */
GjsContext *context = gjs_context_get_current();
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Adding object to keep alive");
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Rooting object");
auto cx = static_cast<JSContext *>(gjs_context_get_native_context(context));
priv->keep_alive.switch_to_rooted(cx, gobj_no_longer_kept_alive_func, priv);
}
......@@ -1187,10 +1186,6 @@ init_object_private (JSContext *context,
g_assert(priv_from_js(context, object) == NULL);
JS_SetPrivate(object, priv);
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
"obj instance constructor, obj %p priv %p",
object.get(), priv);
proto_priv = proto_priv_from_js(context, object);
g_assert(proto_priv != NULL);
......@@ -1199,6 +1194,10 @@ init_object_private (JSContext *context,
if (priv->info)
g_base_info_ref( (GIBaseInfo*) priv->info);
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Instance constructor of %s, "
"JS obj %p, priv %p", g_type_name(priv->gtype),
object.get(), priv);
JS_EndRequest(context);
return priv;
}
......@@ -1208,6 +1207,10 @@ update_heap_wrapper_weak_pointers(JSContext *cx,
JSCompartment *compartment,
gpointer data)
{
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Weak pointer update callback, "
"%zu wrapped GObject(s) to examine",
wrapped_gobject_list.size());
std::vector<GObject *> to_be_disassociated;
for (auto iter = wrapped_gobject_list.begin(); iter != wrapped_gobject_list.end(); ) {
......@@ -1221,6 +1224,10 @@ update_heap_wrapper_weak_pointers(JSContext *cx,
* the weak pointer list first, since the disassociation
* may also cause it to be erased.)
*/
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Found GObject weak pointer "
"whose JS object %p is about to be finalized: "
"%p (%s)", priv->keep_alive.get(), priv->gobj,
G_OBJECT_TYPE_NAME(priv->gobj));
to_be_disassociated.push_back(priv->gobj);
iter = wrapped_gobject_list.erase(iter);
}
......@@ -1409,9 +1416,8 @@ G_GNUC_END_IGNORE_DEPRECATIONS
*/
g_object_unref(gobj);
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
"JSObject created with GObject %p %s",
priv->gobj, g_type_name_from_instance((GTypeInstance*) priv->gobj));
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "JSObject created with GObject %p (%s)",
priv->gobj, G_OBJECT_TYPE_NAME(priv->gobj));
TRACE(GJS_OBJECT_PROXY_NEW(priv, priv->gobj,
priv->info ? g_base_info_get_namespace((GIBaseInfo*) priv->info) : "_gjs_private",
......@@ -1485,13 +1491,10 @@ object_instance_finalize(JSFreeOp *fop,
ObjectInstance *priv;
priv = (ObjectInstance *) JS_GetPrivate(obj);
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
"finalize obj %p priv %p gtype %s gobj %p", obj, priv,
(priv && priv->gobj) ?
g_type_name_from_instance( (GTypeInstance*) priv->gobj) :
"<no gobject>",
priv ? priv->gobj : NULL);
g_assert (priv != NULL);
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
"Finalizing %s, JS obj %p, priv %p, GObject %p",
g_type_name(priv->gtype), obj, priv, priv->gobj);
TRACE(GJS_OBJECT_PROXY_FINALIZE(priv, priv->gobj,
priv->info ? g_base_info_get_namespace((GIBaseInfo*) priv->info) : "_gjs_private",
......@@ -1520,7 +1523,9 @@ object_instance_finalize(JSFreeOp *fop,
priv->info ? g_base_info_get_namespace((GIBaseInfo*) priv->info) : "",
priv->info ? g_base_info_get_name((GIBaseInfo*) priv->info) : g_type_name(priv->gtype));
}
if (!priv->g_object_finalized)
g_object_weak_unref(priv->gobj, wrapped_gobj_dispose_notify, priv);
release_native_object(priv);
}
......@@ -1532,8 +1537,7 @@ object_instance_finalize(JSFreeOp *fop,
gjs_debug(GJS_DEBUG_GOBJECT,
"Wrapper was finalized despite being kept alive, has refcount >1");
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT,
"Removing from keep alive");
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "Unrooting object");
priv->keep_alive.reset();
}
......@@ -2072,9 +2076,9 @@ gjs_define_object_class(JSContext *context,
priv->klass = (GTypeClass*) g_type_class_ref (gtype);
JS_SetPrivate(prototype, priv);
gjs_debug(GJS_DEBUG_GOBJECT, "Defined class %s prototype %p class %p in object %p",
constructor_name, prototype.get(), JS_GetClass(prototype),
in_object.get());
gjs_debug(GJS_DEBUG_GOBJECT, "Defined class for %s (%s), prototype %p, "
"JSClass %p, in object %p", constructor_name, g_type_name(gtype),
prototype.get(), JS_GetClass(prototype), in_object.get());
if (info)
gjs_object_define_static_methods(context, constructor, gtype, info);
......
......@@ -86,6 +86,12 @@ ToggleQueue::cancel(GObject *gobj)
std::lock_guard<std::mutex> hold(lock);
bool had_toggle_down = find_and_erase_operation_locked(gobj, DOWN);
bool had_toggle_up = find_and_erase_operation_locked(gobj, UP);
gjs_debug_lifecycle(GJS_DEBUG_GOBJECT, "ToggleQueue: %p (%s) was %s", gobj,
G_OBJECT_TYPE_NAME(gobj),
had_toggle_down && had_toggle_up ? "queued to toggle BOTH"
: had_toggle_down ? "queued to toggle DOWN"
: had_toggle_up ? "queued to toggle UP"
: "not queued");
return {had_toggle_down, had_toggle_up};
}
......
......@@ -37,6 +37,10 @@
#include <windows.h>
#endif
#ifdef ENABLE_CAIRO
# include <cairo.h>
#endif
/* Implementations of locale-specific operations; these are used
* in the implementation of String.localeCompare(), Date.toLocaleDateString(),
* and so forth. We take the straight-forward approach of converting
......@@ -214,6 +218,16 @@ on_promise_unhandled_rejection(JSContext *cx,
std::move(stack));
}
static void
shutdown(void)
{
JS_ShutDown();
#ifdef ENABLE_CAIRO
cairo_debug_reset_static_data(); /* for valgrind reports */
#endif
}
#ifdef G_OS_WIN32
HMODULE gjs_dll;
static bool gjs_is_inited = false;
......@@ -231,7 +245,7 @@ LPVOID lpvReserved)
break;
case DLL_THREAD_DETACH:
JS_ShutDown ();
shutdown();
break;
default:
......@@ -251,7 +265,7 @@ public:
}
~GjsInit() {
JS_ShutDown();
shutdown();
}
operator bool() {
......
......@@ -133,13 +133,10 @@
match-leak-kinds: definite
fun:malloc
...
fun:FcPatternDuplicate
fun:_cairo_ft_font_face_create_for_pattern
fun:_cairo_ft_font_face_create_for_toy
fun:_cairo_toy_font_face_create_impl_face
fun:_cairo_toy_font_face_init
fun:cairo_toy_font_face_create
fun:_cairo_gstate_ensure_font_face
fun:FcConfigSubstituteWithPat
fun:_cairo_ft_resolve_pattern
fun:_cairo_ft_font_face_get_implementation
fun:cairo_scaled_font_create
fun:_cairo_gstate_ensure_scaled_font
fun:_cairo_gstate_get_scaled_font
fun:_cairo_default_context_get_scaled_font
......
......@@ -406,4 +406,26 @@ describe('Tweener', function () {
expect(overwrite).toHaveBeenCalledTimes(1);
expect(complete).toHaveBeenCalledTimes(1);
});
it('stays within min and max values', function () {
var objectA = {
x: 0,
y: 0
};
var objectB = {
x: 0,
y: 0
};
Tweener.addTween(objectA, { x: 300, y: 300, time: 1, max: 255, transition: "linear" });
Tweener.addTween(objectB, { x: -200, y: -200, time: 1, delay: 0.5, min: 0, transition: "linear" });
jasmine.clock().tick(1001);
expect(objectA.x).toEqual(255);
expect(objectA.y).toEqual(255);
expect(objectB.x).toEqual(0);
expect(objectB.y).toEqual(0);
});
});
......@@ -88,6 +88,8 @@ TweenList.prototype = {
tween.onErrorScope = this.onErrorScope;
}
tween.rounded = this.rounded;
tween.min = this.min;
tween.max = this.max;
tween.isPaused = this.isPaused;
tween.timePaused = this.timePaused;
tween.isCaller = this.isCaller;
......
......@@ -314,6 +314,11 @@ function _updateTweenByIndex(i) {
if (tweening.rounded)
nv = Math.round(nv);
if (tweening.min !== undefined && nv < tweening.min)
nv = tweening.min;
if (tweening.max !== undefined && nv > tweening.max)
nv = tweening.max;
if (property.isSpecialProperty) {
// It's a special property, tunnel via the special property method
_specialPropertyList[name].setValue(scope, nv, _specialPropertyList[name].parameters, tweening.properties[name].extra);
......@@ -368,7 +373,7 @@ function _onEnterFrame() {
return true;
}
const restrictedWords = {
var restrictedWords = {
time: true,
delay: true,
userFrames: true,
......@@ -381,6 +386,8 @@ const restrictedWords = {
onOverwrite: true,
onError: true,
rounded: true,
min: true,
max: true,
onStartParams: true,
onUpdateParams: true,
onCompleteParams: true,
......@@ -571,6 +578,8 @@ function _addTweenOrCaller(target, tweeningParameters, isCaller) {
tween.onOverwriteScope = obj.onOverwriteScope;
tween.onErrorScope = obj.onErrorScope;
tween.rounded = obj.rounded;
tween.min = obj.min;
tween.max = obj.max;
tween.skipUpdates = obj.skipUpdates;
tween.isCaller = isCaller;
......
#!/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