]> git.ipfire.org Git - thirdparty/valgrind.git/commitdiff
Overhaul DHAT.
authorNicholas Nethercote <nnethercote@mozilla.com>
Thu, 4 Oct 2018 01:00:22 +0000 (11:00 +1000)
committerNicholas Nethercote <nnethercote@mozilla.com>
Fri, 1 Feb 2019 03:54:34 +0000 (14:54 +1100)
This commit thoroughly overhauls DHAT, moving it out of the
"experimental" ghetto. It makes moderate changes to DHAT itself,
including dumping profiling data to a JSON format output file. It also
implements a new data viewer (as a web app, in dhat/dh_view.html).

The main benefits over the old DHAT are as follows.

- The separation of data collection and presentation means you can run a
  program once under DHAT and then sort the data in various ways. Also,
  full data is in the output file, and the viewer chooses what to omit.

- The data can be sorted in more ways than previously. Some of these
  sorts involve useful filters such as "short-lived" and "zero reads or
  zero writes".

- The tree structure view avoids the need to choose stack trace depth.
  This avoids both the problem of not enough depth (when records that
  should be distinct are combined, and may not contain enough
  information to be actionable) and the problem of too much depth (when
  records that should be combined are separated, making them seem less
  important than they really are).

- Byte and block measures are shown with a percentage relative to the
  global count, which helps gauge relative significance of different
  parts of the profile.

- Byte and blocks measures are also shown with an allocation rate
  (bytes and blocks per million instructions), which enables comparisons
  across multiple profiles, even if those profiles represent different
  workloads.

- Both global and per-node measurements are taken at the global heap
  peak ("At t-gmax"), which gives Massif-like insight into the point of
  peak memory use.

- The final/liftimes stats are a bit more useful than the old deaths
  stats. (E.g. the old deaths stats didn't take into account lifetimes
  of unfreed blocks.)

- The handling of realloc() has changed. The sequence `p = malloc(100);
  realloc(p, 200);` now increases the total block count by 2 and the
  total byte count by 300. Previously it increased them by 1 and 200.
  The new handling is a more operational view that better reflects the
  effect of allocations on performance. It makes a significant
  difference in the results, giving paths involving reallocation (e.g.
  repeated pushing to a growing vector) more prominence.

Other things of note:

- There is now testing, both regression tests that run within the
  standard test suite, and viewer-specific tests that cannot run within
  the standard test suite. The latter are run by loading
  dh_view.html?test=1 in a web browser.

- The commit puts all tool lists in Makefiles (and similar files) in the
  following consistent order: memcheck, cachegrind, callgrind, helgrind,
  drd, massif, dhat, lackey, none; exp-sgcheck, exp-bbv.

- A lot of fields in dh_main.c have been given more descriptive names.
  Those names now match those used in dh_view.js.

45 files changed:
.gitignore
Makefile.am
NEWS
configure.ac
coregrind/m_libcsetjmp.c
coregrind/m_main.c
coregrind/pub_core_libcsetjmp.h
dhat/Makefile.am [moved from exp-dhat/Makefile.am with 50% similarity]
dhat/dh_main.c [moved from exp-dhat/dh_main.c with 66% similarity]
dhat/dh_test.js [new file with mode: 0644]
dhat/dh_view.css [new file with mode: 0644]
dhat/dh_view.html [new file with mode: 0644]
dhat/dh_view.js [new file with mode: 0644]
dhat/docs/dh-manual.xml [new file with mode: 0644]
dhat/tests/Makefile.am [new file with mode: 0644]
dhat/tests/acc.c [new file with mode: 0644]
dhat/tests/acc.stderr.exp [new file with mode: 0644]
dhat/tests/acc.vgtest [new file with mode: 0644]
dhat/tests/basic.c [new file with mode: 0644]
dhat/tests/basic.stderr.exp [new file with mode: 0644]
dhat/tests/basic.vgtest [new file with mode: 0644]
dhat/tests/big.c [new file with mode: 0644]
dhat/tests/big.stderr.exp [new file with mode: 0644]
dhat/tests/big.vgtest [new file with mode: 0644]
dhat/tests/empty.c [new file with mode: 0644]
dhat/tests/empty.stderr.exp [new file with mode: 0644]
dhat/tests/empty.vgtest [new file with mode: 0644]
dhat/tests/filter_stderr [new file with mode: 0755]
dhat/tests/sig.c [new file with mode: 0644]
dhat/tests/sig.stderr.exp [new file with mode: 0644]
dhat/tests/sig.vgtest [new file with mode: 0644]
dhat/tests/single.c [new file with mode: 0644]
dhat/tests/single.stderr.exp [new file with mode: 0644]
dhat/tests/single.vgtest [new file with mode: 0644]
docs/Makefile.am
docs/README
docs/images/dh-tree.png [new file with mode: 0644]
docs/xml/manual-core.xml
docs/xml/manual.xml
exp-dhat/docs/dh-manual.xml [deleted file]
exp-dhat/tests/Makefile.am [deleted file]
include/pub_tool_libcsetjmp.h
massif/ms_main.c
solaris/valgrind.p5m
tests/check_headers_and_includes

index 3e35b982ce2decc550ffae6ca70827436f30ace2..1d9850086cd4e01ca2ee6fc7e454ecca35df315d 100644 (file)
 /coregrind/m_ume/.deps
 /coregrind/m_ume/.dirstamp
 
+# /dhat/
+/dhat/*.dSYM
+/dhat/.deps
+/dhat/dhat-*-darwin
+/dhat/dhat-*-linux
+/dhat/dhat-*-solaris
+/dhat/Makefile
+/dhat/Makefile.in
+/dhat/vgpreload_dhat-*-linux.so
+/dhat/vgpreload_dhat-*-darwin.so
+/dhat/vgpreload_dhat-*-solaris.so
+
+# /dhat/tests/
+/dhat/tests/Makefile
+/dhat/tests/Makefile.in
+/dhat/tests/*.dSYM
+/dhat/tests/*.so
+/dhat/tests/*.stderr.diff*
+/dhat/tests/*.stderr.out
+/dhat/tests/*.stdout.diff*
+/dhat/tests/*.stdout.out
+/dhat/tests/.deps
+/dhat/tests/acc
+/dhat/tests/basic
+/dhat/tests/big
+/dhat/tests/empty
+/dhat/tests/single
+
 # /docs/
 /docs/FAQ.txt
 /docs/html
 /exp-bbv/tests/x86-linux/Makefile
 /exp-bbv/tests/x86-linux/Makefile.in
 
-# /exp-dhat/
-/exp-dhat/*.dSYM
-/exp-dhat/.deps
-/exp-dhat/exp-dhat-*-darwin
-/exp-dhat/exp-dhat-*-linux
-/exp-dhat/exp-dhat-*-solaris
-/exp-dhat/Makefile
-/exp-dhat/Makefile.in
-/exp-dhat/vgpreload_exp-dhat-*-linux.so
-/exp-dhat/vgpreload_exp-dhat-*-darwin.so
-/exp-dhat/vgpreload_exp-dhat-*-solaris.so
-
-# /exp-dhat/tests/
-/exp-dhat/tests/Makefile
-/exp-dhat/tests/Makefile.in
-
 # /exp-sgcheck/
 /exp-sgcheck/*.dSYM
 /exp-sgcheck/.deps
index 154f68f5fdf7b76cdd52ef7fe493f9b0fd2bf33d..631c845ab2240acc63e38743f11ad7ecdfd37786 100644 (file)
@@ -6,15 +6,15 @@ include $(top_srcdir)/Makefile.all.am
 TOOLS =                memcheck \
                cachegrind \
                callgrind \
+               helgrind \
+               drd \
                massif \
+               dhat \
                lackey \
-               none \
-               helgrind \
-               drd
+               none
 
 EXP_TOOLS =    exp-sgcheck \
-               exp-bbv \
-               exp-dhat
+               exp-bbv
 
 # Put docs last because building the HTML is slow and we want to get
 # everything else working before we try it.
diff --git a/NEWS b/NEWS
index 7014fb038de378b2fef5bc264c8e95e43a7e6d76..4a75a0aa3d6ac4dcce001b795eb526a9f66a68f9 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -20,6 +20,20 @@ support for X86/macOS 10.13, AMD64/macOS 10.13.
 
 * ==================== TOOL CHANGES ====================
 
+* DHAT: 
+
+  - DHAT been thoroughly overhauled and improved. As a result, it has been
+    promoted from an experimental tool to a regular tool. Run it with
+    --tool=dhat instead of --tool=exp-dhat.
+
+  - DHAT now prints only minimal data when the program ends, instead writing
+    the bulk of the profiling data to a file. As a result, the --show-top-n and
+    --sort-by options have been removed.
+    
+  - Data files can be viewed with the new viewer, dh_view.html.
+    
+  - See the documentation for more details.
+
 * Cachegrind:
 
   - cg_annotate has a new option, --show-percs, which prints percentages next
@@ -94,6 +108,8 @@ n-i-bz  Fix callgrind_annotate non deterministic order for equal total
 n-i-bz  callgrind_annotate --threshold=100 does not print all functions.
 n-i-bz  callgrind_annotate Use of uninitialized value in numeric gt (>)
 
+
+
 Release 3.14.0 (9 October 2018)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
index 35c6f744b5a3bd3d57b0481e022120e099f511a8..a02a76b2acc48ee60d62e30f58b2c67a6490dd52 100644 (file)
@@ -4636,9 +4636,14 @@ AC_CONFIG_FILES([
    callgrind/tests/Makefile
    helgrind/Makefile
    helgrind/tests/Makefile
+   drd/Makefile
+   drd/scripts/download-and-build-splash2
+   drd/tests/Makefile
    massif/Makefile
    massif/tests/Makefile
    massif/ms_print
+   dhat/Makefile
+   dhat/tests/Makefile
    lackey/Makefile
    lackey/tests/Makefile
    none/Makefile
@@ -4664,9 +4669,6 @@ AC_CONFIG_FILES([
    none/tests/x86-solaris/Makefile
    exp-sgcheck/Makefile
    exp-sgcheck/tests/Makefile
-   drd/Makefile
-   drd/scripts/download-and-build-splash2
-   drd/tests/Makefile
    exp-bbv/Makefile
    exp-bbv/tests/Makefile
    exp-bbv/tests/x86/Makefile
@@ -4674,8 +4676,6 @@ AC_CONFIG_FILES([
    exp-bbv/tests/amd64-linux/Makefile
    exp-bbv/tests/ppc32-linux/Makefile
    exp-bbv/tests/arm-linux/Makefile
-   exp-dhat/Makefile
-   exp-dhat/tests/Makefile
    shared/Makefile
    solaris/Makefile
 ])
index c731806402f2e515f89845421385e8621b9dd991..85ffc1226204bd7ac8a64436001eb978bf2e56d2 100644 (file)
@@ -7,7 +7,7 @@
    This file is part of Valgrind, a dynamic binary instrumentation
    framework.
 
-   Copyright (C) 2010-2017 Mozilla Inc
+   Copyright (C) 2010-2017 Mozilla Foundation
 
    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
index 7d987ea2668df587c7e37b4a29e212d13368414c..21df6791a73c85094266196df7b0a8f0ba01525b 100644 (file)
@@ -1454,7 +1454,7 @@ Int valgrind_main ( Int argc, HChar **argv, HChar **envp )
        || 0 == VG_(strcmp)(VG_(clo_toolname), "helgrind")
        || 0 == VG_(strcmp)(VG_(clo_toolname), "drd")
        || 0 == VG_(strcmp)(VG_(clo_toolname), "massif")
-       || 0 == VG_(strcmp)(VG_(clo_toolname), "exp-dhat")) {
+       || 0 == VG_(strcmp)(VG_(clo_toolname), "dhat")) {
       /* Change the default setting.  Later on (just below)
          main_process_cmd_line_options should pick up any
          user-supplied setting for it and will override the default
index 91c6801917d9e7af723e5709e4fb4600ca031d12..494703fe1168e64558c1d6f96c8148bcb98b31a8 100644 (file)
@@ -7,7 +7,7 @@
    This file is part of Valgrind, a dynamic binary instrumentation
    framework.
 
-   Copyright (C) 2010-2017 Mozilla Inc
+   Copyright (C) 2010-2017 Mozilla Foundation
 
    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
similarity index 50%
rename from exp-dhat/Makefile.am
rename to dhat/Makefile.am
index b74529858df6e1b2c7cc0b9b7fb40bfebc1553fd..74bf2cd1ccff104dcc3e707758582de16ce553ff 100644 (file)
@@ -11,89 +11,89 @@ EXTRA_DIST = docs/dh-manual.xml
 #bin_SCRIPTS = dh_print
 
 #----------------------------------------------------------------------------
-# exp_dhat-<platform>
+# dhat-<platform>
 #----------------------------------------------------------------------------
 
-noinst_PROGRAMS  = exp-dhat-@VGCONF_ARCH_PRI@-@VGCONF_OS@
+noinst_PROGRAMS  = dhat-@VGCONF_ARCH_PRI@-@VGCONF_OS@
 if VGCONF_HAVE_PLATFORM_SEC
-noinst_PROGRAMS += exp-dhat-@VGCONF_ARCH_SEC@-@VGCONF_OS@
+noinst_PROGRAMS += dhat-@VGCONF_ARCH_SEC@-@VGCONF_OS@
 endif
 
 EXP_DHAT_SOURCES_COMMON = dh_main.c
 
-exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_SOURCES      = \
+dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_SOURCES      = \
        $(EXP_DHAT_SOURCES_COMMON)
-exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_CPPFLAGS     = \
+dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_CPPFLAGS     = \
        $(AM_CPPFLAGS_@VGCONF_PLATFORM_PRI_CAPS@)
-exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_CFLAGS       = $(LTO_CFLAGS) \
+dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_CFLAGS       = $(LTO_CFLAGS) \
        $(AM_CFLAGS_@VGCONF_PLATFORM_PRI_CAPS@)
-exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_DEPENDENCIES = \
+dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_DEPENDENCIES = \
        $(TOOL_DEPENDENCIES_@VGCONF_PLATFORM_PRI_CAPS@)
-exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_LDADD        = \
+dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_LDADD        = \
        $(TOOL_LDADD_@VGCONF_PLATFORM_PRI_CAPS@)
-exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_LDFLAGS      = \
+dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_LDFLAGS      = \
        $(TOOL_LDFLAGS_@VGCONF_PLATFORM_PRI_CAPS@)
-exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_LINK = \
+dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_LINK = \
        $(top_builddir)/coregrind/link_tool_exe_@VGCONF_OS@ \
        @VALT_LOAD_ADDRESS_PRI@ \
        $(LINK) \
-       $(exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_CFLAGS) \
-       $(exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_LDFLAGS)
+       $(dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_CFLAGS) \
+       $(dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_LDFLAGS)
 
 if VGCONF_HAVE_PLATFORM_SEC
-exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_SOURCES      = \
+dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_SOURCES      = \
        $(EXP_DHAT_SOURCES_COMMON)
-exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_CPPFLAGS     = \
+dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_CPPFLAGS     = \
        $(AM_CPPFLAGS_@VGCONF_PLATFORM_SEC_CAPS@)
-exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_CFLAGS       = $(LTO_CFLAGS) \
+dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_CFLAGS       = $(LTO_CFLAGS) \
        $(AM_CFLAGS_@VGCONF_PLATFORM_SEC_CAPS@)
-exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_DEPENDENCIES = \
+dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_DEPENDENCIES = \
        $(TOOL_DEPENDENCIES_@VGCONF_PLATFORM_SEC_CAPS@)
-exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_LDADD        = \
+dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_LDADD        = \
        $(TOOL_LDADD_@VGCONF_PLATFORM_SEC_CAPS@)
-exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_LDFLAGS      = \
+dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_LDFLAGS      = \
        $(TOOL_LDFLAGS_@VGCONF_PLATFORM_SEC_CAPS@)
-exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_LINK = \
+dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_LINK = \
        $(top_builddir)/coregrind/link_tool_exe_@VGCONF_OS@ \
        @VALT_LOAD_ADDRESS_SEC@ \
        $(LINK) \
-       $(exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_CFLAGS) \
-       $(exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_LDFLAGS)
+       $(dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_CFLAGS) \
+       $(dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_LDFLAGS)
 endif
 
 #----------------------------------------------------------------------------
-# vgpreload_exp_dhat-<platform>.so
+# vgpreload_dhat-<platform>.so
 #----------------------------------------------------------------------------
 
-noinst_PROGRAMS += vgpreload_exp-dhat-@VGCONF_ARCH_PRI@-@VGCONF_OS@.so
+noinst_PROGRAMS += vgpreload_dhat-@VGCONF_ARCH_PRI@-@VGCONF_OS@.so
 if VGCONF_HAVE_PLATFORM_SEC
-noinst_PROGRAMS += vgpreload_exp-dhat-@VGCONF_ARCH_SEC@-@VGCONF_OS@.so
+noinst_PROGRAMS += vgpreload_dhat-@VGCONF_ARCH_SEC@-@VGCONF_OS@.so
 endif
 
 if VGCONF_OS_IS_DARWIN
 noinst_DSYMS = $(noinst_PROGRAMS)
 endif
 
-vgpreload_exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_SOURCES      = 
-vgpreload_exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_CPPFLAGS     = \
+vgpreload_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_SOURCES      = 
+vgpreload_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_CPPFLAGS     = \
        $(AM_CPPFLAGS_@VGCONF_PLATFORM_PRI_CAPS@)
-vgpreload_exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_CFLAGS       = \
+vgpreload_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_CFLAGS       = \
        $(AM_CFLAGS_PSO_@VGCONF_PLATFORM_PRI_CAPS@)
-vgpreload_exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_DEPENDENCIES = \
+vgpreload_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_DEPENDENCIES = \
        $(LIBREPLACEMALLOC_@VGCONF_PLATFORM_PRI_CAPS@)
-vgpreload_exp_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_LDFLAGS      = \
+vgpreload_dhat_@VGCONF_ARCH_PRI@_@VGCONF_OS@_so_LDFLAGS      = \
        $(PRELOAD_LDFLAGS_@VGCONF_PLATFORM_PRI_CAPS@) \
        $(LIBREPLACEMALLOC_LDFLAGS_@VGCONF_PLATFORM_PRI_CAPS@)
 
 if VGCONF_HAVE_PLATFORM_SEC
-vgpreload_exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_SOURCES      = 
-vgpreload_exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_CPPFLAGS     = \
+vgpreload_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_SOURCES      = 
+vgpreload_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_CPPFLAGS     = \
        $(AM_CPPFLAGS_@VGCONF_PLATFORM_SEC_CAPS@)
-vgpreload_exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_CFLAGS       = \
+vgpreload_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_CFLAGS       = \
        $(AM_CFLAGS_PSO_@VGCONF_PLATFORM_SEC_CAPS@)
-vgpreload_exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_DEPENDENCIES = \
+vgpreload_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_DEPENDENCIES = \
        $(LIBREPLACEMALLOC_@VGCONF_PLATFORM_SEC_CAPS@)
-vgpreload_exp_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_LDFLAGS      = \
+vgpreload_dhat_@VGCONF_ARCH_SEC@_@VGCONF_OS@_so_LDFLAGS      = \
        $(PRELOAD_LDFLAGS_@VGCONF_PLATFORM_SEC_CAPS@) \
        $(LIBREPLACEMALLOC_LDFLAGS_@VGCONF_PLATFORM_SEC_CAPS@)
 endif
similarity index 66%
rename from exp-dhat/dh_main.c
rename to dhat/dh_main.c
index c23134b2c6006ea68408d081f7bf216ada1dceb0..ffcf87461973f0914460f31a92fc984b7844dd0f 100644 (file)
@@ -1,13 +1,13 @@
 
-//--------------------------------------------------------------------*/
-//--- DHAT: a Dynamic Heap Analysis Tool                 dh_main.c ---*/
-//--------------------------------------------------------------------*/
+//--------------------------------------------------------------------//
+//--- DHAT: a Dynamic Heap Analysis Tool                 dh_main.c ---//
+//--------------------------------------------------------------------//
 
 /*
    This file is part of DHAT, a Valgrind tool for profiling the
    heap usage of programs.
 
-   Copyright (C) 2010-2017 Mozilla Inc
+   Copyright (C) 2010-2018 Mozilla Foundation
 
    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
 
 
 #include "pub_tool_basics.h"
+#include "pub_tool_clientstate.h"
 #include "pub_tool_libcbase.h"
 #include "pub_tool_libcassert.h"
+#include "pub_tool_libcfile.h"
 #include "pub_tool_libcprint.h"
+#include "pub_tool_libcproc.h"
 #include "pub_tool_machine.h"      // VG_(fnptr_to_fnentry)
 #include "pub_tool_mallocfree.h"
 #include "pub_tool_options.h"
 
 #define HISTOGRAM_SIZE_LIMIT 1024
 
-
 //------------------------------------------------------------//
 //--- Globals                                              ---//
 //------------------------------------------------------------//
 
-// Number of guest instructions executed so far.  This is 
-// incremented directly from the generated code.
-static ULong g_guest_instrs_executed = 0;
-
-// Summary statistics for the entire run.
-static ULong g_tot_blocks = 0;   // total blocks allocated
-static ULong g_tot_bytes  = 0;   // total bytes allocated
+// Values for the entire run.
+static ULong g_total_blocks = 0;
+static ULong g_total_bytes  = 0;
 
-static ULong g_cur_blocks_live = 0; // curr # blocks live
-static ULong g_cur_bytes_live  = 0; // curr # bytes live
+// Current values.
+static ULong g_curr_blocks = 0;
+static ULong g_curr_bytes  = 0;
+static ULong g_curr_instrs = 0;  // incremented from generated code
 
-static ULong g_max_blocks_live = 0; // bytes and blocks at
-static ULong g_max_bytes_live  = 0; // the max residency point
+// Values at the global max, i.e. when g_curr_bytes peaks.
+static ULong g_max_blocks = 0;
+static ULong g_max_bytes  = 0;
+static ULong g_max_instrs = 0;
 
+// Values for the entire run. Computed at the end.
+static ULong g_reads_bytes = 0;
+static ULong g_writes_bytes = 0;
 
 //------------------------------------------------------------//
 //--- an Interval Tree of live blocks                      ---//
@@ -74,8 +79,8 @@ typedef
       SizeT       req_szB;
       ExeContext* ap;  /* allocation ec */
       ULong       allocd_at; /* instruction number */
-      ULong       n_reads;
-      ULong       n_writes;
+      ULong       reads_bytes;
+      ULong       writes_bytes;
       /* Approx histogram, one byte per payload byte.  Counts latch up
          therefore at 0xFFFF.  Can be NULL if the block is resized or if
          the block is larger than HISTOGRAM_SIZE_LIMIT. */
@@ -112,14 +117,14 @@ static UWord stats__n_fBc_notfound = 0;
 static Block* find_Block_containing ( Addr a )
 {
    if (LIKELY(fbc_cache0
-              && fbc_cache0->payload <= a 
+              && fbc_cache0->payload <= a
               && a < fbc_cache0->payload + fbc_cache0->req_szB)) {
       // found at 0
       stats__n_fBc_cached++;
       return fbc_cache0;
    }
    if (LIKELY(fbc_cache1
-              && fbc_cache1->payload <= a 
+              && fbc_cache1->payload <= a
               && a < fbc_cache1->payload + fbc_cache1->req_szB)) {
       // found at 1; swap 0 and 1
       Block* tmp = fbc_cache0;
@@ -170,31 +175,37 @@ static void delete_Block_starting_at ( Addr a )
 
 typedef
    struct {
-      // the allocation point that we're summarising stats for
+      // The allocation point that we're summarising stats for.
       ExeContext* ap;
-      // used when printing results
-      Bool shown;
-      // The current number of blocks and bytes live for this AP
-      ULong cur_blocks_live;
-      ULong cur_bytes_live;
-      // The number of blocks and bytes live at the max-liveness
-      // point.  Note this is a bit subtle.  max_blocks_live is not
-      // the maximum number of live blocks, but rather the number of
-      // blocks live at the point of maximum byte liveness.  These are
-      // not necessarily the same thing.
-      ULong max_blocks_live;
-      ULong max_bytes_live;
+
       // Total number of blocks and bytes allocated by this AP.
-      ULong tot_blocks;
-      ULong tot_bytes;
-      // Sum of death ages for all blocks allocated by this AP,
-      // that have subsequently been freed.
-      ULong death_ages_sum;
-      ULong deaths;
+      ULong total_blocks;
+      ULong total_bytes;
+
+      // The current number of blocks and bytes live for this AP.
+      ULong curr_blocks;
+      ULong curr_bytes;
+
+      // Values at the AP max, i.e. when this AP's curr_bytes peaks.
+      ULong max_blocks;     // Blocks at the AP max.
+      ULong max_bytes;      // The AP max, measured in bytes.
+
+      // Values at the global max.
+      ULong at_tgmax_blocks;
+      ULong at_tgmax_bytes;
+
+      // Total lifetimes of all blocks allocated by this AP. Includes blocks
+      // explicitly freed and blocks implicitly freed at termination.
+      ULong total_lifetimes_instrs;
+
+      // Number of blocks freed by this AP. (Only used in assertions.)
+      ULong freed_blocks;
+
       // Total number of reads and writes in all blocks allocated
       // by this AP.
-      ULong n_reads;
-      ULong n_writes;
+      ULong reads_bytes;
+      ULong writes_bytes;
+
       /* Histogram information.  We maintain a histogram aggregated for
          all retiring Blocks allocated by this AP, but only if:
          - this AP has only ever allocated objects of one size
@@ -203,7 +214,7 @@ typedef
          has only ever allocated blocks of one size.
 
          3 states:
-            Unknown          because no retirement yet 
+            Unknown          because no retirement yet
             Exactly xsize    all retiring blocks are of this size
             Mixed            multiple different sizes seen
       */
@@ -218,6 +229,26 @@ typedef
 static WordFM* apinfo = NULL;  /* WordFM* ExeContext* APInfo* */
 
 
+// Are we at peak memory? If so, update at_tgmax_blocks and at_tgmax_bytes in
+// all APInfos. Note that this is moderately expensive so we avoid calling it
+// on every allocation.
+static void check_for_peak(void)
+{
+   if (g_curr_bytes == g_max_bytes) {
+      // It's a peak. (If there are multiple equal peaks we record the latest
+      // one.)
+      UWord keyW, valW;
+      VG_(initIterFM)(apinfo);
+      while (VG_(nextIterFM)(apinfo, &keyW, &valW)) {
+         APInfo* api = (APInfo*)valW;
+         tl_assert(api && api->ap == (ExeContext*)keyW);
+         api->at_tgmax_blocks = api->curr_blocks;
+         api->at_tgmax_bytes = api->curr_bytes;
+      }
+      VG_(doneIterFM)(apinfo);
+   }
+}
+
 /* 'bk' is being introduced (has just been allocated).  Find the
    relevant APInfo entry for it, or create one, based on the block's
    allocation EC.  Then, update the APInfo to the extent that we
@@ -236,14 +267,14 @@ static void intro_Block ( Block* bk )
       api = (APInfo*)valW;
       tl_assert(keyW == (UWord)bk->ap);
    } else {
-      api = VG_(malloc)( "dh.main.intro_Block.1", sizeof(APInfo) );
+      api = VG_(malloc)( "dh.intro_Block.1", sizeof(APInfo) );
       VG_(memset)(api, 0, sizeof(*api));
       api->ap = bk->ap;
       Bool present = VG_(addToFM)( apinfo,
                                    (UWord)bk->ap, (UWord)api );
       tl_assert(!present);
       // histo stuff
-      tl_assert(api->deaths == 0);
+      tl_assert(api->freed_blocks == 0);
       api->xsize_tag = Unknown;
       api->xsize = 0;
       if (0) VG_(printf)("api %p   -->  Unknown\n", api);
@@ -251,35 +282,32 @@ static void intro_Block ( Block* bk )
 
    tl_assert(api->ap == bk->ap);
 
-   /* So: update stats to reflect an allocation */
+   // Update global stats first.
 
-   // # live blocks
-   api->cur_blocks_live++;
+   g_total_blocks++;
+   g_total_bytes += bk->req_szB;
 
-   // # live bytes
-   api->cur_bytes_live += bk->req_szB;
-   if (api->cur_bytes_live > api->max_bytes_live) {
-      api->max_bytes_live  = api->cur_bytes_live;
-      api->max_blocks_live = api->cur_blocks_live;
+   g_curr_blocks++;
+   g_curr_bytes += bk->req_szB;
+   if (g_curr_bytes > g_max_bytes) {
+      g_max_blocks = g_curr_blocks;
+      g_max_bytes = g_curr_bytes;
+      g_max_instrs = g_curr_instrs;
    }
 
-   // total blocks and bytes allocated here
-   api->tot_blocks++;
-   api->tot_bytes += bk->req_szB;
+   // Now update APInfo stats.
 
-   // update summary globals
-   g_tot_blocks++;
-   g_tot_bytes += bk->req_szB;
+   api->total_blocks++;
+   api->total_bytes += bk->req_szB;
 
-   g_cur_blocks_live++;
-   g_cur_bytes_live += bk->req_szB;
-   if (g_cur_bytes_live > g_max_bytes_live) {
-      g_max_bytes_live = g_cur_bytes_live;
-      g_max_blocks_live = g_cur_blocks_live;
+   api->curr_blocks++;
+   api->curr_bytes += bk->req_szB;
+   if (api->curr_bytes > api->max_bytes) {
+      api->max_blocks = api->curr_blocks;
+      api->max_bytes  = api->curr_bytes;
    }
 }
 
-
 /* 'bk' is retiring (being freed).  Find the relevant APInfo entry for
    it, which must already exist.  Then, fold info from 'bk' into that
    entry.  'because_freed' is True if the block is retiring because
@@ -305,52 +333,57 @@ static void retire_Block ( Block* bk, Bool because_freed )
 
    // update stats following this free.
    if (0)
-   VG_(printf)("ec %p  api->c_by_l %llu  bk->rszB %llu\n",
-               bk->ap, api->cur_bytes_live, (ULong)bk->req_szB);
+      VG_(printf)("ec %p  api->c_by_l %llu  bk->rszB %llu\n",
+                  bk->ap, api->curr_bytes, (ULong)bk->req_szB);
 
-   // update total blocks live etc for this AP
    if (because_freed) {
-      tl_assert(api->cur_blocks_live >= 1);
-      tl_assert(api->cur_bytes_live >= bk->req_szB);
-      api->cur_blocks_live--;
-      api->cur_bytes_live -= bk->req_szB;
-
-      api->deaths++;
-
-      tl_assert(bk->allocd_at <= g_guest_instrs_executed);
-      api->death_ages_sum += (g_guest_instrs_executed - bk->allocd_at);
-
-      // update global summary stats
-      tl_assert(g_cur_blocks_live > 0);
-      g_cur_blocks_live--;
-      tl_assert(g_cur_bytes_live >= bk->req_szB);
-      g_cur_bytes_live -= bk->req_szB;
+      // Total bytes is coming down from a possible peak.
+      check_for_peak();
+
+      // Then update global stats.
+      tl_assert(g_curr_blocks >= 1);
+      tl_assert(g_curr_bytes >= bk->req_szB);
+      g_curr_blocks--;
+      g_curr_bytes -= bk->req_szB;
+
+      // Then update APInfo stats.
+      tl_assert(api->curr_blocks >= 1);
+      tl_assert(api->curr_bytes >= bk->req_szB);
+      api->curr_blocks--;
+      api->curr_bytes -= bk->req_szB;
+
+      api->freed_blocks++;
    }
 
+   tl_assert(bk->allocd_at <= g_curr_instrs);
+   api->total_lifetimes_instrs += (g_curr_instrs - bk->allocd_at);
+
    // access counts
-   api->n_reads  += bk->n_reads;
-   api->n_writes += bk->n_writes;
+   api->reads_bytes += bk->reads_bytes;
+   api->writes_bytes += bk->writes_bytes;
+   g_reads_bytes += bk->reads_bytes;
+   g_writes_bytes += bk->writes_bytes;
 
    // histo stuff.  First, do state transitions for xsize/xsize_tag.
    switch (api->xsize_tag) {
 
       case Unknown:
          tl_assert(api->xsize == 0);
-         tl_assert(api->deaths == 1 || api->deaths == 0);
+         tl_assert(api->freed_blocks == 1 || api->freed_blocks == 0);
          tl_assert(!api->histo);
          api->xsize_tag = Exactly;
          api->xsize = bk->req_szB;
          if (0) VG_(printf)("api %p   -->  Exactly(%lu)\n", api, api->xsize);
          // and allocate the histo
          if (bk->histoW) {
-            api->histo = VG_(malloc)("dh.main.retire_Block.1",
+            api->histo = VG_(malloc)("dh.retire_Block.1",
                                      api->xsize * sizeof(UInt));
             VG_(memset)(api->histo, 0, api->xsize * sizeof(UInt));
          }
          break;
 
       case Exactly:
-         //tl_assert(api->deaths > 1);
+         //tl_assert(api->freed_blocks > 1);
          if (bk->req_szB != api->xsize) {
             if (0) VG_(printf)("api %p   -->  Mixed(%lu -> %lu)\n",
                                api, api->xsize, bk->req_szB);
@@ -365,7 +398,7 @@ static void retire_Block ( Block* bk, Bool because_freed )
          break;
 
       case Mixed:
-         //tl_assert(api->deaths > 1);
+         //tl_assert(api->freed_blocks > 1);
          break;
 
       default:
@@ -386,8 +419,6 @@ static void retire_Block ( Block* bk, Bool because_freed )
       if (0) VG_(printf)("fold in, AP = %p\n", api);
    }
 
-
-
 #if 0
    if (bk->histoB) {
       VG_(printf)("block retiring, histo %lu: ", bk->req_szB);
@@ -403,8 +434,9 @@ static void retire_Block ( Block* bk, Bool because_freed )
 
 /* This handles block resizing.  When a block with AP 'ec' has a
    size change of 'delta', call here to update the APInfo. */
-static void apinfo_change_cur_bytes_live( ExeContext* ec, Long delta )
+static void resize_Block(ExeContext* ec, SizeT old_req_szB, SizeT new_req_szB)
 {
+   Long    delta = (Long)new_req_szB - (Long)old_req_szB;
    APInfo* api   = NULL;
    UWord   keyW  = 0;
    UWord   valW  = 0;
@@ -416,30 +448,46 @@ static void apinfo_change_cur_bytes_live( ExeContext* ec, Long delta )
    tl_assert(api->ap == ec);
 
    if (delta < 0) {
-      tl_assert(api->cur_bytes_live >= -delta);
-      tl_assert(g_cur_bytes_live >= -delta);
+      tl_assert(api->curr_bytes >= -delta);
+      tl_assert(g_curr_bytes >= -delta);
    }
 
-   // adjust current live size
-   api->cur_bytes_live += delta;
-   g_cur_bytes_live += delta;
-
-   if (delta > 0 && api->cur_bytes_live > api->max_bytes_live) {
-      api->max_bytes_live  = api->cur_bytes_live;
-      api->max_blocks_live = api->cur_blocks_live;
+   // Total bytes might be coming down from a possible peak.
+   if (delta < 0)
+      check_for_peak();
+
+   // Note: we treat realloc() like malloc() + free() for total counts, i.e. we
+   // increment total_blocks by 1 and increment total_bytes by new_req_szB.
+   //
+   // A reasonable alternative would be to leave total_blocks unchanged and
+   // increment total_bytes by delta (but only if delta is positive). But then
+   // calls to realloc wouldn't be counted towards the total_blocks count,
+   // which is undesirable.
+
+   // Update global stats first.
+
+   g_total_blocks++;
+   g_total_bytes += new_req_szB;
+
+   g_curr_blocks += 0;  // unchanged
+   g_curr_bytes += delta;
+   if (g_curr_bytes > g_max_bytes) {
+      g_max_blocks = g_curr_blocks;
+      g_max_bytes = g_curr_bytes;
+      g_max_instrs = g_curr_instrs;
    }
 
-   // update global summary stats
-   if (delta > 0 && g_cur_bytes_live > g_max_bytes_live) {
-      g_max_bytes_live = g_cur_bytes_live;
-      g_max_blocks_live = g_cur_blocks_live;
-   }
-   if (delta > 0)
-      g_tot_bytes += delta;
+   // Now update APInfo stats.
 
-   // adjust total allocation size
-   if (delta > 0)
-      api->tot_bytes += delta;
+   api->total_blocks++;
+   api->total_bytes += new_req_szB;
+
+   api->curr_blocks += 0;  // unchanged
+   api->curr_bytes += delta;
+   if (api->curr_bytes > api->max_bytes) {
+      api->max_blocks = api->curr_blocks;
+      api->max_bytes  = api->curr_bytes;
+   }
 }
 
 
@@ -473,14 +521,14 @@ void* new_block ( ThreadId tid, void* p, SizeT req_szB, SizeT req_alignB,
       /* slop_szB = 0; */
    }
 
-   // Make new HP_Chunk node, add to malloc_list
+   // Make new Block, add to interval_tree.
    Block* bk = VG_(malloc)("dh.new_block.1", sizeof(Block));
-   bk->payload   = (Addr)p;
-   bk->req_szB   = req_szB;
-   bk->ap        = VG_(record_ExeContext)(tid, 0/*first word delta*/);
-   bk->allocd_at = g_guest_instrs_executed;
-   bk->n_reads   = 0;
-   bk->n_writes  = 0;
+   bk->payload      = (Addr)p;
+   bk->req_szB      = req_szB;
+   bk->ap           = VG_(record_ExeContext)(tid, 0/*first word delta*/);
+   bk->allocd_at    = g_curr_instrs;
+   bk->reads_bytes  = 0;
+   bk->writes_bytes = 0;
    // set up histogram array, if the block isn't too large
    bk->histoW = NULL;
    if (req_szB <= HISTOGRAM_SIZE_LIMIT) {
@@ -520,7 +568,7 @@ void die_block ( void* p, Bool custom_free )
    }
 
    if (0) VG_(printf)(" FREE %p %llu\n",
-                      p, g_guest_instrs_executed - bk->allocd_at);
+                      p, g_curr_instrs - bk->allocd_at);
 
    retire_Block(bk, True/*because_freed*/);
 
@@ -568,8 +616,7 @@ void* renew_block ( ThreadId tid, void* p_old, SizeT new_req_szB )
    if (new_req_szB <= bk->req_szB) {
 
       // New size is smaller or same; block not moved.
-      apinfo_change_cur_bytes_live(bk->ap,
-                                   (Long)new_req_szB - (Long)bk->req_szB);
+      resize_Block(bk->ap, bk->req_szB, new_req_szB);
       bk->req_szB = new_req_szB;
       return p_old;
 
@@ -595,14 +642,13 @@ void* renew_block ( ThreadId tid, void* p_old, SizeT new_req_szB )
       // is still alive
 
       // Update the metadata.
-      apinfo_change_cur_bytes_live(bk->ap,
-                                   (Long)new_req_szB - (Long)bk->req_szB);
+      resize_Block(bk->ap, bk->req_szB, new_req_szB);
       bk->payload = (Addr)p_new;
       bk->req_szB = new_req_szB;
 
       // and re-add
       Bool present
-         = VG_(addToFM)( interval_tree, (UWord)bk, (UWord)0/*no val*/); 
+         = VG_(addToFM)( interval_tree, (UWord)bk, (UWord)0/*no val*/);
       tl_assert(!present);
       fbc_cache0 = fbc_cache1 = NULL;
 
@@ -670,10 +716,10 @@ static void* dh_realloc ( ThreadId tid, void* p_old, SizeT new_szB )
 }
 
 static SizeT dh_malloc_usable_size ( ThreadId tid, void* p )
-{                                                            
+{
    Block* bk = find_Block_containing( (Addr)p );
    return bk ? bk->req_szB : 0;
-}                                                            
+}
 
 
 //------------------------------------------------------------//
@@ -702,7 +748,7 @@ void dh_handle_write ( Addr addr, UWord szB )
 {
    Block* bk = find_Block_containing(addr);
    if (bk) {
-      bk->n_writes += szB;
+      bk->writes_bytes += szB;
       if (bk->histoW)
          inc_histo_for_block(bk, addr, szB);
    }
@@ -713,7 +759,7 @@ void dh_handle_read ( Addr addr, UWord szB )
 {
    Block* bk = find_Block_containing(addr);
    if (bk) {
-      bk->n_reads += szB;
+      bk->reads_bytes += szB;
       if (bk->histoW)
          inc_histo_for_block(bk, addr, szB);
    }
@@ -778,13 +824,13 @@ void add_counter_update(IRSB* sbOut, Int n)
    #else
    # error "Unknown endianness"
    #endif
-   // Add code to increment 'g_guest_instrs_executed' by 'n', like this:
-   //   WrTmp(t1, Load64(&g_guest_instrs_executed))
+   // Add code to increment 'g_curr_instrs' by 'n', like this:
+   //   WrTmp(t1, Load64(&g_curr_instrs))
    //   WrTmp(t2, Add64(RdTmp(t1), Const(n)))
-   //   Store(&g_guest_instrs_executed, t2)
+   //   Store(&g_curr_instrs, t2)
    IRTemp t1 = newIRTemp(sbOut->tyenv, Ity_I64);
    IRTemp t2 = newIRTemp(sbOut->tyenv, Ity_I64);
-   IRExpr* counter_addr = mkIRExpr_HWord( (HWord)&g_guest_instrs_executed );
+   IRExpr* counter_addr = mkIRExpr_HWord( (HWord)&g_curr_instrs );
 
    IRStmt* st1 = assign(t1, IRExpr_Load(END, Ity_I64, counter_addr));
    IRStmt* st2 = assign(t2, binop(Iop_Add64, mkexpr(t1), mkU64(n)));
@@ -850,7 +896,7 @@ void addMemEvent(IRSB* sbOut, Bool isWrite, Int szB, IRExpr* addr,
    addStmtToIRSB(
       sbOut,
       assign(diff,
-             tyAddr == Ity_I32 
+             tyAddr == Ity_I32
                 ? binop(Iop_Sub32, addr, mkexpr(sp_minus_rz))
                 : binop(Iop_Sub64, addr, mkexpr(sp_minus_rz)))
    );
@@ -859,7 +905,7 @@ void addMemEvent(IRSB* sbOut, Bool isWrite, Int szB, IRExpr* addr,
    addStmtToIRSB(
       sbOut,
       assign(guard,
-             tyAddr == Ity_I32 
+             tyAddr == Ity_I32
                 ? binop(Iop_CmpLT32U, mkU32(THRESH), mkexpr(diff))
                 : binop(Iop_CmpLT64U, mkU64(THRESH), mkexpr(diff)))
    );
@@ -886,7 +932,7 @@ IRSB* dh_instrument ( VgCallbackClosure* closure,
    // - just before any Ist_Exit statements;
    // - just before the IRSB's end.
    // In the former case, we zero 'n' and then continue instrumenting.
-   
+
    sbOut = deepCopyIRSBExceptStmts(sbIn);
 
    // Copy verbatim any IR preamble preceding the first IMark
@@ -895,10 +941,10 @@ IRSB* dh_instrument ( VgCallbackClosure* closure,
       addStmtToIRSB( sbOut, sbIn->stmts[i] );
       i++;
    }
-   
+
    for (/*use current i*/; i < sbIn->stmts_used; i++) {
       IRStmt* st = sbIn->stmts[i];
-      
+
       if (!st || st->tag == Ist_NoOp) continue;
 
       switch (st->tag) {
@@ -933,7 +979,7 @@ IRSB* dh_instrument ( VgCallbackClosure* closure,
          case Ist_Store: {
             IRExpr* data  = st->Ist.Store.data;
             IRExpr* aexpr = st->Ist.Store.addr;
-            addMemEvent( sbOut, True/*isWrite*/, 
+            addMemEvent( sbOut, True/*isWrite*/,
                          sizeofIRType(typeOfIRExpr(tyenv, data)),
                          aexpr, goff_sp );
             break;
@@ -1027,28 +1073,11 @@ IRSB* dh_instrument ( VgCallbackClosure* closure,
 //--- Command line args                                    ---//
 //------------------------------------------------------------//
 
-// FORWARDS
-static Bool identify_metric ( /*OUT*/ULong(**get_metricP)(APInfo*),
-                              /*OUT*/Bool* increasingP,
-                              const HChar* metric_name );
-
-static Int    clo_show_top_n = 10;
-static const HChar *clo_sort_by = "max-bytes-live";
+static const HChar* clo_dhat_out_file = "dhat.out.%p";
 
 static Bool dh_process_cmd_line_option(const HChar* arg)
 {
-   if VG_BINT_CLO(arg, "--show-top-n", clo_show_top_n, 1, 100000) {}
-
-   else if VG_STR_CLO(arg, "--sort-by", clo_sort_by) {
-       ULong (*dummyFn)(APInfo*);
-       Bool dummyB;
-       Bool ok = identify_metric( &dummyFn, &dummyB, clo_sort_by);
-       if (!ok)
-          return False;
-       // otherwise it's OK, in which case leave it alone.
-       // show_top_n_apinfos will later convert the string by a
-       // second call to identify_metric.
-   }
+   if VG_STR_CLO(arg, "--dhat-out-file", clo_dhat_out_file) {}
 
    else
       return VG_(replacement_malloc_process_cmd_line_option)(arg);
@@ -1056,18 +1085,10 @@ static Bool dh_process_cmd_line_option(const HChar* arg)
    return True;
 }
 
-
 static void dh_print_usage(void)
 {
    VG_(printf)(
-"    --show-top-n=number       show the top <number> alloc points [10]\n"
-"    --sort-by=string\n"
-"            sort the allocation points by the metric\n"
-"            defined by <string>, thusly:\n"
-"                max-bytes-live    maximum live bytes [default]\n"
-"                tot-bytes-allocd  bytes allocated in total (turnover)\n"
-"                max-blocks-live   maximum live blocks\n"
-"                tot-blocks-allocd blocks allocated in total (turnover)\n"
+"    --dhat-out-file=<file>  output file name [dhat.out.%%p]\n"
    );
 }
 
@@ -1083,204 +1104,236 @@ static void dh_print_debug_usage(void)
 //--- Finalisation                                         ---//
 //------------------------------------------------------------//
 
-static void show_N_div_100( /*OUT*/HChar* buf, ULong n )
+// File format notes.
+//
+// - The files are JSON, because it's a widely-used format and saves us having
+//   to write a parser in dh_view.js.
+//
+// - We use a comma-first style for the generated JSON. Comma-first style
+//   moves the special case for arrays/objects from the last item to the
+//   first. This helps in cases where you can't easily tell in advance the
+//   size of arrays/objects, such as iterating over a WordFM (because
+//   VG_(sizeFM) is O(n) rather than O(1)), and iterating over stack frames
+//   using VG_(apply_ExeContext) in combination with an InlIpCursor.
+//
+// - We use short field names and minimal whitespace to minimize file sizes.
+
+static VgFile* fp;
+
+#define FP(format, args...) ({ VG_(fprintf)(fp, format, ##args); })
+
+// The frame table holds unique frames.
+static WordFM* frame_tbl = NULL;
+static UWord next_frame_n = 0;
+
+static Word frame_cmp(UWord a, UWord b)
 {
-   ULong nK = n / 100;
-   ULong nR = n % 100;
-   VG_(sprintf)(buf, "%llu.%s%llu", nK,
-                nR < 10 ? "0" : "",
-                nR);
+   return VG_(strcmp)((const HChar*)a, (const HChar*)b);
 }
 
-static void show_APInfo ( APInfo* api )
+static HChar hex_digit_to_ascii_char(UChar d)
 {
-   HChar bufA[80];   // large enough
-   VG_(memset)(bufA, 0, sizeof(bufA));
-   if (api->tot_blocks > 0) {
-      show_N_div_100( bufA, ((ULong)api->tot_bytes * 100ULL)
-                              / (ULong)api->tot_blocks );
-   } else {
-      bufA[0] = 'N'; bufA[1] = 'a'; bufA[2] = 'N';
-   }
+   d = d & 0xf;
+   return (d < 10) ? ('0' + d) : ('a' + (d - 10));
+}
 
-   VG_(umsg)("max-live:    %'llu in %'llu blocks\n",
-             api->max_bytes_live, api->max_blocks_live);
-   VG_(umsg)("tot-alloc:   %'llu in %'llu blocks (avg size %s)\n",
-             api->tot_bytes, api->tot_blocks, bufA);
-
-   tl_assert(api->tot_blocks >= api->max_blocks_live);
-   tl_assert(api->tot_bytes >= api->max_bytes_live);
-
-   if (api->deaths > 0) {
-      // Average Age at Death
-      ULong aad = api->deaths == 0
-                  ? 0 : (api->death_ages_sum / api->deaths);
-      // AAD as a fraction of the total program lifetime (so far)
-      // measured in ten-thousand-ths (aad_frac_10k == 10000 means the
-      // complete lifetime of the program.
-      ULong aad_frac_10k
-         = g_guest_instrs_executed == 0
-           ? 0 : (10000ULL * aad) / g_guest_instrs_executed;
-      HChar buf[80];  // large enough
-      show_N_div_100(buf, aad_frac_10k);
-      VG_(umsg)("deaths:      %'llu, at avg age %'llu "
-                "(%s%% of prog lifetime)\n",
-                api->deaths, aad, buf );
-   } else {
-      VG_(umsg)("deaths:      none (none of these blocks were freed)\n");
+// For JSON, we must escape double quote, backslash, and 0x00..0x1f.
+//
+// Returns the original string if no escaping was required. Returns a pointer
+// to a static buffer if escaping was required. Therefore, the return value is
+// only valid until the next call to this function.
+static const HChar* json_escape(const HChar* s)
+{
+   static HChar* buf = NULL;
+   static SizeT bufcap = 0;
+
+   // Do we need any escaping?
+   SizeT extra = 0;
+   const HChar* p = s;
+   while (*p) {
+      UChar c = *p;
+      if (c == '"' || c == '\\') {
+         extra += 1;
+      } else if (c <= 0x1f) {
+         extra += 5;
+      }
+      p++;
    }
+   SizeT len = p - s;
 
-   HChar bufR[80], bufW[80];   // large enough
-   VG_(memset)(bufR, 0, sizeof(bufR));
-   VG_(memset)(bufW, 0, sizeof(bufW));
-   if (api->tot_bytes > 0) {
-      show_N_div_100(bufR, (100ULL * api->n_reads) / api->tot_bytes);
-      show_N_div_100(bufW, (100ULL * api->n_writes) / api->tot_bytes);
-   } else {
-      VG_(strcat)(bufR, "Inf");
-      VG_(strcat)(bufW, "Inf");
+   if (extra == 0) {
+      // No escaping needed.
+      return s;
    }
 
-   VG_(umsg)("acc-ratios:  %s rd, %s wr "
-             " (%'llu b-read, %'llu b-written)\n",
-             bufR, bufW,
-             api->n_reads, api->n_writes);
-
-   VG_(pp_ExeContext)(api->ap);
+   // Escaping needed. (The +1 is for the NUL terminator.) Enlarge buf if
+   // necessary.
+   SizeT newcap = len + extra + 1;
+   if (bufcap < newcap) {
+      buf = VG_(realloc)("dh.json", buf, newcap);
+      bufcap = newcap;
+   }
 
-   if (api->histo && api->xsize_tag == Exactly) {
-      VG_(umsg)("\nAggregated access counts by offset:\n");
-      VG_(umsg)("\n");
-      UWord i;
-      if (api->xsize > 0)
-         VG_(umsg)("[   0]  ");
-      for (i = 0; i < api->xsize; i++) {
-         if (i > 0 && (i % 16) == 0 && i != api->xsize-1) {
-            VG_(umsg)("\n");
-            VG_(umsg)("[%4lu]  ", i);
-         }
-         VG_(umsg)("%u ", api->histo[i]);
+   p = s;
+   HChar* q = buf;
+   while (*p) {
+      UChar c = *p;
+      if (c == '"') {
+         *q++ = '\\';
+         *q++ = '"';
+      } else if (c == '\\') {
+         *q++ = '\\';
+         *q++ = '\\';
+      } else if (c <= 0x1f) {
+         *q++ = '\\';
+         *q++ = 'u';
+         *q++ = '0';
+         *q++ = '0';
+         *q++ = hex_digit_to_ascii_char((c & 0x00f0) >> 4);
+         *q++ = hex_digit_to_ascii_char(c & 0x000f);
+      } else {
+         *q++ = c;
       }
-      VG_(umsg)("\n");
+      p++;
    }
-}
+   *q = '\0';
 
-
-/* Metric-access functions for APInfos. */
-static ULong get_metric__max_bytes_live ( APInfo* api ) {
-   return api->max_bytes_live;
-}
-static ULong get_metric__tot_bytes ( APInfo* api ) {
-   return api->tot_bytes;
-}
-static ULong get_metric__max_blocks_live ( APInfo* api ) {
-   return api->max_blocks_live;
-}
-static ULong get_metric__tot_blocks ( APInfo* api ) {
-   return api->tot_blocks;
+   return buf;
 }
 
-/* Given a string, return the metric-access function and also a Bool
-   indicating whether we want increasing or decreasing values of the
-   metric.  This is used twice, once in command line processing, and
-   then again in show_top_n_apinfos.  Returns False if the given
-   string could not be identified.*/
-static Bool identify_metric ( /*OUT*/ULong(**get_metricP)(APInfo*),
-                              /*OUT*/Bool* increasingP,
-                              const HChar* metric_name )
+static void write_APInfo_frame(UInt n, DiEpoch ep, Addr ip, void* opaque)
 {
-   if (0 == VG_(strcmp)(metric_name, "max-bytes-live")) {
-      *get_metricP = get_metric__max_bytes_live;
-      *increasingP = False;
-      return True;
-   }
-   if (0 == VG_(strcmp)(metric_name, "tot-bytes-allocd")) {
-      *get_metricP = get_metric__tot_bytes;
-      *increasingP = False;
-      return True;
-   }
-   if (0 == VG_(strcmp)(metric_name, "max-blocks-live")) {
-      *get_metricP = get_metric__max_blocks_live;
-      *increasingP = False;
-      return True;
-   }
-   if (0 == VG_(strcmp)(metric_name, "tot-blocks-allocd")) {
-      *get_metricP = get_metric__tot_blocks;
-      *increasingP = False;
-      return True;
-   }
-   return False;
-}
+   Bool* is_first = (Bool*)opaque;
+   InlIPCursor* iipc = VG_(new_IIPC)(ep, ip);
 
+   do {
+      const HChar* buf = VG_(describe_IP)(ep, ip, iipc);
 
-static void show_top_n_apinfos ( void )
-{
-   Int   i;
-   UWord keyW, valW;
-   ULong (*get_metric)(APInfo*);
-   Bool  increasing;
+      // Skip entries in vg_replace_malloc.c (e.g. `malloc`, `calloc`,
+      // `realloc`, `operator new`) because they're boring and clog up the
+      // output.
+      if (VG_(strstr)(buf, "vg_replace_malloc.c")) {
+         continue;
+      }
 
-   const HChar* metric_name = clo_sort_by;
-   tl_assert(metric_name); // ensured by clo processing
+      // If this description has been seen before, get its number. Otherwise,
+      // give it a new number and put it in the table.
+      UWord keyW = 0, valW = 0;
+      UWord frame_n = 0;
+      Bool found = VG_(lookupFM)(frame_tbl, &keyW, &valW, (UWord)buf);
+      if (found) {
+         //const HChar* str = (const HChar*)keyW;
+         //tl_assert(0 == VG_(strcmp)(buf, str));
+         frame_n = valW;
+      } else {
+         // `buf` is a static buffer, we must copy it.
+         const HChar* str = VG_(strdup)("dh.frame_tbl.3", buf);
+         frame_n = next_frame_n++;
+         Bool present = VG_(addToFM)(frame_tbl, (UWord)str, frame_n);
+         tl_assert(!present);
+      }
 
-   Bool ok = identify_metric( &get_metric, &increasing, metric_name );
-   tl_assert(ok); // ensured by clo processing
+      FP("%c%lu", *is_first ? '[' : ',', frame_n);
+      *is_first = False;
 
-   VG_(umsg)("\n");
-   VG_(umsg)("======== ORDERED BY %s \"%s\": "
-             "top %d allocators ========\n", 
-             increasing ? "increasing" : "decreasing",
-             metric_name, clo_show_top_n );
+   } while (VG_(next_IIPC)(iipc));
 
-   // Clear all .shown bits
-   VG_(initIterFM)( apinfo );
-   while (VG_(nextIterFM)( apinfo, &keyW, &valW )) {
-      APInfo* api = (APInfo*)valW;
-      tl_assert(api && api->ap == (ExeContext*)keyW);
-      api->shown = False;
-   }
-   VG_(doneIterFM)( apinfo );
+   VG_(delete_IIPC)(iipc);
+};
 
-   // Now print the top N entries.  Each one requires a 
-   // complete scan of the set.  Duh.
-   for (i = 0; i < clo_show_top_n; i++) {
-      ULong   best_metric = increasing ? ~0ULL : 0ULL;
-      APInfo* best_api    = NULL;
+static void write_APInfo(APInfo* api, Bool is_first)
+{
+   tl_assert(api->total_blocks >= api->max_blocks);
+   tl_assert(api->total_bytes >= api->max_bytes);
+
+   FP(" %c{\"tb\":%llu,\"tbk\":%llu,\"tli\":%llu\n",
+      is_first ? '[' : ',',
+      api->total_bytes, api->total_blocks, api->total_lifetimes_instrs);
+   FP("  ,\"mb\":%llu,\"mbk\":%llu\n",
+      api->max_bytes, api->max_blocks);
+   FP("  ,\"gb\":%llu,\"gbk\":%llu\n",
+      api->at_tgmax_bytes, api->at_tgmax_blocks);
+   FP("  ,\"fb\":%llu,\"fbk\":%llu\n",
+      api->curr_bytes, api->curr_blocks);
+   FP("  ,\"rb\":%llu,\"wb\":%llu\n",
+      api->reads_bytes, api->writes_bytes);
 
-      VG_(initIterFM)( apinfo );
-      while (VG_(nextIterFM)( apinfo, &keyW, &valW )) {
-         APInfo* api = (APInfo*)valW;
-         if (api->shown)
-            continue;
-         ULong metric = get_metric(api);
-         if (increasing ? (metric < best_metric) : (metric > best_metric)) {
-            best_metric = metric;
-            best_api = api;
+   if (api->histo && api->xsize_tag == Exactly) {
+      FP("  ,\"acc\":[");
+
+      // Simple run-length encoding: when N entries in a row have the same
+      // value M, we print "-N,M". If there is just one in a row, we just
+      // print "M". This reduces file size significantly.
+      UShort repval = 0;
+      Int reps = 0;
+      for (UWord i = 0; i < api->xsize; i++) {
+         UShort h = api->histo[i];
+         if (repval == h) {
+            // Continue current run.
+            reps++;
+         } else {
+            // End of run; print it.
+            if (reps == 1) {
+               FP("%u,", repval);
+            } else if (reps > 1) {
+               FP("-%d,%u,", reps, repval);
+            }
+            reps = 1;
+            repval = h;
          }
       }
-      VG_(doneIterFM)( apinfo );
-
-      if (!best_api)
-         break; // all APIs have been shown.  Stop.
+      // Print the final run.
+      if (reps == 1) {
+         FP("%u", repval);
+      } else if (reps > 1) {
+         FP("-%d,%u", reps, repval);
+      }
 
-      VG_(umsg)("\n");
-      VG_(umsg)("-------------------- %d of %d --------------------\n",
-                i+1, clo_show_top_n );
-      show_APInfo(best_api);
-      best_api->shown = True;
+      FP("]\n");
    }
 
-   VG_(umsg)("\n");
+   FP("  ,\"fs\":");
+   Bool is_first_frame = True;
+   VG_(apply_ExeContext)(write_APInfo_frame, &is_first_frame, api->ap);
+   FP("]\n");
+
+   FP("  }\n");
 }
 
+static void write_APInfos(void)
+{
+   UWord keyW, valW;
+
+   FP(",\"aps\":\n");
+
+   VG_(initIterFM)(apinfo);
+   Bool is_first = True;
+   while (VG_(nextIterFM)(apinfo, &keyW, &valW)) {
+      APInfo* api = (APInfo*)valW;
+      tl_assert(api && api->ap == (ExeContext*)keyW);
+      write_APInfo(api, is_first);
+      is_first = False;
+   }
+   VG_(doneIterFM)(apinfo);
+
+   if (is_first) {
+      // We didn't print any elements. This happens if apinfo is empty.
+      FP(" [\n");
+   }
+
+   FP(" ]\n");
+}
 
 static void dh_fini(Int exit_status)
 {
-   // Before printing statistics, we must harvest access counts for
-   // all the blocks that are still alive.  Not doing so gives
-   // access ratios which are too low (zero, in the worst case)
-   // for such blocks, since the accesses that do get made will
-   // (if we skip this step) not get folded into the AP summaries.
+   // This function does lots of allocations that it doesn't bother to free,
+   // because execution is almost over anyway.
+
+   // Total bytes might be at a possible peak.
+   check_for_peak();
+
+   // Before printing statistics, we must harvest various stats (such as
+   // lifetimes and accesses) for all the blocks that are still alive.
    UWord keyW, valW;
    VG_(initIterFM)( interval_tree );
    while (VG_(nextIterFM)( interval_tree, &keyW, &valW )) {
@@ -1291,46 +1344,7 @@ static void dh_fini(Int exit_status)
    }
    VG_(doneIterFM)( interval_tree );
 
-   // show results
-   VG_(umsg)("======== SUMMARY STATISTICS ========\n");
-   VG_(umsg)("\n");
-   VG_(umsg)("guest_insns:  %'llu\n", g_guest_instrs_executed);
-   VG_(umsg)("\n");
-   VG_(umsg)("max_live:     %'llu in %'llu blocks\n",
-             g_max_bytes_live, g_max_blocks_live);
-   VG_(umsg)("\n");
-   VG_(umsg)("tot_alloc:    %'llu in %'llu blocks\n",
-             g_tot_bytes, g_tot_blocks);
-   VG_(umsg)("\n");
-   if (g_tot_bytes > 0) {
-      VG_(umsg)("insns per allocated byte: %'llu\n",
-                g_guest_instrs_executed / g_tot_bytes);
-      VG_(umsg)("\n");
-   }
-
-   show_top_n_apinfos();
-
-   VG_(umsg)("\n");
-   VG_(umsg)("\n");
-   VG_(umsg)("==============================================================\n");
-   VG_(umsg)("\n");
-   VG_(umsg)("Some hints: (see --help for command line option details):\n");
-   VG_(umsg)("\n");
-   VG_(umsg)("* summary stats for whole program are at the top of this output\n");
-   VG_(umsg)("\n");
-   VG_(umsg)("* --show-top-n=  controls how many alloc points are shown.\n");
-   VG_(umsg)("                 You probably want to set it much higher than\n");
-   VG_(umsg)("                 the default value (10)\n");
-   VG_(umsg)("\n");
-   VG_(umsg)("* --sort-by=     specifies the sort key for output.\n");
-   VG_(umsg)("                 See --help for details.\n");
-   VG_(umsg)("\n");
-   VG_(umsg)("* Each allocation stack, by default 12 frames, counts as\n");
-   VG_(umsg)("  a separate alloc point.  This causes the data to be spread out\n");
-   VG_(umsg)("  over far too many alloc points.  I strongly suggest using\n");
-   VG_(umsg)("  --num-callers=4 or some such, to reduce the spreading.\n");
-   VG_(umsg)("\n");
-
+   // Stats.
    if (VG_(clo_stats)) {
       VG_(dmsg)(" dhat: find_Block_containing:\n");
       VG_(dmsg)("             found: %'lu (%'lu cached + %'lu uncached)\n",
@@ -1340,8 +1354,94 @@ static void dh_fini(Int exit_status)
       VG_(dmsg)("          notfound: %'lu\n", stats__n_fBc_notfound);
       VG_(dmsg)("\n");
    }
-}
 
+   // Create the frame table, and insert the special "[root]" node at index 0.
+   frame_tbl = VG_(newFM)(VG_(malloc),
+                          "dh.frame_tbl.1",
+                          VG_(free),
+                          frame_cmp);
+   const HChar* root = VG_(strdup)("dh.frame_tbl.2", "[root]");
+   Bool present = VG_(addToFM)(frame_tbl, (UWord)root, 0);
+   tl_assert(!present);
+   next_frame_n = 1;
+
+   // Setup output filename. Nb: it's important to do this now, i.e. as late
+   // as possible. If we do it at start-up and the program forks and the
+   // output file format string contains a %p (pid) specifier, both the parent
+   // and child will incorrectly write to the same file; this happened in
+   // 3.3.0.
+   HChar* dhat_out_file =
+      VG_(expand_file_name)("--dhat-out-file", clo_dhat_out_file);
+
+   fp = VG_(fopen)(dhat_out_file, VKI_O_CREAT|VKI_O_TRUNC|VKI_O_WRONLY,
+                   VKI_S_IRUSR|VKI_S_IWUSR);
+   if (!fp) {
+      VG_(umsg)("error: can't open DHAT output file '%s'\n", dhat_out_file);
+      return;
+   }
+
+   // Write to data file.
+   FP("{\"dhatFileVersion\":1\n");
+
+   // The command.
+   const HChar* exe = VG_(args_the_exename);
+   FP(",\"cmd\":\"%s", json_escape(exe));
+   for (Word i = 0; i < VG_(sizeXA)(VG_(args_for_client)); i++) {
+      const HChar* arg = *(HChar**)VG_(indexXA)(VG_(args_for_client), i);
+      FP(" %s", json_escape(arg));
+   }
+   FP("\"\n");
+
+   // The PID.
+   FP(",\"pid\":%d\n", VG_(getpid)());
+
+   // Times.
+   FP(",\"mi\":%llu,\"ei\":%llu\n", g_max_instrs, g_curr_instrs);
+
+   // APs.
+   write_APInfos();
+
+   // Frame table.
+   FP(",\"ftbl\":\n");
+
+   // The frame table maps strings to numbers. We want to print it ordered by
+   // numbers. So we create an array and fill it in from the frame table, then
+   // print that.
+   UWord n_frames = next_frame_n;
+   const HChar** frames =
+      VG_(malloc)("dh.frames", n_frames * sizeof(const HChar*));
+   VG_(initIterFM)(frame_tbl);
+   while (VG_(nextIterFM)(frame_tbl, &keyW, &valW)) {
+      const HChar* str = (const HChar*)keyW;
+      UWord n = valW;
+      frames[n] = str;
+   }
+   VG_(doneIterFM)(frame_tbl);
+
+   for (UWord i = 0; i < n_frames; i++) {
+      FP(" %c\"%s\"\n", i == 0 ? '[' : ',', json_escape(frames[i]));
+   }
+   FP(" ]\n");
+
+   FP("}\n");
+
+   VG_(fclose)(fp);
+   fp = NULL;
+
+   if (VG_(clo_verbosity) == 0) {
+      return;
+   }
+
+   // Print brief global stats.
+   VG_(umsg)("Total:     %'llu bytes in %'llu blocks\n",
+             g_total_bytes, g_total_blocks);
+   VG_(umsg)("At t-gmax: %'llu bytes in %'llu blocks\n",
+             g_max_bytes, g_max_blocks);
+   VG_(umsg)("At t-end:  %'llu bytes in %'llu blocks\n",
+             g_curr_bytes, g_curr_blocks);
+   VG_(umsg)("Reads:     %'llu bytes\n", g_reads_bytes);
+   VG_(umsg)("Writes:    %'llu bytes\n", g_writes_bytes);
+}
 
 //------------------------------------------------------------//
 //--- Initialisation                                       ---//
@@ -1357,7 +1457,7 @@ static void dh_pre_clo_init(void)
    VG_(details_version)         (NULL);
    VG_(details_description)     ("a dynamic heap analysis tool");
    VG_(details_copyright_author)(
-      "Copyright (C) 2010-2017, and GNU GPL'd, by Mozilla Inc");
+      "Copyright (C) 2010-2018, and GNU GPL'd, by Mozilla Foundation");
    VG_(details_bug_reports_to)  (VG_BUGS_TO);
 
    // Basic functions.
@@ -1395,12 +1495,12 @@ static void dh_pre_clo_init(void)
    tl_assert(!fbc_cache1);
 
    interval_tree = VG_(newFM)( VG_(malloc),
-                               "dh.main.interval_tree.1",
+                               "dh.interval_tree.1",
                                VG_(free),
                                interval_tree_Cmp );
 
    apinfo = VG_(newFM)( VG_(malloc),
-                        "dh.main.apinfo.1",
+                        "dh.apinfo.1",
                         VG_(free),
                         NULL/*unboxedcmp*/ );
 }
diff --git a/dhat/dh_test.js b/dhat/dh_test.js
new file mode 100644 (file)
index 0000000..7a15261
--- /dev/null
@@ -0,0 +1,2553 @@
+
+//--------------------------------------------------------------------*/
+//--- DHAT: a Dynamic Heap Analysis Tool                dh_test.js ---*/
+//--------------------------------------------------------------------*/
+
+/*
+   This file is part of DHAT, a Valgrind tool for profiling the
+   heap usage of programs.
+
+   Copyright (C) 2018 Mozilla Foundation
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2 of the
+   License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+   02111-1307, USA.
+
+   The GNU General Public License is contained in the file COPYING.
+*/
+
+// We can't fully automate testing of a web app via the normal Valgrind
+// regression testing. Instead we have this code, which is executed when
+// dh_view.html is loaded with a "?test=1" parameter.
+//
+// Things tested by this file:
+// - Tree building, with multiple sort metrics.
+// - Text content of the displayed tree.
+//
+// Things not tested by this file:
+// - Output from DHAT itself (unless that output is regenerated when necessary
+//   and copy-and-pasted in the "input" fields in this file).
+// - Interactions with the "Load" button and "Sort metric" menu.
+// - File loading and parsing.
+// - Non-text content of the displayed tree (e.g. node colours, sortKey
+//   highlighting).
+// - Tree interactions (collapsing and expanding of nodes).
+
+"use strict";
+
+// Test inputs are copied verbatim from DHAT output files, not as strings but
+// as actual JavaScript code. This works because output files are JSON, and
+// JSON is valid JavaScript.
+//
+// Expected outputs are paired with a sort metric, and copied verbatim from the
+// DHAT viewer.
+let tests = []
+
+//---------------------------------------------------------------------------
+// empty (corresponds to dhat/tests/empty.c)
+//---------------------------------------------------------------------------
+
+let empty = {
+  name: "empty",
+  input:
+//---------------------------------------------------------------------------
+{"dhatFileVersion":1
+,"cmd":"./empty"
+,"pid":23431
+,"mi":0,"ei":248602
+,"aps":
+ [
+ ]
+,"ftbl":
+ ["[root]"
+ ]
+}
+//---------------------------------------------------------------------------
+  ,
+  outputs: [
+    {
+      label: "Total (bytes)",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: ./empty
+  PID:     23431
+}
+
+Times {
+  t-gmax: 0 instrs (0% of program duration)
+  t-end:  248,602 instrs
+}
+
+─ AP 1/1 {
+    Total:     0 bytes (0%, 0/Minstr) in 0 blocks (0%, 0/Minstr), avg size 0 bytes, avg lifetime 0 instrs (0% of program duration)
+    At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+    At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+    Reads:     0 bytes (0%, 0/Minstr), 0/byte
+    Writes:    0 bytes (0%, 0/Minstr), 0/byte
+    Allocated at {
+      #0: [root]
+    }
+  }
+
+AP significance threshold: total >= 0 bytes (0%)
+`
+//---------------------------------------------------------------------------
+    }
+  ]
+};
+tests.push(empty);
+
+//---------------------------------------------------------------------------
+// single (corresponds to dhat/tests/single.c)
+//---------------------------------------------------------------------------
+
+let single = {
+  name: "single",
+  input:
+//---------------------------------------------------------------------------
+{"dhatFileVersion":1
+,"cmd":"./single"
+,"pid":30563
+,"mi":242900,"ei":249824
+,"aps":
+ [{"tb":16,"tbk":1,"tli":6924
+  ,"mb":16,"mbk":1
+  ,"gb":16,"gbk":1
+  ,"fb":16,"fbk":1
+  ,"rb":0,"wb":12
+  ,"acc":[-4,3,-12,0]
+  ,"fs":[1]
+  }
+ ]
+,"ftbl":
+ ["[root]"
+ ,"0x10865B: main (single.cpp:4)"
+ ]
+}
+//---------------------------------------------------------------------------
+  ,
+  outputs: [
+    {
+      label: "Total (bytes)",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: ./single
+  PID:     30563
+}
+
+Times {
+  t-gmax: 242,900 instrs (97.23% of program duration)
+  t-end:  249,824 instrs
+}
+
+─ AP 1/1 {
+    Total:     16 bytes (100%, 64.05/Minstr) in 1 blocks (100%, 4/Minstr), avg size 16 bytes, avg lifetime 6,924 instrs (2.77% of program duration)
+    At t-gmax: 16 bytes (100%) in 1 blocks (100%), avg size 16 bytes
+    At t-end:  16 bytes (100%) in 1 blocks (100%), avg size 16 bytes
+    Reads:     0 bytes (0%, 0/Minstr), 0/byte
+    Writes:    12 bytes (100%, 48.03/Minstr), 0.75/byte
+    Accesses: {
+      [  0]  3 ã€ƒ ã€ƒ ã€ƒ - - - - - - - - - - - - 
+    }
+    Allocated at {
+      #0: [root]
+      #1: 0x10865B: main (single.cpp:4)
+    }
+  }
+
+AP significance threshold: total >= 0.16 bytes (1%)
+`
+//---------------------------------------------------------------------------
+    }
+  ]
+};
+tests.push(single);
+
+//---------------------------------------------------------------------------
+// subseqs (a synthetic test for locations that are subsequences of other
+// locations, which are rare but can happen in practice, esp. with recursion)
+//---------------------------------------------------------------------------
+
+let subseqs = {
+  name: "subseqs",
+  input:
+//---------------------------------------------------------------------------
+{"dhatFileVersion":1
+,"cmd":"subseqs"
+,"pid":0
+,"mi":10000,"ei":20000
+,"aps":
+ [{"tb":15,"tbk":1,"tli":1000
+  ,"mb":15,"mbk":1
+  ,"gb":15,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-15,0]
+  ,"fs":[1,2,3]
+  }
+ ,{"tb":14,"tbk":1,"tli":1000
+  ,"mb":14,"mbk":1
+  ,"gb":14,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-14,0]
+  ,"fs":[1,2,3,3]
+  }
+ ,{"tb":13,"tbk":1,"tli":1000
+  ,"mb":13,"mbk":1
+  ,"gb":13,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-13,0]
+  ,"fs":[1,2,3,3,3]
+  }
+ ,{"tb":12,"tbk":1,"tli":1000
+  ,"mb":12,"mbk":1
+  ,"gb":12,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-12,0]
+  ,"fs":[4,5,6,6,6]
+  }
+ ,{"tb":11,"tbk":1,"tli":1000
+  ,"mb":11,"mbk":1
+  ,"gb":11,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-11,0]
+  ,"fs":[4,5,6,6]
+  }
+ ,{"tb":10,"tbk":1,"tli":1000
+  ,"mb":10,"mbk":1
+  ,"gb":10,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[4,5,6]
+  }
+ ,{"tb":9,"tbk":1,"tli":1000
+  ,"mb":9,"mbk":1
+  ,"gb":9,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-9,0]
+  ,"fs":[7,8,9]
+  }
+ ,{"tb":8,"tbk":1,"tli":1000
+  ,"mb":8,"mbk":1
+  ,"gb":8,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-8,0]
+  ,"fs":[7,8,10]
+  }
+ ,{"tb":7,"tbk":1,"tli":1000
+  ,"mb":7,"mbk":1
+  ,"gb":7,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-7,0]
+  ,"fs":[7,8]
+  }
+ ]
+,"ftbl":
+ ["[root]"
+ ,"a()"
+ ,"b()"
+ ,"c()"
+ ,"d()"
+ ,"e()"
+ ,"f()"
+ ,"g()"
+ ,"h()"
+ ,"i()"
+ ,"j()"
+ ]
+}
+//---------------------------------------------------------------------------
+  ,
+  outputs: [
+    {
+      label: "Total (bytes)",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: subseqs
+  PID:     0
+}
+
+Times {
+  t-gmax: 10,000 instrs (50% of program duration)
+  t-end:  20,000 instrs
+}
+
+â–¼ AP 1/1 (3 children) {
+    Total:     99 bytes (100%, 4,950/Minstr) in 9 blocks (100%, 450/Minstr), avg size 11 bytes, avg lifetime 1,000 instrs (5% of program duration)
+    At t-gmax: 99 bytes (100%) in 9 blocks (100%), avg size 11 bytes
+    At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+    Reads:     0 bytes (0%, 0/Minstr), 0/byte
+    Writes:    0 bytes (0%, 0/Minstr), 0/byte
+    Allocated at {
+      #0: [root]
+    }
+  }
+  â”œâ”€â–¼ AP 1.1/3 (2 children) {
+  â”‚     Total:     42 bytes (42.42%, 2,100/Minstr) in 3 blocks (33.33%, 150/Minstr), avg size 14 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚     At t-gmax: 42 bytes (42.42%) in 3 blocks (33.33%), avg size 14 bytes
+  â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Allocated at {
+  â”‚       #1: a()
+  â”‚       #2: b()
+  â”‚       #3: c()
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â–¼ AP 1.1.1/2 (2 children) {
+  â”‚   â”‚     Total:     27 bytes (27.27%, 1,350/Minstr) in 2 blocks (22.22%, 100/Minstr), avg size 13.5 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚   â”‚     At t-gmax: 27 bytes (27.27%) in 2 blocks (22.22%), avg size 13.5 bytes
+  â”‚   â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: a()
+  â”‚   â”‚       ^2: b()
+  â”‚   â”‚       ^3: c()
+  â”‚   â”‚       #4: c()
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”‚   â”œâ”€â”€ AP 1.1.1.1/2 {
+  â”‚   â”‚   â”‚     Total:     14 bytes (14.14%, 700/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 14 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚   â”‚   â”‚     Max:       14 bytes in 1 blocks, avg size 14 bytes
+  â”‚   â”‚   â”‚     At t-gmax: 14 bytes (14.14%) in 1 blocks (11.11%), avg size 14 bytes
+  â”‚   â”‚   â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Accesses: {
+  â”‚   â”‚   â”‚       [  0]  - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚     Allocated at {
+  â”‚   â”‚   â”‚       ^1: a()
+  â”‚   â”‚   â”‚       ^2: b()
+  â”‚   â”‚   â”‚       ^3: c()
+  â”‚   â”‚   â”‚       ^4: c()
+  â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   }
+  â”‚   â”‚   â””── AP 1.1.1.2/2 {
+  â”‚   â”‚         Total:     13 bytes (13.13%, 650/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 13 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚   â”‚         Max:       13 bytes in 1 blocks, avg size 13 bytes
+  â”‚   â”‚         At t-gmax: 13 bytes (13.13%) in 1 blocks (11.11%), avg size 13 bytes
+  â”‚   â”‚         At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚         Accesses: {
+  â”‚   â”‚           [  0]  - - - - - - - - - - - - - 
+  â”‚   â”‚         }
+  â”‚   â”‚         Allocated at {
+  â”‚   â”‚           ^1: a()
+  â”‚   â”‚           ^2: b()
+  â”‚   â”‚           ^3: c()
+  â”‚   â”‚           ^4: c()
+  â”‚   â”‚           #5: c()
+  â”‚   â”‚         }
+  â”‚   â”‚       }
+  â”‚   â””── AP 1.1.2/2 {
+  â”‚         Total:     15 bytes (15.15%, 750/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 15 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚         Max:       15 bytes in 1 blocks, avg size 15 bytes
+  â”‚         At t-gmax: 15 bytes (15.15%) in 1 blocks (11.11%), avg size 15 bytes
+  â”‚         At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Accesses: {
+  â”‚           [  0]  - - - - - - - - - - - - - - - 
+  â”‚         }
+  â”‚         Allocated at {
+  â”‚           ^1: a()
+  â”‚           ^2: b()
+  â”‚           ^3: c()
+  â”‚         }
+  â”‚       }
+  â”œâ”€â–¼ AP 1.2/3 (2 children) {
+  â”‚     Total:     33 bytes (33.33%, 1,650/Minstr) in 3 blocks (33.33%, 150/Minstr), avg size 11 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚     At t-gmax: 33 bytes (33.33%) in 3 blocks (33.33%), avg size 11 bytes
+  â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Allocated at {
+  â”‚       #1: d()
+  â”‚       #2: e()
+  â”‚       #3: f()
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â–¼ AP 1.2.1/2 (2 children) {
+  â”‚   â”‚     Total:     23 bytes (23.23%, 1,150/Minstr) in 2 blocks (22.22%, 100/Minstr), avg size 11.5 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚   â”‚     At t-gmax: 23 bytes (23.23%) in 2 blocks (22.22%), avg size 11.5 bytes
+  â”‚   â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: d()
+  â”‚   â”‚       ^2: e()
+  â”‚   â”‚       ^3: f()
+  â”‚   â”‚       #4: f()
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”‚   â”œâ”€â”€ AP 1.2.1.1/2 {
+  â”‚   â”‚   â”‚     Total:     12 bytes (12.12%, 600/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 12 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚   â”‚   â”‚     Max:       12 bytes in 1 blocks, avg size 12 bytes
+  â”‚   â”‚   â”‚     At t-gmax: 12 bytes (12.12%) in 1 blocks (11.11%), avg size 12 bytes
+  â”‚   â”‚   â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Accesses: {
+  â”‚   â”‚   â”‚       [  0]  - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚     Allocated at {
+  â”‚   â”‚   â”‚       ^1: d()
+  â”‚   â”‚   â”‚       ^2: e()
+  â”‚   â”‚   â”‚       ^3: f()
+  â”‚   â”‚   â”‚       ^4: f()
+  â”‚   â”‚   â”‚       #5: f()
+  â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   }
+  â”‚   â”‚   â””── AP 1.2.1.2/2 {
+  â”‚   â”‚         Total:     11 bytes (11.11%, 550/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 11 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚   â”‚         Max:       11 bytes in 1 blocks, avg size 11 bytes
+  â”‚   â”‚         At t-gmax: 11 bytes (11.11%) in 1 blocks (11.11%), avg size 11 bytes
+  â”‚   â”‚         At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚         Accesses: {
+  â”‚   â”‚           [  0]  - - - - - - - - - - - 
+  â”‚   â”‚         }
+  â”‚   â”‚         Allocated at {
+  â”‚   â”‚           ^1: d()
+  â”‚   â”‚           ^2: e()
+  â”‚   â”‚           ^3: f()
+  â”‚   â”‚           ^4: f()
+  â”‚   â”‚         }
+  â”‚   â”‚       }
+  â”‚   â””── AP 1.2.2/2 {
+  â”‚         Total:     10 bytes (10.1%, 500/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 10 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚         Max:       10 bytes in 1 blocks, avg size 10 bytes
+  â”‚         At t-gmax: 10 bytes (10.1%) in 1 blocks (11.11%), avg size 10 bytes
+  â”‚         At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Accesses: {
+  â”‚           [  0]  - - - - - - - - - - 
+  â”‚         }
+  â”‚         Allocated at {
+  â”‚           ^1: d()
+  â”‚           ^2: e()
+  â”‚           ^3: f()
+  â”‚         }
+  â”‚       }
+  â””─▼ AP 1.3/3 (3 children) {
+        Total:     24 bytes (24.24%, 1,200/Minstr) in 3 blocks (33.33%, 150/Minstr), avg size 8 bytes, avg lifetime 1,000 instrs (5% of program duration)
+        At t-gmax: 24 bytes (24.24%) in 3 blocks (33.33%), avg size 8 bytes
+        At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+        Reads:     0 bytes (0%, 0/Minstr), 0/byte
+        Writes:    0 bytes (0%, 0/Minstr), 0/byte
+        Allocated at {
+          #1: g()
+          #2: h()
+        }
+      }
+      â”œâ”€â”€ AP 1.3.1/3 {
+      â”‚     Total:     9 bytes (9.09%, 450/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 9 bytes, avg lifetime 1,000 instrs (5% of program duration)
+      â”‚     Max:       9 bytes in 1 blocks, avg size 9 bytes
+      â”‚     At t-gmax: 9 bytes (9.09%) in 1 blocks (11.11%), avg size 9 bytes
+      â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+      â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+      â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+      â”‚     Accesses: {
+      â”‚       [  0]  - - - - - - - - - 
+      â”‚     }
+      â”‚     Allocated at {
+      â”‚       ^1: g()
+      â”‚       ^2: h()
+      â”‚       #3: i()
+      â”‚     }
+      â”‚   }
+      â”œâ”€â”€ AP 1.3.2/3 {
+      â”‚     Total:     8 bytes (8.08%, 400/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 8 bytes, avg lifetime 1,000 instrs (5% of program duration)
+      â”‚     Max:       8 bytes in 1 blocks, avg size 8 bytes
+      â”‚     At t-gmax: 8 bytes (8.08%) in 1 blocks (11.11%), avg size 8 bytes
+      â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+      â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+      â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+      â”‚     Accesses: {
+      â”‚       [  0]  - - - - - - - - 
+      â”‚     }
+      â”‚     Allocated at {
+      â”‚       ^1: g()
+      â”‚       ^2: h()
+      â”‚       #3: j()
+      â”‚     }
+      â”‚   }
+      â””── AP 1.3.3/3 {
+            Total:     7 bytes (7.07%, 350/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 7 bytes, avg lifetime 1,000 instrs (5% of program duration)
+            Max:       7 bytes in 1 blocks, avg size 7 bytes
+            At t-gmax: 7 bytes (7.07%) in 1 blocks (11.11%), avg size 7 bytes
+            At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+            Reads:     0 bytes (0%, 0/Minstr), 0/byte
+            Writes:    0 bytes (0%, 0/Minstr), 0/byte
+            Accesses: {
+              [  0]  - - - - - - - 
+            }
+            Allocated at {
+              ^1: g()
+              ^2: h()
+            }
+          }
+
+AP significance threshold: total >= 0.99 bytes (1%)
+`
+//---------------------------------------------------------------------------
+    }
+  ]
+};
+tests.push(subseqs);
+
+//---------------------------------------------------------------------------
+// acc (corresponds to dhat/tests/acc.c)
+//---------------------------------------------------------------------------
+
+let acc = {
+  name: "acc",
+  input:
+//---------------------------------------------------------------------------
+{"dhatFileVersion":1
+,"cmd":"./acc"
+,"pid":23513
+,"mi":265120,"ei":1337753
+,"aps":
+ [{"tb":32,"tbk":1,"tli":4751
+  ,"mb":32,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":496
+  ,"acc":[0,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]
+  ,"fs":[1]
+  }
+ ,{"tb":20,"tbk":1,"tli":106
+  ,"mb":20,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":0,"fbk":0
+  ,"rb":4,"wb":48
+  ,"acc":[-4,2,-4,0,-4,1,-4,0,-4,10]
+  ,"fs":[2]
+  }
+ ,{"tb":33,"tbk":1,"tli":39
+  ,"mb":33,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":1
+  ,"acc":[-32,0,1]
+  ,"fs":[3]
+  }
+ ,{"tb":1024,"tbk":1,"tli":15179
+  ,"mb":1024,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":0,"fbk":0
+  ,"rb":1024,"wb":1124
+  ,"acc":[-500,2,-100,3,-424,2]
+  ,"fs":[4]
+  }
+ ,{"tb":1025,"tbk":1,"tli":15415
+  ,"mb":1025,"mbk":1
+  ,"gb":1025,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":1025,"wb":1025
+  ,"fs":[5]
+  }
+ ,{"tb":100,"tbk":1,"tli":350084
+  ,"mb":100,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":200000
+  ,"acc":[-4,50000,-96,0]
+  ,"fs":[6,7]
+  }
+ ,{"tb":100,"tbk":1,"tli":350072
+  ,"mb":100,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":200000
+  ,"acc":[-4,50000,-96,0]
+  ,"fs":[6,8]
+  }
+ ,{"tb":100,"tbk":1,"tli":700084
+  ,"mb":100,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":400000
+  ,"acc":[-4,65535,-96,0]
+  ,"fs":[9,10]
+  }
+ ,{"tb":100,"tbk":1,"tli":700072
+  ,"mb":100,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":400000
+  ,"acc":[-4,65535,-96,0]
+  ,"fs":[9,11]
+  }
+ ]
+,"ftbl":
+ ["[root]"
+ ,"0x10871F: main (acc.c:14)"
+ ,"0x108771: main (acc.c:23)"
+ ,"0x1087CB: main (acc.c:32)"
+ ,"0x1087F0: main (acc.c:37)"
+ ,"0x10886F: main (acc.c:47)"
+ ,"0x1086F1: m1 (acc.c:7)"
+ ,"0x1088C3: main (acc.c:54)"
+ ,"0x1088D1: main (acc.c:55)"
+ ,"0x10870B: m2 (acc.c:9)"
+ ,"0x108921: main (acc.c:64)"
+ ,"0x10892F: main (acc.c:65)"
+ ]
+}
+//---------------------------------------------------------------------------
+  ,
+  outputs: [
+    {
+      // All blocks are freed, which means all "At t-end" values are 0, so
+      // sorting by atTEndBytes results in no aggregate nodes, which is what we
+      // want here.
+      label: "At t-end (bytes)",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: ./acc
+  PID:     23513
+}
+
+Times {
+  t-gmax: 265,120 instrs (19.82% of program duration)
+  t-end:  1,337,753 instrs
+}
+
+â–¼ AP 1/1 (7 children) {
+    Total:     2,534 bytes (100%, 1,894.22/Minstr) in 9 blocks (100%, 6.73/Minstr), avg size 281.56 bytes, avg lifetime 237,311.33 instrs (17.74% of program duration)
+    At t-gmax: 1,025 bytes (100%) in 1 blocks (100%), avg size 1,025 bytes
+    At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+    Reads:     2,053 bytes (100%, 1,534.66/Minstr), 0.81/byte
+    Writes:    1,202,694 bytes (100%, 899,040.41/Minstr), 474.62/byte
+    Allocated at {
+      #0: [root]
+    }
+  }
+  â”œâ”€â”€ AP 1.1/7 {
+  â”‚     Total:     1,025 bytes (40.45%, 766.21/Minstr) in 1 blocks (11.11%, 0.75/Minstr), avg size 1,025 bytes, avg lifetime 15,415 instrs (1.15% of program duration)
+  â”‚     Max:       1,025 bytes in 1 blocks, avg size 1,025 bytes
+  â”‚     At t-gmax: 1,025 bytes (100%) in 1 blocks (100%), avg size 1,025 bytes
+  â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     Reads:     1,025 bytes (49.93%, 766.21/Minstr), 1/byte
+  â”‚     Writes:    1,025 bytes (0.09%, 766.21/Minstr), 1/byte
+  â”‚     Allocated at {
+  â”‚       #1: 0x10886F: main (acc.c:47)
+  â”‚     }
+  â”‚   }
+  â”œâ”€â”€ AP 1.2/7 {
+  â”‚     Total:     1,024 bytes (40.41%, 765.46/Minstr) in 1 blocks (11.11%, 0.75/Minstr), avg size 1,024 bytes, avg lifetime 15,179 instrs (1.13% of program duration)
+  â”‚     Max:       1,024 bytes in 1 blocks, avg size 1,024 bytes
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     Reads:     1,024 bytes (49.88%, 765.46/Minstr), 1/byte
+  â”‚     Writes:    1,124 bytes (0.09%, 840.21/Minstr), 1.1/byte
+  â”‚     Accesses: {
+  â”‚       [  0]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [ 32]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [ 64]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [ 96]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [128]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [160]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [192]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [224]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [256]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [288]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [320]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [352]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [384]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [416]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [448]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [480]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 3 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [512]  3 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [544]  3 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [576]  3 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [608]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [640]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [672]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [704]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [736]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [768]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [800]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [832]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [864]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [896]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [928]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [960]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       [992]  2 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚     }
+  â”‚     Allocated at {
+  â”‚       #1: 0x1087F0: main (acc.c:37)
+  â”‚     }
+  â”‚   }
+  â”œâ”€â–¼ AP 1.3/7 (2 children) {
+  â”‚     Total:     200 bytes (7.89%, 149.5/Minstr) in 2 blocks (22.22%, 1.5/Minstr), avg size 100 bytes, avg lifetime 350,078 instrs (26.17% of program duration)
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    400,000 bytes (33.26%, 299,008.86/Minstr), 2,000/byte
+  â”‚     Accesses: {
+  â”‚       [  0]  100000 ã€ƒ ã€ƒ ã€ƒ - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚       [ 32]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚       [ 64]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚       [ 96]  - - - - 
+  â”‚     }
+  â”‚     Allocated at {
+  â”‚       #1: 0x1086F1: m1 (acc.c:7)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.3.1/2 {
+  â”‚   â”‚     Total:     100 bytes (3.95%, 74.75/Minstr) in 1 blocks (11.11%, 0.75/Minstr), avg size 100 bytes, avg lifetime 350,084 instrs (26.17% of program duration)
+  â”‚   â”‚     Max:       100 bytes in 1 blocks, avg size 100 bytes
+  â”‚   â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    200,000 bytes (16.63%, 149,504.43/Minstr), 2,000/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  50000 ã€ƒ ã€ƒ ã€ƒ - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚       [ 32]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚       [ 64]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚       [ 96]  - - - - 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x1086F1: m1 (acc.c:7)
+  â”‚   â”‚       #2: 0x1088C3: main (acc.c:54)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â””── AP 1.3.2/2 {
+  â”‚         Total:     100 bytes (3.95%, 74.75/Minstr) in 1 blocks (11.11%, 0.75/Minstr), avg size 100 bytes, avg lifetime 350,072 instrs (26.17% of program duration)
+  â”‚         Max:       100 bytes in 1 blocks, avg size 100 bytes
+  â”‚         At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚         At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Writes:    200,000 bytes (16.63%, 149,504.43/Minstr), 2,000/byte
+  â”‚         Accesses: {
+  â”‚           [  0]  50000 ã€ƒ ã€ƒ ã€ƒ - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚           [ 32]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚           [ 64]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚           [ 96]  - - - - 
+  â”‚         }
+  â”‚         Allocated at {
+  â”‚           ^1: 0x1086F1: m1 (acc.c:7)
+  â”‚           #2: 0x1088D1: main (acc.c:55)
+  â”‚         }
+  â”‚       }
+  â”œâ”€â–¼ AP 1.4/7 (2 children) {
+  â”‚     Total:     200 bytes (7.89%, 149.5/Minstr) in 2 blocks (22.22%, 1.5/Minstr), avg size 100 bytes, avg lifetime 700,078 instrs (52.33% of program duration)
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    800,000 bytes (66.52%, 598,017.72/Minstr), 4,000/byte
+  â”‚     Accesses: {
+  â”‚       [  0]  âˆž ã€ƒ ã€ƒ ã€ƒ - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚       [ 32]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚       [ 64]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚       [ 96]  - - - - 
+  â”‚     }
+  â”‚     Allocated at {
+  â”‚       #1: 0x10870B: m2 (acc.c:9)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.4.1/2 {
+  â”‚   â”‚     Total:     100 bytes (3.95%, 74.75/Minstr) in 1 blocks (11.11%, 0.75/Minstr), avg size 100 bytes, avg lifetime 700,084 instrs (52.33% of program duration)
+  â”‚   â”‚     Max:       100 bytes in 1 blocks, avg size 100 bytes
+  â”‚   â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    400,000 bytes (33.26%, 299,008.86/Minstr), 4,000/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  âˆž ã€ƒ ã€ƒ ã€ƒ - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚       [ 32]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚       [ 64]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚       [ 96]  - - - - 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x10870B: m2 (acc.c:9)
+  â”‚   â”‚       #2: 0x108921: main (acc.c:64)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â””── AP 1.4.2/2 {
+  â”‚         Total:     100 bytes (3.95%, 74.75/Minstr) in 1 blocks (11.11%, 0.75/Minstr), avg size 100 bytes, avg lifetime 700,072 instrs (52.33% of program duration)
+  â”‚         Max:       100 bytes in 1 blocks, avg size 100 bytes
+  â”‚         At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚         At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Writes:    400,000 bytes (33.26%, 299,008.86/Minstr), 4,000/byte
+  â”‚         Accesses: {
+  â”‚           [  0]  âˆž ã€ƒ ã€ƒ ã€ƒ - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚           [ 32]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚           [ 64]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚           [ 96]  - - - - 
+  â”‚         }
+  â”‚         Allocated at {
+  â”‚           ^1: 0x10870B: m2 (acc.c:9)
+  â”‚           #2: 0x10892F: main (acc.c:65)
+  â”‚         }
+  â”‚       }
+  â”œâ”€â”€ AP 1.5/7 {
+  â”‚     Total:     33 bytes (1.3%, 24.67/Minstr) in 1 blocks (11.11%, 0.75/Minstr), avg size 33 bytes, avg lifetime 39 instrs (0% of program duration)
+  â”‚     Max:       33 bytes in 1 blocks, avg size 33 bytes
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    1 bytes (0%, 0.75/Minstr), 0.03/byte
+  â”‚     Accesses: {
+  â”‚       [  0]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚       [ 32]  1 
+  â”‚     }
+  â”‚     Allocated at {
+  â”‚       #1: 0x1087CB: main (acc.c:32)
+  â”‚     }
+  â”‚   }
+  â”œâ”€â”€ AP 1.6/7 {
+  â”‚     Total:     32 bytes (1.26%, 23.92/Minstr) in 1 blocks (11.11%, 0.75/Minstr), avg size 32 bytes, avg lifetime 4,751 instrs (0.36% of program duration)
+  â”‚     Max:       32 bytes in 1 blocks, avg size 32 bytes
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    496 bytes (0.04%, 370.77/Minstr), 15.5/byte
+  â”‚     Accesses: {
+  â”‚       [  0]  - 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 
+  â”‚     }
+  â”‚     Allocated at {
+  â”‚       #1: 0x10871F: main (acc.c:14)
+  â”‚     }
+  â”‚   }
+  â””── AP 1.7/7 {
+        Total:     20 bytes (0.79%, 14.95/Minstr) in 1 blocks (11.11%, 0.75/Minstr), avg size 20 bytes, avg lifetime 106 instrs (0.01% of program duration)
+        Max:       20 bytes in 1 blocks, avg size 20 bytes
+        At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+        At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+        Reads:     4 bytes (0.19%, 2.99/Minstr), 0.2/byte
+        Writes:    48 bytes (0%, 35.88/Minstr), 2.4/byte
+        Accesses: {
+          [  0]  2 ã€ƒ ã€ƒ ã€ƒ - - - - 1 ã€ƒ ã€ƒ ã€ƒ - - - - 10 ã€ƒ ã€ƒ ã€ƒ 
+        }
+        Allocated at {
+          #1: 0x108771: main (acc.c:23)
+        }
+      }
+
+AP significance threshold: at-t-end >= 0 bytes (0%)
+`
+//---------------------------------------------------------------------------
+    }
+  ]
+};
+tests.push(acc);
+
+//---------------------------------------------------------------------------
+// big (corresponds to dhat/tests/big.c)
+//---------------------------------------------------------------------------
+
+let big = {
+  name: "big",
+  input:
+//---------------------------------------------------------------------------
+{"dhatFileVersion":1
+,"cmd":"./big"
+,"pid":3902
+,"mi":245281,"ei":253354
+,"aps":
+ [{"tb":706,"tbk":1,"tli":543
+  ,"mb":706,"mbk":1
+  ,"gb":706,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-706,0]
+  ,"fs":[1,2,3,4,5]
+  }
+ ,{"tb":5,"tbk":1,"tli":7972
+  ,"mb":5,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":5,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-5,0]
+  ,"fs":[1,2,3,6,7]
+  }
+ ,{"tb":30,"tbk":1,"tli":7910
+  ,"mb":30,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":30,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-30,0]
+  ,"fs":[1,2,8,9]
+  }
+ ,{"tb":20,"tbk":1,"tli":7857
+  ,"mb":20,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":20,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-20,0]
+  ,"fs":[1,10,11]
+  }
+ ,{"tb":10,"tbk":1,"tli":7792
+  ,"mb":10,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":10,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[1,12,13,14,15]
+  }
+ ,{"tb":60,"tbk":1,"tli":7709
+  ,"mb":60,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":60,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-60,0]
+  ,"fs":[16,17,18,19,20,21,22]
+  }
+ ,{"tb":30,"tbk":1,"tli":7622
+  ,"mb":30,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":30,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-30,0]
+  ,"fs":[16,17,18,23,24,25,26]
+  }
+ ,{"tb":20,"tbk":1,"tli":7528
+  ,"mb":20,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":20,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-20,0]
+  ,"fs":[16,17,18,23,24,27,28,29]
+  }
+ ,{"tb":7,"tbk":1,"tli":7446
+  ,"mb":7,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":7,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-7,0]
+  ,"fs":[16,17,18,30,31,32]
+  }
+ ,{"tb":3,"tbk":1,"tli":7375
+  ,"mb":3,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":3,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-3,0]
+  ,"fs":[16,17,18,33,34]
+  }
+ ,{"tb":30,"tbk":1,"tli":7299
+  ,"mb":30,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":30,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-30,0]
+  ,"fs":[35,36,37,38,39,40]
+  }
+ ,{"tb":20,"tbk":1,"tli":7249
+  ,"mb":20,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":20,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-20,0]
+  ,"fs":[41,42]
+  }
+ ,{"tb":19,"tbk":1,"tli":7207
+  ,"mb":19,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":19,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-19,0]
+  ,"fs":[43,44]
+  }
+ ,{"tb":9,"tbk":1,"tli":7158
+  ,"mb":9,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":9,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-9,0]
+  ,"fs":[45,46,47]
+  }
+ ,{"tb":8,"tbk":1,"tli":7107
+  ,"mb":8,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":8,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-8,0]
+  ,"fs":[45,48,49]
+  }
+ ,{"tb":7,"tbk":1,"tli":7056
+  ,"mb":7,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":7,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-7,0]
+  ,"fs":[45,50,51]
+  }
+ ,{"tb":5,"tbk":1,"tli":7005
+  ,"mb":5,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":5,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-5,0]
+  ,"fs":[45,52,53]
+  }
+ ,{"tb":1,"tbk":1,"tli":6954
+  ,"mb":1,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":1,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[0]
+  ,"fs":[45,52,54]
+  }
+ ,{"tb":10,"tbk":1,"tli":6917
+  ,"mb":10,"mbk":1
+  ,"gb":0,"gbk":0
+  ,"fb":10,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[55]
+  }
+ ]
+,"ftbl":
+ ["[root]"
+ ,"0x1086A1: a (big.c:10)"
+ ,"0x1086BB: b1 (big.c:11)"
+ ,"0x1086D5: c1 (big.c:12)"
+ ,"0x1086EF: d1 (big.c:13)"
+ ,"0x108A43: main (big.c:38)"
+ ,"0x108709: d2 (big.c:14)"
+ ,"0x108A5D: main (big.c:41)"
+ ,"0x108723: c2 (big.c:15)"
+ ,"0x108A67: main (big.c:42)"
+ ,"0x10873D: b2 (big.c:16)"
+ ,"0x108A71: main (big.c:43)"
+ ,"0x108757: b3 (big.c:17)"
+ ,"0x108771: e (big.c:17)"
+ ,"0x10878B: f (big.c:17)"
+ ,"0x108A7B: main (big.c:44)"
+ ,"0x1087A5: g (big.c:18)"
+ ,"0x1087BF: h (big.c:18)"
+ ,"0x1087D9: i (big.c:18)"
+ ,"0x1087F3: j2 (big.c:19)"
+ ,"0x10880D: k (big.c:19)"
+ ,"0x108827: l (big.c:19)"
+ ,"0x108A85: main (big.c:45)"
+ ,"0x108841: j3 (big.c:20)"
+ ,"0x10885B: m (big.c:20)"
+ ,"0x108875: n1 (big.c:21)"
+ ,"0x108A8F: main (big.c:46)"
+ ,"0x10888F: n2 (big.c:22)"
+ ,"0x1088A9: o (big.c:22)"
+ ,"0x108A99: main (big.c:47)"
+ ,"0x1088C3: p (big.c:23)"
+ ,"0x1088DD: q (big.c:23)"
+ ,"0x108AA3: main (big.c:48)"
+ ,"0x1088F7: r (big.c:24)"
+ ,"0x108AAD: main (big.c:49)"
+ ,"0x108911: s1 (big.c:25)"
+ ,"0x10892B: s2 (big.c:25)"
+ ,"0x108945: s3 (big.c:25)"
+ ,"0x10895F: s4 (big.c:25)"
+ ,"0x108979: s5 (big.c:25)"
+ ,"0x108AB7: main (big.c:50)"
+ ,"0x108993: t (big.c:26)"
+ ,"0x108AC1: main (big.c:51)"
+ ,"0x1089AD: u (big.c:27)"
+ ,"0x108ACB: main (big.c:52)"
+ ,"0x1089C7: v (big.c:28)"
+ ,"0x1089E1: w (big.c:29)"
+ ,"0x108AD5: main (big.c:53)"
+ ,"0x1089FB: x (big.c:30)"
+ ,"0x108ADF: main (big.c:54)"
+ ,"0x108A15: y (big.c:31)"
+ ,"0x108AE9: main (big.c:55)"
+ ,"0x108A2F: z (big.c:32)"
+ ,"0x108AF3: main (big.c:56)"
+ ,"0x108AFD: main (big.c:57)"
+ ,"0x108B07: main (big.c:60)"
+ ]
+}
+//---------------------------------------------------------------------------
+  ,
+  outputs: [
+    {
+      label: "Total (bytes)",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: ./big
+  PID:     3902
+}
+
+Times {
+  t-gmax: 245,281 instrs (96.81% of program duration)
+  t-end:  253,354 instrs
+}
+
+â–¼ AP 1/1 (7 children) {
+    Total:     1,000 bytes (100%, 3,947.05/Minstr) in 19 blocks (100%, 74.99/Minstr), avg size 52.63 bytes, avg lifetime 7,037.16 instrs (2.78% of program duration)
+    At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+    At t-end:  294 bytes (100%) in 18 blocks (100%), avg size 16.33 bytes
+    Reads:     0 bytes (0%, 0/Minstr), 0/byte
+    Writes:    0 bytes (0%, 0/Minstr), 0/byte
+    Allocated at {
+      #0: [root]
+    }
+  }
+  â”œâ”€â–¼ AP 1.1/7 (3 children) {
+  â”‚     Total:     771 bytes (77.1%, 3,043.17/Minstr) in 5 blocks (26.32%, 19.74/Minstr), avg size 154.2 bytes, avg lifetime 6,414.8 instrs (2.53% of program duration)
+  â”‚     At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+  â”‚     At t-end:  65 bytes (22.11%) in 4 blocks (22.22%), avg size 16.25 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Allocated at {
+  â”‚       #1: 0x1086A1: a (big.c:10)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â–¼ AP 1.1.1/3 (2 children) {
+  â”‚   â”‚     Total:     741 bytes (74.1%, 2,924.76/Minstr) in 3 blocks (15.79%, 11.84/Minstr), avg size 247 bytes, avg lifetime 5,475 instrs (2.16% of program duration)
+  â”‚   â”‚     At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+  â”‚   â”‚     At t-end:  35 bytes (11.9%) in 2 blocks (11.11%), avg size 17.5 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x1086A1: a (big.c:10)
+  â”‚   â”‚       #2: 0x1086BB: b1 (big.c:11)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”‚   â”œâ”€â–¼ AP 1.1.1.1/2 (2 children) {
+  â”‚   â”‚   â”‚     Total:     711 bytes (71.1%, 2,806.35/Minstr) in 2 blocks (10.53%, 7.89/Minstr), avg size 355.5 bytes, avg lifetime 4,257.5 instrs (1.68% of program duration)
+  â”‚   â”‚   â”‚     At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+  â”‚   â”‚   â”‚     At t-end:  5 bytes (1.7%) in 1 blocks (5.56%), avg size 5 bytes
+  â”‚   â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Allocated at {
+  â”‚   â”‚   â”‚       ^1: 0x1086A1: a (big.c:10)
+  â”‚   â”‚   â”‚       ^2: 0x1086BB: b1 (big.c:11)
+  â”‚   â”‚   â”‚       #3: 0x1086D5: c1 (big.c:12)
+  â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   }
+  â”‚   â”‚   â”‚   â”œâ”€â”€ AP 1.1.1.1.1/2 {
+  â”‚   â”‚   â”‚   â”‚     Total:     706 bytes (70.6%, 2,786.61/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 706 bytes, avg lifetime 543 instrs (0.21% of program duration)
+  â”‚   â”‚   â”‚   â”‚     Max:       706 bytes in 1 blocks, avg size 706 bytes
+  â”‚   â”‚   â”‚   â”‚     At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+  â”‚   â”‚   â”‚   â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚   â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚   â”‚     Accesses: {
+  â”‚   â”‚   â”‚   â”‚       [  0]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [ 32]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [ 64]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [ 96]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [128]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [160]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [192]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [224]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [256]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [288]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [320]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [352]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [384]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [416]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [448]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [480]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [512]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [544]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [576]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [608]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [640]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [672]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [704]  - - 
+  â”‚   â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   â”‚     Allocated at {
+  â”‚   â”‚   â”‚   â”‚       ^1: 0x1086A1: a (big.c:10)
+  â”‚   â”‚   â”‚   â”‚       ^2: 0x1086BB: b1 (big.c:11)
+  â”‚   â”‚   â”‚   â”‚       ^3: 0x1086D5: c1 (big.c:12)
+  â”‚   â”‚   â”‚   â”‚       #4: 0x1086EF: d1 (big.c:13)
+  â”‚   â”‚   â”‚   â”‚       #5: 0x108A43: main (big.c:38)
+  â”‚   â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   â”‚   }
+  â”‚   â”‚   â”‚   â””── AP 1.1.1.1.2/2 {
+  â”‚   â”‚   â”‚         Total:     5 bytes (0.5%, 19.74/Minstr)
+  â”‚   â”‚   â”‚         Allocated at {
+  â”‚   â”‚   â”‚           [1 insignificant]
+  â”‚   â”‚   â”‚         }
+  â”‚   â”‚   â”‚       }
+  â”‚   â”‚   â””── AP 1.1.1.2/2 {
+  â”‚   â”‚         Total:     30 bytes (3%, 118.41/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 30 bytes, avg lifetime 7,910 instrs (3.12% of program duration)
+  â”‚   â”‚         Max:       30 bytes in 1 blocks, avg size 30 bytes
+  â”‚   â”‚         At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚         At t-end:  30 bytes (10.2%) in 1 blocks (5.56%), avg size 30 bytes
+  â”‚   â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚         Accesses: {
+  â”‚   â”‚           [  0]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚         }
+  â”‚   â”‚         Allocated at {
+  â”‚   â”‚           ^1: 0x1086A1: a (big.c:10)
+  â”‚   â”‚           ^2: 0x1086BB: b1 (big.c:11)
+  â”‚   â”‚           #3: 0x108723: c2 (big.c:15)
+  â”‚   â”‚           #4: 0x108A67: main (big.c:42)
+  â”‚   â”‚         }
+  â”‚   â”‚       }
+  â”‚   â”œâ”€â”€ AP 1.1.2/3 {
+  â”‚   â”‚     Total:     20 bytes (2%, 78.94/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 20 bytes, avg lifetime 7,857 instrs (3.1% of program duration)
+  â”‚   â”‚     Max:       20 bytes in 1 blocks, avg size 20 bytes
+  â”‚   â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚     At t-end:  20 bytes (6.8%) in 1 blocks (5.56%), avg size 20 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x1086A1: a (big.c:10)
+  â”‚   â”‚       #2: 0x10873D: b2 (big.c:16)
+  â”‚   â”‚       #3: 0x108A71: main (big.c:43)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â””── AP 1.1.3/3 {
+  â”‚         Total:     10 bytes (1%, 39.47/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 10 bytes, avg lifetime 7,792 instrs (3.08% of program duration)
+  â”‚         Max:       10 bytes in 1 blocks, avg size 10 bytes
+  â”‚         At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚         At t-end:  10 bytes (3.4%) in 1 blocks (5.56%), avg size 10 bytes
+  â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Accesses: {
+  â”‚           [  0]  - - - - - - - - - - 
+  â”‚         }
+  â”‚         Allocated at {
+  â”‚           ^1: 0x1086A1: a (big.c:10)
+  â”‚           #2: 0x108757: b3 (big.c:17)
+  â”‚           #3: 0x108771: e (big.c:17)
+  â”‚           #4: 0x10878B: f (big.c:17)
+  â”‚           #5: 0x108A7B: main (big.c:44)
+  â”‚         }
+  â”‚       }
+  â”œâ”€â–¼ AP 1.2/7 (3 children) {
+  â”‚     Total:     120 bytes (12%, 473.65/Minstr) in 5 blocks (26.32%, 19.74/Minstr), avg size 24 bytes, avg lifetime 7,536 instrs (2.97% of program duration)
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  120 bytes (40.82%) in 5 blocks (27.78%), avg size 24 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Allocated at {
+  â”‚       #1: 0x1087A5: g (big.c:18)
+  â”‚       #2: 0x1087BF: h (big.c:18)
+  â”‚       #3: 0x1087D9: i (big.c:18)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.2.1/3 {
+  â”‚   â”‚     Total:     60 bytes (6%, 236.82/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 60 bytes, avg lifetime 7,709 instrs (3.04% of program duration)
+  â”‚   â”‚     Max:       60 bytes in 1 blocks, avg size 60 bytes
+  â”‚   â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚     At t-end:  60 bytes (20.41%) in 1 blocks (5.56%), avg size 60 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚       [ 32]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x1087A5: g (big.c:18)
+  â”‚   â”‚       ^2: 0x1087BF: h (big.c:18)
+  â”‚   â”‚       ^3: 0x1087D9: i (big.c:18)
+  â”‚   â”‚       #4: 0x1087F3: j2 (big.c:19)
+  â”‚   â”‚       #5: 0x10880D: k (big.c:19)
+  â”‚   â”‚       #6: 0x108827: l (big.c:19)
+  â”‚   â”‚       #7: 0x108A85: main (big.c:45)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”œâ”€â–¼ AP 1.2.2/3 (2 children) {
+  â”‚   â”‚     Total:     50 bytes (5%, 197.35/Minstr) in 2 blocks (10.53%, 7.89/Minstr), avg size 25 bytes, avg lifetime 7,575 instrs (2.99% of program duration)
+  â”‚   â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚     At t-end:  50 bytes (17.01%) in 2 blocks (11.11%), avg size 25 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x1087A5: g (big.c:18)
+  â”‚   â”‚       ^2: 0x1087BF: h (big.c:18)
+  â”‚   â”‚       ^3: 0x1087D9: i (big.c:18)
+  â”‚   â”‚       #4: 0x108841: j3 (big.c:20)
+  â”‚   â”‚       #5: 0x10885B: m (big.c:20)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”‚   â”œâ”€â”€ AP 1.2.2.1/2 {
+  â”‚   â”‚   â”‚     Total:     30 bytes (3%, 118.41/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 30 bytes, avg lifetime 7,622 instrs (3.01% of program duration)
+  â”‚   â”‚   â”‚     Max:       30 bytes in 1 blocks, avg size 30 bytes
+  â”‚   â”‚   â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚   â”‚     At t-end:  30 bytes (10.2%) in 1 blocks (5.56%), avg size 30 bytes
+  â”‚   â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Accesses: {
+  â”‚   â”‚   â”‚       [  0]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚     Allocated at {
+  â”‚   â”‚   â”‚       ^1: 0x1087A5: g (big.c:18)
+  â”‚   â”‚   â”‚       ^2: 0x1087BF: h (big.c:18)
+  â”‚   â”‚   â”‚       ^3: 0x1087D9: i (big.c:18)
+  â”‚   â”‚   â”‚       ^4: 0x108841: j3 (big.c:20)
+  â”‚   â”‚   â”‚       ^5: 0x10885B: m (big.c:20)
+  â”‚   â”‚   â”‚       #6: 0x108875: n1 (big.c:21)
+  â”‚   â”‚   â”‚       #7: 0x108A8F: main (big.c:46)
+  â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   }
+  â”‚   â”‚   â””── AP 1.2.2.2/2 {
+  â”‚   â”‚         Total:     20 bytes (2%, 78.94/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 20 bytes, avg lifetime 7,528 instrs (2.97% of program duration)
+  â”‚   â”‚         Max:       20 bytes in 1 blocks, avg size 20 bytes
+  â”‚   â”‚         At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚         At t-end:  20 bytes (6.8%) in 1 blocks (5.56%), avg size 20 bytes
+  â”‚   â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚         Accesses: {
+  â”‚   â”‚           [  0]  - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚         }
+  â”‚   â”‚         Allocated at {
+  â”‚   â”‚           ^1: 0x1087A5: g (big.c:18)
+  â”‚   â”‚           ^2: 0x1087BF: h (big.c:18)
+  â”‚   â”‚           ^3: 0x1087D9: i (big.c:18)
+  â”‚   â”‚           ^4: 0x108841: j3 (big.c:20)
+  â”‚   â”‚           ^5: 0x10885B: m (big.c:20)
+  â”‚   â”‚           #6: 0x10888F: n2 (big.c:22)
+  â”‚   â”‚           #7: 0x1088A9: o (big.c:22)
+  â”‚   â”‚           #8: 0x108A99: main (big.c:47)
+  â”‚   â”‚         }
+  â”‚   â”‚       }
+  â”‚   â””── AP 1.2.3/3 {
+  â”‚         Total:     10 bytes (1%, 39.47/Minstr)
+  â”‚         Allocated at {
+  â”‚           [2 insignificant]
+  â”‚         }
+  â”‚       }
+  â”œâ”€â”€ AP 1.3/7 {
+  â”‚     Total:     30 bytes (3%, 118.41/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 30 bytes, avg lifetime 7,299 instrs (2.88% of program duration)
+  â”‚     Max:       30 bytes in 1 blocks, avg size 30 bytes
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  30 bytes (10.2%) in 1 blocks (5.56%), avg size 30 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Accesses: {
+  â”‚       [  0]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚     }
+  â”‚     Allocated at {
+  â”‚       #1: 0x108911: s1 (big.c:25)
+  â”‚       #2: 0x10892B: s2 (big.c:25)
+  â”‚       #3: 0x108945: s3 (big.c:25)
+  â”‚       #4: 0x10895F: s4 (big.c:25)
+  â”‚       #5: 0x108979: s5 (big.c:25)
+  â”‚       #6: 0x108AB7: main (big.c:50)
+  â”‚     }
+  â”‚   }
+  â”œâ”€â–¼ AP 1.4/7 (1 children) {
+  â”‚     Total:     30 bytes (3%, 118.41/Minstr) in 5 blocks (26.32%, 19.74/Minstr), avg size 6 bytes, avg lifetime 7,056 instrs (2.79% of program duration)
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  30 bytes (10.2%) in 5 blocks (27.78%), avg size 6 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Allocated at {
+  â”‚       #1: 0x1089C7: v (big.c:28)
+  â”‚     }
+  â”‚   }
+  â”‚   â””── AP 1.4.1/1 {
+  â”‚         Total:     30 bytes (3%, 118.41/Minstr)
+  â”‚         Allocated at {
+  â”‚           [4 insignificant]
+  â”‚         }
+  â”‚       }
+  â”œâ”€â”€ AP 1.5/7 {
+  â”‚     Total:     20 bytes (2%, 78.94/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 20 bytes, avg lifetime 7,249 instrs (2.86% of program duration)
+  â”‚     Max:       20 bytes in 1 blocks, avg size 20 bytes
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  20 bytes (6.8%) in 1 blocks (5.56%), avg size 20 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Accesses: {
+  â”‚       [  0]  - - - - - - - - - - - - - - - - - - - - 
+  â”‚     }
+  â”‚     Allocated at {
+  â”‚       #1: 0x108993: t (big.c:26)
+  â”‚       #2: 0x108AC1: main (big.c:51)
+  â”‚     }
+  â”‚   }
+  â”œâ”€â”€ AP 1.6/7 {
+  â”‚     Total:     19 bytes (1.9%, 74.99/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 19 bytes, avg lifetime 7,207 instrs (2.84% of program duration)
+  â”‚     Max:       19 bytes in 1 blocks, avg size 19 bytes
+  â”‚     At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚     At t-end:  19 bytes (6.46%) in 1 blocks (5.56%), avg size 19 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Accesses: {
+  â”‚       [  0]  - - - - - - - - - - - - - - - - - - - 
+  â”‚     }
+  â”‚     Allocated at {
+  â”‚       #1: 0x1089AD: u (big.c:27)
+  â”‚       #2: 0x108ACB: main (big.c:52)
+  â”‚     }
+  â”‚   }
+  â””── AP 1.7/7 {
+        Total:     10 bytes (1%, 39.47/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 10 bytes, avg lifetime 6,917 instrs (2.73% of program duration)
+        Max:       10 bytes in 1 blocks, avg size 10 bytes
+        At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+        At t-end:  10 bytes (3.4%) in 1 blocks (5.56%), avg size 10 bytes
+        Reads:     0 bytes (0%, 0/Minstr), 0/byte
+        Writes:    0 bytes (0%, 0/Minstr), 0/byte
+        Accesses: {
+          [  0]  - - - - - - - - - - 
+        }
+        Allocated at {
+          #1: 0x108B07: main (big.c:60)
+        }
+      }
+
+AP significance threshold: total >= 10 bytes (1%)
+`
+//---------------------------------------------------------------------------
+    },
+    {
+      label: "Total (blocks), short-lived",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: ./big
+  PID:     3902
+}
+
+Times {
+  t-gmax: 245,281 instrs (96.81% of program duration)
+  t-end:  253,354 instrs
+}
+
+â–¼ AP 1/1 (1 children) {
+    Total:     19 blocks (100%, 74.99/Minstr), avg lifetime 7,037.16 instrs (2.78% of program duration)
+    Allocated at {
+      #0: [root]
+    }
+  }
+  â””── AP 1.1/1 {
+        Total:     19 blocks (100%, 74.99/Minstr), avg lifetime 7,037.16 instrs (2.78% of program duration)
+        Allocated at {
+          [7 insignificant]
+        }
+      }
+
+AP significance threshold: (total >= 0.1 blocks (0.5%)) && (total avg lifetime <= 500 instrs)
+`
+//---------------------------------------------------------------------------
+    },
+    {
+      label: "At t-gmax (bytes)",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: ./big
+  PID:     3902
+}
+
+Times {
+  t-gmax: 245,281 instrs (96.81% of program duration)
+  t-end:  253,354 instrs
+}
+
+â–¼ AP 1/1 (2 children) {
+    Total:     1,000 bytes (100%, 3,947.05/Minstr) in 19 blocks (100%, 74.99/Minstr), avg size 52.63 bytes, avg lifetime 7,037.16 instrs (2.78% of program duration)
+    At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+    At t-end:  294 bytes (100%) in 18 blocks (100%), avg size 16.33 bytes
+    Reads:     0 bytes (0%, 0/Minstr), 0/byte
+    Writes:    0 bytes (0%, 0/Minstr), 0/byte
+    Allocated at {
+      #0: [root]
+    }
+  }
+  â”œâ”€â–¼ AP 1.1/2 (2 children) {
+  â”‚     Total:     771 bytes (77.1%, 3,043.17/Minstr) in 5 blocks (26.32%, 19.74/Minstr), avg size 154.2 bytes, avg lifetime 6,414.8 instrs (2.53% of program duration)
+  â”‚     At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+  â”‚     At t-end:  65 bytes (22.11%) in 4 blocks (22.22%), avg size 16.25 bytes
+  â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚     Allocated at {
+  â”‚       #1: 0x1086A1: a (big.c:10)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â–¼ AP 1.1.1/2 (2 children) {
+  â”‚   â”‚     Total:     741 bytes (74.1%, 2,924.76/Minstr) in 3 blocks (15.79%, 11.84/Minstr), avg size 247 bytes, avg lifetime 5,475 instrs (2.16% of program duration)
+  â”‚   â”‚     At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+  â”‚   â”‚     At t-end:  35 bytes (11.9%) in 2 blocks (11.11%), avg size 17.5 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x1086A1: a (big.c:10)
+  â”‚   â”‚       #2: 0x1086BB: b1 (big.c:11)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”‚   â”œâ”€â–¼ AP 1.1.1.1/2 (2 children) {
+  â”‚   â”‚   â”‚     Total:     711 bytes (71.1%, 2,806.35/Minstr) in 2 blocks (10.53%, 7.89/Minstr), avg size 355.5 bytes, avg lifetime 4,257.5 instrs (1.68% of program duration)
+  â”‚   â”‚   â”‚     At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+  â”‚   â”‚   â”‚     At t-end:  5 bytes (1.7%) in 1 blocks (5.56%), avg size 5 bytes
+  â”‚   â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚     Allocated at {
+  â”‚   â”‚   â”‚       ^1: 0x1086A1: a (big.c:10)
+  â”‚   â”‚   â”‚       ^2: 0x1086BB: b1 (big.c:11)
+  â”‚   â”‚   â”‚       #3: 0x1086D5: c1 (big.c:12)
+  â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   }
+  â”‚   â”‚   â”‚   â”œâ”€â”€ AP 1.1.1.1.1/2 {
+  â”‚   â”‚   â”‚   â”‚     Total:     706 bytes (70.6%, 2,786.61/Minstr) in 1 blocks (5.26%, 3.95/Minstr), avg size 706 bytes, avg lifetime 543 instrs (0.21% of program duration)
+  â”‚   â”‚   â”‚   â”‚     Max:       706 bytes in 1 blocks, avg size 706 bytes
+  â”‚   â”‚   â”‚   â”‚     At t-gmax: 706 bytes (100%) in 1 blocks (100%), avg size 706 bytes
+  â”‚   â”‚   â”‚   â”‚     At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚   â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚   â”‚     Accesses: {
+  â”‚   â”‚   â”‚   â”‚       [  0]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [ 32]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [ 64]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [ 96]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [128]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [160]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [192]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [224]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [256]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [288]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [320]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [352]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [384]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [416]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [448]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [480]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [512]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [544]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [576]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [608]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [640]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [672]  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+  â”‚   â”‚   â”‚   â”‚       [704]  - - 
+  â”‚   â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   â”‚     Allocated at {
+  â”‚   â”‚   â”‚   â”‚       ^1: 0x1086A1: a (big.c:10)
+  â”‚   â”‚   â”‚   â”‚       ^2: 0x1086BB: b1 (big.c:11)
+  â”‚   â”‚   â”‚   â”‚       ^3: 0x1086D5: c1 (big.c:12)
+  â”‚   â”‚   â”‚   â”‚       #4: 0x1086EF: d1 (big.c:13)
+  â”‚   â”‚   â”‚   â”‚       #5: 0x108A43: main (big.c:38)
+  â”‚   â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   â”‚   }
+  â”‚   â”‚   â”‚   â””── AP 1.1.1.1.2/2 {
+  â”‚   â”‚   â”‚         At t-gmax: 0 bytes (0%)
+  â”‚   â”‚   â”‚         Allocated at {
+  â”‚   â”‚   â”‚           [1 insignificant]
+  â”‚   â”‚   â”‚         }
+  â”‚   â”‚   â”‚       }
+  â”‚   â”‚   â””── AP 1.1.1.2/2 {
+  â”‚   â”‚         At t-gmax: 0 bytes (0%)
+  â”‚   â”‚         Allocated at {
+  â”‚   â”‚           [1 insignificant]
+  â”‚   â”‚         }
+  â”‚   â”‚       }
+  â”‚   â””── AP 1.1.2/2 {
+  â”‚         At t-gmax: 0 bytes (0%)
+  â”‚         Allocated at {
+  â”‚           [2 insignificant]
+  â”‚         }
+  â”‚       }
+  â””── AP 1.2/2 {
+        At t-gmax: 0 bytes (0%)
+        Allocated at {
+          [6 insignificant]
+        }
+      }
+
+AP significance threshold: at-t-gmax >= 7.06 bytes (1%)
+`
+//---------------------------------------------------------------------------
+    }
+  ]
+};
+tests.push(big);
+
+//---------------------------------------------------------------------------
+// sig (corresponds to dhat/tests/sig.c)
+//---------------------------------------------------------------------------
+
+let sig = {
+  name: "sig",
+  input:
+//---------------------------------------------------------------------------
+{"dhatFileVersion":1
+,"cmd":"./sig"
+,"pid":21476
+,"mi":1311861,"ei":1318783
+,"aps":
+ [{"tb":11,"tbk":1,"tli":1075941
+  ,"mb":11,"mbk":1
+  ,"gb":11,"gbk":1
+  ,"fb":11,"fbk":1
+  ,"rb":11,"wb":16489
+  ,"acc":[-11,1500]
+  ,"fs":[1,2]
+  }
+ ,{"tb":10,"tbk":1,"tli":880845
+  ,"mb":10,"mbk":1
+  ,"gb":10,"gbk":1
+  ,"fb":10,"fbk":1
+  ,"rb":10,"wb":14990
+  ,"acc":[-10,1500]
+  ,"fs":[1,3,4]
+  }
+ ,{"tb":5,"tbk":1,"tli":702250
+  ,"mb":5,"mbk":1
+  ,"gb":5,"gbk":1
+  ,"fb":5,"fbk":1
+  ,"rb":5,"wb":7495
+  ,"acc":[-5,1500]
+  ,"fs":[1,5,6]
+  }
+ ,{"tb":4,"tbk":1,"tli":606170
+  ,"mb":4,"mbk":1
+  ,"gb":4,"gbk":1
+  ,"fb":4,"fbk":1
+  ,"rb":4,"wb":5996
+  ,"acc":[-4,1500]
+  ,"fs":[1,5,7]
+  }
+ ,{"tb":10,"tbk":1,"tli":510097
+  ,"mb":10,"mbk":1
+  ,"gb":10,"gbk":1
+  ,"fb":10,"fbk":1
+  ,"rb":10,"wb":14990
+  ,"acc":[-10,1500]
+  ,"fs":[8,9]
+  }
+ ,{"tb":9,"tbk":1,"tli":331504
+  ,"mb":9,"mbk":1
+  ,"gb":9,"gbk":1
+  ,"fb":9,"fbk":1
+  ,"rb":9,"wb":13491
+  ,"acc":[-9,1500]
+  ,"fs":[8,10,11]
+  }
+ ,{"tb":5,"tbk":1,"tli":169412
+  ,"mb":5,"mbk":1
+  ,"gb":5,"gbk":1
+  ,"fb":5,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-5,0]
+  ,"fs":[8,12,13]
+  }
+ ,{"tb":3,"tbk":1,"tli":169360
+  ,"mb":3,"mbk":1
+  ,"gb":3,"gbk":1
+  ,"fb":3,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-3,0]
+  ,"fs":[8,12,14]
+  }
+ ,{"tb":9,"tbk":1,"tli":169315
+  ,"mb":9,"mbk":1
+  ,"gb":9,"gbk":1
+  ,"fb":9,"fbk":1
+  ,"rb":9,"wb":13491
+  ,"acc":[-9,1500]
+  ,"fs":[15,16]
+  }
+ ,{"tb":8,"tbk":1,"tli":7225
+  ,"mb":8,"mbk":1
+  ,"gb":8,"gbk":1
+  ,"fb":8,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-8,0]
+  ,"fs":[15,17,18]
+  }
+ ,{"tb":4,"tbk":1,"tli":7173
+  ,"mb":4,"mbk":1
+  ,"gb":4,"gbk":1
+  ,"fb":4,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-4,0]
+  ,"fs":[15,19,20]
+  }
+ ,{"tb":3,"tbk":1,"tli":7121
+  ,"mb":3,"mbk":1
+  ,"gb":3,"gbk":1
+  ,"fb":3,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-3,0]
+  ,"fs":[15,19,21]
+  }
+ ,{"tb":8,"tbk":1,"tli":7076
+  ,"mb":8,"mbk":1
+  ,"gb":8,"gbk":1
+  ,"fb":8,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-8,0]
+  ,"fs":[22,23]
+  }
+ ,{"tb":7,"tbk":1,"tli":7026
+  ,"mb":7,"mbk":1
+  ,"gb":7,"gbk":1
+  ,"fb":7,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-7,0]
+  ,"fs":[22,24,25]
+  }
+ ,{"tb":4,"tbk":1,"tli":6974
+  ,"mb":4,"mbk":1
+  ,"gb":4,"gbk":1
+  ,"fb":4,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-4,0]
+  ,"fs":[22,26,27]
+  }
+ ,{"tb":2,"tbk":1,"tli":6922
+  ,"mb":2,"mbk":1
+  ,"gb":2,"gbk":1
+  ,"fb":2,"fbk":1
+  ,"rb":0,"wb":0
+  ,"acc":[-2,0]
+  ,"fs":[22,26,28]
+  }
+ ]
+,"ftbl":
+ ["[root]"
+ ,"0x108681: am (sig.c:9)"
+ ,"0x10883C: main (sig.c:57)"
+ ,"0x10869B: a2 (sig.c:11)"
+ ,"0x10885B: main (sig.c:58)"
+ ,"0x1086B5: a3 (sig.c:12)"
+ ,"0x10887A: main (sig.c:59)"
+ ,"0x108899: main (sig.c:60)"
+ ,"0x1086CF: bm (sig.c:15)"
+ ,"0x1088B8: main (sig.c:62)"
+ ,"0x1086E9: b2 (sig.c:17)"
+ ,"0x1088D7: main (sig.c:63)"
+ ,"0x108703: b3 (sig.c:18)"
+ ,"0x1088F6: main (sig.c:64)"
+ ,"0x108904: main (sig.c:65)"
+ ,"0x10871D: cm (sig.c:21)"
+ ,"0x108912: main (sig.c:67)"
+ ,"0x108737: c2 (sig.c:23)"
+ ,"0x108931: main (sig.c:68)"
+ ,"0x108751: c3 (sig.c:24)"
+ ,"0x10893F: main (sig.c:69)"
+ ,"0x10894D: main (sig.c:70)"
+ ,"0x10876B: dm (sig.c:27)"
+ ,"0x10895B: main (sig.c:72)"
+ ,"0x108785: d2 (sig.c:29)"
+ ,"0x108969: main (sig.c:73)"
+ ,"0x10879F: d3 (sig.c:30)"
+ ,"0x108977: main (sig.c:74)"
+ ,"0x108985: main (sig.c:75)"
+ ]
+}
+//---------------------------------------------------------------------------
+  ,
+  outputs: [
+    {
+      label: "Total (bytes), zero reads or zero writes",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: ./sig
+  PID:     21476
+}
+
+Times {
+  t-gmax: 1,311,861 instrs (99.48% of program duration)
+  t-end:  1,318,783 instrs
+}
+
+â–¼ AP 1/1 (4 children) {
+    Total:     102 bytes (100%, 77.34/Minstr)
+    Reads:     58 bytes (100%, 43.98/Minstr)
+    Writes:    86,942 bytes (100%, 65,925.93/Minstr)
+    Allocated at {
+      #0: [root]
+    }
+  }
+  â”œâ”€â”€ AP 1.1/4 {
+  â”‚     Total:     30 bytes (29.41%, 22.75/Minstr)
+  â”‚     Reads:     30 bytes (51.72%, 22.75/Minstr)
+  â”‚     Writes:    44,970 bytes (51.72%, 34,099.62/Minstr)
+  â”‚     Allocated at {
+  â”‚       [1 insignificant]
+  â”‚     }
+  â”‚   }
+  â”œâ”€â–¼ AP 1.2/4 (2 children) {
+  â”‚     Total:     27 bytes (26.47%, 20.47/Minstr)
+  â”‚     Reads:     19 bytes (32.76%, 14.41/Minstr)
+  â”‚     Writes:    28,481 bytes (32.76%, 21,596.43/Minstr)
+  â”‚     Allocated at {
+  â”‚       #1: 0x1086CF: bm (sig.c:15)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.2.1/2 {
+  â”‚   â”‚     Total:     19 bytes (18.63%, 14.41/Minstr)
+  â”‚   â”‚     Reads:     19 bytes (32.76%, 14.41/Minstr)
+  â”‚   â”‚     Writes:    28,481 bytes (32.76%, 21,596.43/Minstr)
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       [2 insignificant]
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â””─▼ AP 1.2.2/2 (2 children) {
+  â”‚         Total:     8 bytes (7.84%, 6.07/Minstr) in 2 blocks (12.5%, 1.52/Minstr), avg size 4 bytes, avg lifetime 169,386 instrs (12.84% of program duration)
+  â”‚         At t-gmax: 8 bytes (7.84%) in 2 blocks (12.5%), avg size 4 bytes
+  â”‚         At t-end:  8 bytes (7.84%) in 2 blocks (12.5%), avg size 4 bytes
+  â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Allocated at {
+  â”‚           ^1: 0x1086CF: bm (sig.c:15)
+  â”‚           #2: 0x108703: b3 (sig.c:18)
+  â”‚         }
+  â”‚       }
+  â”‚       â”œâ”€â”€ AP 1.2.2.1/2 {
+  â”‚       â”‚     Total:     5 bytes (4.9%, 3.79/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 5 bytes, avg lifetime 169,412 instrs (12.85% of program duration)
+  â”‚       â”‚     Max:       5 bytes in 1 blocks, avg size 5 bytes
+  â”‚       â”‚     At t-gmax: 5 bytes (4.9%) in 1 blocks (6.25%), avg size 5 bytes
+  â”‚       â”‚     At t-end:  5 bytes (4.9%) in 1 blocks (6.25%), avg size 5 bytes
+  â”‚       â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚       â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚       â”‚     Accesses: {
+  â”‚       â”‚       [  0]  - - - - - 
+  â”‚       â”‚     }
+  â”‚       â”‚     Allocated at {
+  â”‚       â”‚       ^1: 0x1086CF: bm (sig.c:15)
+  â”‚       â”‚       ^2: 0x108703: b3 (sig.c:18)
+  â”‚       â”‚       #3: 0x1088F6: main (sig.c:64)
+  â”‚       â”‚     }
+  â”‚       â”‚   }
+  â”‚       â””── AP 1.2.2.2/2 {
+  â”‚             Total:     3 bytes (2.94%, 2.27/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 3 bytes, avg lifetime 169,360 instrs (12.84% of program duration)
+  â”‚             Max:       3 bytes in 1 blocks, avg size 3 bytes
+  â”‚             At t-gmax: 3 bytes (2.94%) in 1 blocks (6.25%), avg size 3 bytes
+  â”‚             At t-end:  3 bytes (2.94%) in 1 blocks (6.25%), avg size 3 bytes
+  â”‚             Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚             Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚             Accesses: {
+  â”‚               [  0]  - - - 
+  â”‚             }
+  â”‚             Allocated at {
+  â”‚               ^1: 0x1086CF: bm (sig.c:15)
+  â”‚               ^2: 0x108703: b3 (sig.c:18)
+  â”‚               #3: 0x108904: main (sig.c:65)
+  â”‚             }
+  â”‚           }
+  â”œâ”€â–¼ AP 1.3/4 (3 children) {
+  â”‚     Total:     24 bytes (23.53%, 18.2/Minstr)
+  â”‚     Reads:     9 bytes (15.52%, 6.82/Minstr)
+  â”‚     Writes:    13,491 bytes (15.52%, 10,229.89/Minstr)
+  â”‚     Allocated at {
+  â”‚       #1: 0x10871D: cm (sig.c:21)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.3.1/3 {
+  â”‚   â”‚     Total:     9 bytes (8.82%, 6.82/Minstr)
+  â”‚   â”‚     Reads:     9 bytes (15.52%, 6.82/Minstr)
+  â”‚   â”‚     Writes:    13,491 bytes (15.52%, 10,229.89/Minstr)
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       [1 insignificant]
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.3.2/3 {
+  â”‚   â”‚     Total:     8 bytes (7.84%, 6.07/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 8 bytes, avg lifetime 7,225 instrs (0.55% of program duration)
+  â”‚   â”‚     Max:       8 bytes in 1 blocks, avg size 8 bytes
+  â”‚   â”‚     At t-gmax: 8 bytes (7.84%) in 1 blocks (6.25%), avg size 8 bytes
+  â”‚   â”‚     At t-end:  8 bytes (7.84%) in 1 blocks (6.25%), avg size 8 bytes
+  â”‚   â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  - - - - - - - - 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x10871D: cm (sig.c:21)
+  â”‚   â”‚       #2: 0x108737: c2 (sig.c:23)
+  â”‚   â”‚       #3: 0x108931: main (sig.c:68)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â””─▼ AP 1.3.3/3 (2 children) {
+  â”‚         Total:     7 bytes (6.86%, 5.31/Minstr) in 2 blocks (12.5%, 1.52/Minstr), avg size 3.5 bytes, avg lifetime 7,147 instrs (0.54% of program duration)
+  â”‚         At t-gmax: 7 bytes (6.86%) in 2 blocks (12.5%), avg size 3.5 bytes
+  â”‚         At t-end:  7 bytes (6.86%) in 2 blocks (12.5%), avg size 3.5 bytes
+  â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Allocated at {
+  â”‚           ^1: 0x10871D: cm (sig.c:21)
+  â”‚           #2: 0x108751: c3 (sig.c:24)
+  â”‚         }
+  â”‚       }
+  â”‚       â”œâ”€â”€ AP 1.3.3.1/2 {
+  â”‚       â”‚     Total:     4 bytes (3.92%, 3.03/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 4 bytes, avg lifetime 7,173 instrs (0.54% of program duration)
+  â”‚       â”‚     Max:       4 bytes in 1 blocks, avg size 4 bytes
+  â”‚       â”‚     At t-gmax: 4 bytes (3.92%) in 1 blocks (6.25%), avg size 4 bytes
+  â”‚       â”‚     At t-end:  4 bytes (3.92%) in 1 blocks (6.25%), avg size 4 bytes
+  â”‚       â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚       â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚       â”‚     Accesses: {
+  â”‚       â”‚       [  0]  - - - - 
+  â”‚       â”‚     }
+  â”‚       â”‚     Allocated at {
+  â”‚       â”‚       ^1: 0x10871D: cm (sig.c:21)
+  â”‚       â”‚       ^2: 0x108751: c3 (sig.c:24)
+  â”‚       â”‚       #3: 0x10893F: main (sig.c:69)
+  â”‚       â”‚     }
+  â”‚       â”‚   }
+  â”‚       â””── AP 1.3.3.2/2 {
+  â”‚             Total:     3 bytes (2.94%, 2.27/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 3 bytes, avg lifetime 7,121 instrs (0.54% of program duration)
+  â”‚             Max:       3 bytes in 1 blocks, avg size 3 bytes
+  â”‚             At t-gmax: 3 bytes (2.94%) in 1 blocks (6.25%), avg size 3 bytes
+  â”‚             At t-end:  3 bytes (2.94%) in 1 blocks (6.25%), avg size 3 bytes
+  â”‚             Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚             Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚             Accesses: {
+  â”‚               [  0]  - - - 
+  â”‚             }
+  â”‚             Allocated at {
+  â”‚               ^1: 0x10871D: cm (sig.c:21)
+  â”‚               ^2: 0x108751: c3 (sig.c:24)
+  â”‚               #3: 0x10894D: main (sig.c:70)
+  â”‚             }
+  â”‚           }
+  â””─▼ AP 1.4/4 (3 children) {
+        Total:     21 bytes (20.59%, 15.92/Minstr) in 4 blocks (25%, 3.03/Minstr), avg size 5.25 bytes, avg lifetime 6,999.5 instrs (0.53% of program duration)
+        At t-gmax: 21 bytes (20.59%) in 4 blocks (25%), avg size 5.25 bytes
+        At t-end:  21 bytes (20.59%) in 4 blocks (25%), avg size 5.25 bytes
+        Reads:     0 bytes (0%, 0/Minstr), 0/byte
+        Writes:    0 bytes (0%, 0/Minstr), 0/byte
+        Allocated at {
+          #1: 0x10876B: dm (sig.c:27)
+        }
+      }
+      â”œâ”€â”€ AP 1.4.1/3 {
+      â”‚     Total:     8 bytes (7.84%, 6.07/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 8 bytes, avg lifetime 7,076 instrs (0.54% of program duration)
+      â”‚     Max:       8 bytes in 1 blocks, avg size 8 bytes
+      â”‚     At t-gmax: 8 bytes (7.84%) in 1 blocks (6.25%), avg size 8 bytes
+      â”‚     At t-end:  8 bytes (7.84%) in 1 blocks (6.25%), avg size 8 bytes
+      â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+      â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+      â”‚     Accesses: {
+      â”‚       [  0]  - - - - - - - - 
+      â”‚     }
+      â”‚     Allocated at {
+      â”‚       ^1: 0x10876B: dm (sig.c:27)
+      â”‚       #2: 0x10895B: main (sig.c:72)
+      â”‚     }
+      â”‚   }
+      â”œâ”€â”€ AP 1.4.2/3 {
+      â”‚     Total:     7 bytes (6.86%, 5.31/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 7 bytes, avg lifetime 7,026 instrs (0.53% of program duration)
+      â”‚     Max:       7 bytes in 1 blocks, avg size 7 bytes
+      â”‚     At t-gmax: 7 bytes (6.86%) in 1 blocks (6.25%), avg size 7 bytes
+      â”‚     At t-end:  7 bytes (6.86%) in 1 blocks (6.25%), avg size 7 bytes
+      â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+      â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+      â”‚     Accesses: {
+      â”‚       [  0]  - - - - - - - 
+      â”‚     }
+      â”‚     Allocated at {
+      â”‚       ^1: 0x10876B: dm (sig.c:27)
+      â”‚       #2: 0x108785: d2 (sig.c:29)
+      â”‚       #3: 0x108969: main (sig.c:73)
+      â”‚     }
+      â”‚   }
+      â””─▼ AP 1.4.3/3 (2 children) {
+            Total:     6 bytes (5.88%, 4.55/Minstr) in 2 blocks (12.5%, 1.52/Minstr), avg size 3 bytes, avg lifetime 6,948 instrs (0.53% of program duration)
+            At t-gmax: 6 bytes (5.88%) in 2 blocks (12.5%), avg size 3 bytes
+            At t-end:  6 bytes (5.88%) in 2 blocks (12.5%), avg size 3 bytes
+            Reads:     0 bytes (0%, 0/Minstr), 0/byte
+            Writes:    0 bytes (0%, 0/Minstr), 0/byte
+            Allocated at {
+              ^1: 0x10876B: dm (sig.c:27)
+              #2: 0x10879F: d3 (sig.c:30)
+            }
+          }
+          â”œâ”€â”€ AP 1.4.3.1/2 {
+          â”‚     Total:     4 bytes (3.92%, 3.03/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 4 bytes, avg lifetime 6,974 instrs (0.53% of program duration)
+          â”‚     Max:       4 bytes in 1 blocks, avg size 4 bytes
+          â”‚     At t-gmax: 4 bytes (3.92%) in 1 blocks (6.25%), avg size 4 bytes
+          â”‚     At t-end:  4 bytes (3.92%) in 1 blocks (6.25%), avg size 4 bytes
+          â”‚     Reads:     0 bytes (0%, 0/Minstr), 0/byte
+          â”‚     Writes:    0 bytes (0%, 0/Minstr), 0/byte
+          â”‚     Accesses: {
+          â”‚       [  0]  - - - - 
+          â”‚     }
+          â”‚     Allocated at {
+          â”‚       ^1: 0x10876B: dm (sig.c:27)
+          â”‚       ^2: 0x10879F: d3 (sig.c:30)
+          â”‚       #3: 0x108977: main (sig.c:74)
+          â”‚     }
+          â”‚   }
+          â””── AP 1.4.3.2/2 {
+                Total:     2 bytes (1.96%, 1.52/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 2 bytes, avg lifetime 6,922 instrs (0.52% of program duration)
+                Max:       2 bytes in 1 blocks, avg size 2 bytes
+                At t-gmax: 2 bytes (1.96%) in 1 blocks (6.25%), avg size 2 bytes
+                At t-end:  2 bytes (1.96%) in 1 blocks (6.25%), avg size 2 bytes
+                Reads:     0 bytes (0%, 0/Minstr), 0/byte
+                Writes:    0 bytes (0%, 0/Minstr), 0/byte
+                Accesses: {
+                  [  0]  - - 
+                }
+                Allocated at {
+                  ^1: 0x10876B: dm (sig.c:27)
+                  ^2: 0x10879F: d3 (sig.c:30)
+                  #3: 0x108985: main (sig.c:75)
+                }
+              }
+
+AP significance threshold: (total >= 0.51 bytes (0.5%)) && ((reads == 0 bytes) || (writes == 0 bytes))
+`
+//---------------------------------------------------------------------------
+    },
+    {
+      label: "Total (blocks), low-access",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: ./sig
+  PID:     21476
+}
+
+Times {
+  t-gmax: 1,311,861 instrs (99.48% of program duration)
+  t-end:  1,318,783 instrs
+}
+
+â–¼ AP 1/1 (2 children) {
+    Total:     16 blocks (100%, 12.13/Minstr)
+    Reads:     0.57/byte
+    Writes:    852.37/byte
+    Allocated at {
+      #0: [root]
+    }
+  }
+  â”œâ”€â”€ AP 1.1/2 {
+  â”‚     Total:     12 blocks (75%, 9.1/Minstr)
+  â”‚     Reads:     0.63/byte
+  â”‚     Writes:    941.68/byte
+  â”‚     Allocated at {
+  â”‚       [3 insignificant]
+  â”‚     }
+  â”‚   }
+  â””─▼ AP 1.2/2 (1 children) {
+        Total:     24 bytes (23.53%, 18.2/Minstr) in 4 blocks (25%, 3.03/Minstr), avg size 6 bytes, avg lifetime 47,708.5 instrs (3.62% of program duration)
+        At t-gmax: 24 bytes (23.53%) in 4 blocks (25%), avg size 6 bytes
+        At t-end:  24 bytes (23.53%) in 4 blocks (25%), avg size 6 bytes
+        Reads:     9 bytes (15.52%, 6.82/Minstr), 0.38/byte
+        Writes:    13,491 bytes (15.52%, 10,229.89/Minstr), 562.13/byte
+        Allocated at {
+          #1: 0x10871D: cm (sig.c:21)
+        }
+      }
+      â””── AP 1.2.1/1 {
+            Total:     4 blocks (25%, 3.03/Minstr)
+            Reads:     0.38/byte
+            Writes:    562.13/byte
+            Allocated at {
+              [3 insignificant]
+            }
+          }
+
+AP significance threshold: (total >= 0.08 blocks (0.5%)) && (reads != 0 bytes) && (writes != 0 bytes) && ((reads <= 0.4/byte) || (writes <= 0.4/byte))
+`
+//---------------------------------------------------------------------------
+    },
+    {
+      label: "Writes (bytes), high-access",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: ./sig
+  PID:     21476
+}
+
+Times {
+  t-gmax: 1,311,861 instrs (99.48% of program duration)
+  t-end:  1,318,783 instrs
+}
+
+â–¼ AP 1/1 (4 children) {
+    Writes:    86,942 bytes (100%, 65,925.93/Minstr), 852.37/byte
+    Allocated at {
+      #0: [root]
+    }
+  }
+  â”œâ”€â–¼ AP 1.1/4 (3 children) {
+  â”‚     Total:     30 bytes (29.41%, 22.75/Minstr) in 4 blocks (25%, 3.03/Minstr), avg size 7.5 bytes, avg lifetime 816,301.5 instrs (61.9% of program duration)
+  â”‚     At t-gmax: 30 bytes (29.41%) in 4 blocks (25%), avg size 7.5 bytes
+  â”‚     At t-end:  30 bytes (29.41%) in 4 blocks (25%), avg size 7.5 bytes
+  â”‚     Reads:     30 bytes (51.72%, 22.75/Minstr), 1/byte
+  â”‚     Writes:    44,970 bytes (51.72%, 34,099.62/Minstr), 1,499/byte
+  â”‚     Allocated at {
+  â”‚       #1: 0x108681: am (sig.c:9)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.1.1/3 {
+  â”‚   â”‚     Total:     11 bytes (10.78%, 8.34/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 11 bytes, avg lifetime 1,075,941 instrs (81.59% of program duration)
+  â”‚   â”‚     Max:       11 bytes in 1 blocks, avg size 11 bytes
+  â”‚   â”‚     At t-gmax: 11 bytes (10.78%) in 1 blocks (6.25%), avg size 11 bytes
+  â”‚   â”‚     At t-end:  11 bytes (10.78%) in 1 blocks (6.25%), avg size 11 bytes
+  â”‚   â”‚     Reads:     11 bytes (18.97%, 8.34/Minstr), 1/byte
+  â”‚   â”‚     Writes:    16,489 bytes (18.97%, 12,503.19/Minstr), 1,499/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  1500 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x108681: am (sig.c:9)
+  â”‚   â”‚       #2: 0x10883C: main (sig.c:57)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.1.2/3 {
+  â”‚   â”‚     Total:     10 bytes (9.8%, 7.58/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 10 bytes, avg lifetime 880,845 instrs (66.79% of program duration)
+  â”‚   â”‚     Max:       10 bytes in 1 blocks, avg size 10 bytes
+  â”‚   â”‚     At t-gmax: 10 bytes (9.8%) in 1 blocks (6.25%), avg size 10 bytes
+  â”‚   â”‚     At t-end:  10 bytes (9.8%) in 1 blocks (6.25%), avg size 10 bytes
+  â”‚   â”‚     Reads:     10 bytes (17.24%, 7.58/Minstr), 1/byte
+  â”‚   â”‚     Writes:    14,990 bytes (17.24%, 11,366.54/Minstr), 1,499/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  1500 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x108681: am (sig.c:9)
+  â”‚   â”‚       #2: 0x10869B: a2 (sig.c:11)
+  â”‚   â”‚       #3: 0x10885B: main (sig.c:58)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â””─▼ AP 1.1.3/3 (2 children) {
+  â”‚         Total:     9 bytes (8.82%, 6.82/Minstr) in 2 blocks (12.5%, 1.52/Minstr), avg size 4.5 bytes, avg lifetime 654,210 instrs (49.61% of program duration)
+  â”‚         At t-gmax: 9 bytes (8.82%) in 2 blocks (12.5%), avg size 4.5 bytes
+  â”‚         At t-end:  9 bytes (8.82%) in 2 blocks (12.5%), avg size 4.5 bytes
+  â”‚         Reads:     9 bytes (15.52%, 6.82/Minstr), 1/byte
+  â”‚         Writes:    13,491 bytes (15.52%, 10,229.89/Minstr), 1,499/byte
+  â”‚         Allocated at {
+  â”‚           ^1: 0x108681: am (sig.c:9)
+  â”‚           #2: 0x1086B5: a3 (sig.c:12)
+  â”‚         }
+  â”‚       }
+  â”‚       â”œâ”€â”€ AP 1.1.3.1/2 {
+  â”‚       â”‚     Total:     5 bytes (4.9%, 3.79/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 5 bytes, avg lifetime 702,250 instrs (53.25% of program duration)
+  â”‚       â”‚     Max:       5 bytes in 1 blocks, avg size 5 bytes
+  â”‚       â”‚     At t-gmax: 5 bytes (4.9%) in 1 blocks (6.25%), avg size 5 bytes
+  â”‚       â”‚     At t-end:  5 bytes (4.9%) in 1 blocks (6.25%), avg size 5 bytes
+  â”‚       â”‚     Reads:     5 bytes (8.62%, 3.79/Minstr), 1/byte
+  â”‚       â”‚     Writes:    7,495 bytes (8.62%, 5,683.27/Minstr), 1,499/byte
+  â”‚       â”‚     Accesses: {
+  â”‚       â”‚       [  0]  1500 ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚       â”‚     }
+  â”‚       â”‚     Allocated at {
+  â”‚       â”‚       ^1: 0x108681: am (sig.c:9)
+  â”‚       â”‚       ^2: 0x1086B5: a3 (sig.c:12)
+  â”‚       â”‚       #3: 0x10887A: main (sig.c:59)
+  â”‚       â”‚     }
+  â”‚       â”‚   }
+  â”‚       â””── AP 1.1.3.2/2 {
+  â”‚             Total:     4 bytes (3.92%, 3.03/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 4 bytes, avg lifetime 606,170 instrs (45.96% of program duration)
+  â”‚             Max:       4 bytes in 1 blocks, avg size 4 bytes
+  â”‚             At t-gmax: 4 bytes (3.92%) in 1 blocks (6.25%), avg size 4 bytes
+  â”‚             At t-end:  4 bytes (3.92%) in 1 blocks (6.25%), avg size 4 bytes
+  â”‚             Reads:     4 bytes (6.9%, 3.03/Minstr), 1/byte
+  â”‚             Writes:    5,996 bytes (6.9%, 4,546.62/Minstr), 1,499/byte
+  â”‚             Accesses: {
+  â”‚               [  0]  1500 ã€ƒ ã€ƒ ã€ƒ 
+  â”‚             }
+  â”‚             Allocated at {
+  â”‚               ^1: 0x108681: am (sig.c:9)
+  â”‚               ^2: 0x1086B5: a3 (sig.c:12)
+  â”‚               #3: 0x108899: main (sig.c:60)
+  â”‚             }
+  â”‚           }
+  â”œâ”€â–¼ AP 1.2/4 (3 children) {
+  â”‚     Total:     27 bytes (26.47%, 20.47/Minstr) in 4 blocks (25%, 3.03/Minstr), avg size 6.75 bytes, avg lifetime 295,093.25 instrs (22.38% of program duration)
+  â”‚     At t-gmax: 27 bytes (26.47%) in 4 blocks (25%), avg size 6.75 bytes
+  â”‚     At t-end:  27 bytes (26.47%) in 4 blocks (25%), avg size 6.75 bytes
+  â”‚     Reads:     19 bytes (32.76%, 14.41/Minstr), 0.7/byte
+  â”‚     Writes:    28,481 bytes (32.76%, 21,596.43/Minstr), 1,054.85/byte
+  â”‚     Allocated at {
+  â”‚       #1: 0x1086CF: bm (sig.c:15)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.2.1/3 {
+  â”‚   â”‚     Total:     10 bytes (9.8%, 7.58/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 10 bytes, avg lifetime 510,097 instrs (38.68% of program duration)
+  â”‚   â”‚     Max:       10 bytes in 1 blocks, avg size 10 bytes
+  â”‚   â”‚     At t-gmax: 10 bytes (9.8%) in 1 blocks (6.25%), avg size 10 bytes
+  â”‚   â”‚     At t-end:  10 bytes (9.8%) in 1 blocks (6.25%), avg size 10 bytes
+  â”‚   â”‚     Reads:     10 bytes (17.24%, 7.58/Minstr), 1/byte
+  â”‚   â”‚     Writes:    14,990 bytes (17.24%, 11,366.54/Minstr), 1,499/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  1500 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x1086CF: bm (sig.c:15)
+  â”‚   â”‚       #2: 0x1088B8: main (sig.c:62)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.2.2/3 {
+  â”‚   â”‚     Total:     9 bytes (8.82%, 6.82/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 9 bytes, avg lifetime 331,504 instrs (25.14% of program duration)
+  â”‚   â”‚     Max:       9 bytes in 1 blocks, avg size 9 bytes
+  â”‚   â”‚     At t-gmax: 9 bytes (8.82%) in 1 blocks (6.25%), avg size 9 bytes
+  â”‚   â”‚     At t-end:  9 bytes (8.82%) in 1 blocks (6.25%), avg size 9 bytes
+  â”‚   â”‚     Reads:     9 bytes (15.52%, 6.82/Minstr), 1/byte
+  â”‚   â”‚     Writes:    13,491 bytes (15.52%, 10,229.89/Minstr), 1,499/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  1500 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x1086CF: bm (sig.c:15)
+  â”‚   â”‚       #2: 0x1086E9: b2 (sig.c:17)
+  â”‚   â”‚       #3: 0x1088D7: main (sig.c:63)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â””── AP 1.2.3/3 {
+  â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Allocated at {
+  â”‚           [1 insignificant]
+  â”‚         }
+  â”‚       }
+  â”œâ”€â–¼ AP 1.3/4 (2 children) {
+  â”‚     Writes:    13,491 bytes (15.52%, 10,229.89/Minstr), 562.13/byte
+  â”‚     Allocated at {
+  â”‚       #1: 0x10871D: cm (sig.c:21)
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â”€ AP 1.3.1/2 {
+  â”‚   â”‚     Total:     9 bytes (8.82%, 6.82/Minstr) in 1 blocks (6.25%, 0.76/Minstr), avg size 9 bytes, avg lifetime 169,315 instrs (12.84% of program duration)
+  â”‚   â”‚     Max:       9 bytes in 1 blocks, avg size 9 bytes
+  â”‚   â”‚     At t-gmax: 9 bytes (8.82%) in 1 blocks (6.25%), avg size 9 bytes
+  â”‚   â”‚     At t-end:  9 bytes (8.82%) in 1 blocks (6.25%), avg size 9 bytes
+  â”‚   â”‚     Reads:     9 bytes (15.52%, 6.82/Minstr), 1/byte
+  â”‚   â”‚     Writes:    13,491 bytes (15.52%, 10,229.89/Minstr), 1,499/byte
+  â”‚   â”‚     Accesses: {
+  â”‚   â”‚       [  0]  1500 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 
+  â”‚   â”‚     }
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       ^1: 0x10871D: cm (sig.c:21)
+  â”‚   â”‚       #2: 0x108912: main (sig.c:67)
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â””── AP 1.3.2/2 {
+  â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚         Allocated at {
+  â”‚           [2 insignificant]
+  â”‚         }
+  â”‚       }
+  â””── AP 1.4/4 {
+        Writes:    0 bytes (0%, 0/Minstr), 0/byte
+        Allocated at {
+          [1 insignificant]
+        }
+      }
+
+AP significance threshold: (writes >= 434.71 bytes (0.5%)) && ((reads >= 1,000/byte) || (writes >= 1,000/byte))
+`
+//---------------------------------------------------------------------------
+    }
+  ]
+};
+tests.push(sig);
+
+//---------------------------------------------------------------------------
+// sig2 (doesn't corresponds to a .c file)
+//---------------------------------------------------------------------------
+
+let sig2 = {
+  name: "sig2",
+  input:
+//---------------------------------------------------------------------------
+{"dhatFileVersion":1
+,"cmd":"subseqs"
+,"pid":0
+,"mi":10000,"ei":20000
+,"aps":
+ [{"tb":100,"tbk":1,"tli":1000
+  ,"mb":100,"mbk":1
+  ,"gb":100,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[1]
+  }
+ ,{"tb":101,"tbk":1,"tli":1000
+  ,"mb":101,"mbk":1
+  ,"gb":101,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[2]
+  }
+ ,{"tb":102,"tbk":1,"tli":1000
+  ,"mb":102,"mbk":1
+  ,"gb":102,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[3,4]
+  }
+ ,{"tb":103,"tbk":1,"tli":1000
+  ,"mb":103,"mbk":1
+  ,"gb":103,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[3,5]
+  }
+ ,{"tb":104,"tbk":1,"tli":1000
+  ,"mb":104,"mbk":1
+  ,"gb":104,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[3,6,7]
+  }
+ ,{"tb":105,"tbk":1,"tli":1000
+  ,"mb":105,"mbk":1
+  ,"gb":105,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[3,6,8]
+  }
+ ,{"tb":10,"tbk":1,"tli":1000
+  ,"mb":10,"mbk":1
+  ,"gb":10,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[3,6,9,10]
+  }
+ ,{"tb":106,"tbk":1,"tli":1000
+  ,"mb":106,"mbk":1
+  ,"gb":106,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[3,6,9,11]
+  }
+ ,{"tb":107,"tbk":1,"tli":1000
+  ,"mb":107,"mbk":1
+  ,"gb":107,"gbk":1
+  ,"fb":0,"fbk":0
+  ,"rb":0,"wb":0
+  ,"acc":[-10,0]
+  ,"fs":[3,6,9,12]
+  }
+ ]
+,"ftbl":
+ ["[root]"
+ ,"a1()"
+ ,"a2()"
+ ,"a3()"
+ ,"b1()"
+ ,"b2()"
+ ,"b3()"
+ ,"c1()"
+ ,"c2()"
+ ,"c3()"
+ ,"d1()"
+ ,"d2()"
+ ,"d3()"
+ ]
+}
+//---------------------------------------------------------------------------
+  ,
+  outputs: [
+    {
+      label: "Total (blocks), tiny",
+      expected:
+//---------------------------------------------------------------------------
+`\
+Invocation {
+  Command: subseqs
+  PID:     0
+}
+
+Times {
+  t-gmax: 10,000 instrs (50% of program duration)
+  t-end:  20,000 instrs
+}
+
+â–¼ AP 1/1 (2 children) {
+    Total:     9 blocks (100%, 450/Minstr), avg size 93.11 bytes
+    Allocated at {
+      #0: [root]
+    }
+  }
+  â”œâ”€â–¼ AP 1.1/2 (2 children) {
+  â”‚     Total:     7 blocks (77.78%, 350/Minstr), avg size 91 bytes
+  â”‚     Allocated at {
+  â”‚       #1: a3()
+  â”‚     }
+  â”‚   }
+  â”‚   â”œâ”€â–¼ AP 1.1.1/2 (2 children) {
+  â”‚   â”‚     Total:     5 blocks (55.56%, 250/Minstr), avg size 86.4 bytes
+  â”‚   â”‚     Allocated at {
+  â”‚   â”‚       #2: b3()
+  â”‚   â”‚     }
+  â”‚   â”‚   }
+  â”‚   â”‚   â”œâ”€â–¼ AP 1.1.1.1/2 (2 children) {
+  â”‚   â”‚   â”‚     Total:     3 blocks (33.33%, 150/Minstr), avg size 74.33 bytes
+  â”‚   â”‚   â”‚     Allocated at {
+  â”‚   â”‚   â”‚       #3: c3()
+  â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   }
+  â”‚   â”‚   â”‚   â”œâ”€â”€ AP 1.1.1.1.1/2 {
+  â”‚   â”‚   â”‚   â”‚     Total:     2 blocks (22.22%, 100/Minstr), avg size 106.5 bytes
+  â”‚   â”‚   â”‚   â”‚     Allocated at {
+  â”‚   â”‚   â”‚   â”‚       [2 insignificant]
+  â”‚   â”‚   â”‚   â”‚     }
+  â”‚   â”‚   â”‚   â”‚   }
+  â”‚   â”‚   â”‚   â””── AP 1.1.1.1.2/2 {
+  â”‚   â”‚   â”‚         Total:     10 bytes (1.19%, 500/Minstr) in 1 blocks (11.11%, 50/Minstr), avg size 10 bytes, avg lifetime 1,000 instrs (5% of program duration)
+  â”‚   â”‚   â”‚         Max:       10 bytes in 1 blocks, avg size 10 bytes
+  â”‚   â”‚   â”‚         At t-gmax: 10 bytes (1.19%) in 1 blocks (11.11%), avg size 10 bytes
+  â”‚   â”‚   â”‚         At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  â”‚   â”‚   â”‚         Reads:     0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚         Writes:    0 bytes (0%, 0/Minstr), 0/byte
+  â”‚   â”‚   â”‚         Accesses: {
+  â”‚   â”‚   â”‚           [  0]  - - - - - - - - - - 
+  â”‚   â”‚   â”‚         }
+  â”‚   â”‚   â”‚         Allocated at {
+  â”‚   â”‚   â”‚           ^1: a3()
+  â”‚   â”‚   â”‚           ^2: b3()
+  â”‚   â”‚   â”‚           ^3: c3()
+  â”‚   â”‚   â”‚           #4: d1()
+  â”‚   â”‚   â”‚         }
+  â”‚   â”‚   â”‚       }
+  â”‚   â”‚   â””── AP 1.1.1.2/2 {
+  â”‚   â”‚         Total:     2 blocks (22.22%, 100/Minstr), avg size 104.5 bytes
+  â”‚   â”‚         Allocated at {
+  â”‚   â”‚           [2 insignificant]
+  â”‚   â”‚         }
+  â”‚   â”‚       }
+  â”‚   â””── AP 1.1.2/2 {
+  â”‚         Total:     2 blocks (22.22%, 100/Minstr), avg size 102.5 bytes
+  â”‚         Allocated at {
+  â”‚           [2 insignificant]
+  â”‚         }
+  â”‚       }
+  â””── AP 1.2/2 {
+        Total:     2 blocks (22.22%, 100/Minstr), avg size 100.5 bytes
+        Allocated at {
+          [2 insignificant]
+        }
+      }
+
+AP significance threshold: (total >= 0.05 blocks (0.5%)) && (total avg size <= 16 bytes)
+`
+//---------------------------------------------------------------------------
+    }
+  ]
+};
+tests.push(sig2);
+
+//---------------------------------------------------------------------------
+// Code
+//---------------------------------------------------------------------------
+
+function runTests() {
+  let pre = appendElement(gTestingDiv, "pre");
+
+  for (let [i, test] of tests.entries()) {
+    let name = test.name;
+    gData = test.input;
+
+    for (let output of test.outputs) {
+      // Set the sort metric.
+      let label = output.label;
+      let j = 0;
+      let labelFound = false;
+      for (let opt of gSelect.options) {
+        if (gSelectData[opt.value].label == label) {
+          gSelect.selectedIndex = j;
+          labelFound = true;
+          break;
+        }
+        j++;
+      }
+      assert(labelFound, "test label not found in gSelectData");
+
+      // Build and display the tree.
+      tryFunc(() => {
+        gFilename = "TEST MODE";
+        buildTree();
+        displayTree();
+      });
+
+      // Compare actual text output against expected.
+      let expected = output.expected;
+      let actual = gMainDiv.textContent;
+
+      let id = `Test ${i} - ${test.name} - ${label}`;
+
+      if (expected !== actual) {
+        // Test failed. Do a crude diff: find the line and column of the first
+        // char that differs.
+        let j = 0, line = 1, col = 1;
+        while (expected[j] === actual[j]) {
+          if (expected[j] === "\n") {
+            line++;
+            col = 1;
+          } else {
+            col++;
+          }
+          j++;
+        }
+
+        let s = `\
+FAIL - ${id}
+
+Expected length: ${expected.length}, actual length: ${actual.length}
+First differing char at ${line}:${col}
+
+EXPECTED OUTPUT
+<<<
+`;
+
+        // Print line numbers for the expected output, because it makes it much
+        // easier to find the first differing char.
+        for (let [n, line] of expected.split('\n').entries()) {
+          s += `${(n + 1).toString().padStart(3)} ${line}\n`;
+        }
+
+        s += ">>>";
+
+        appendElementWithText(pre, "div", s);
+        return;  // stop on the first failure
+      }
+
+      // Test passed.
+      appendElementWithText(pre, "div", `PASS - ${id}`);
+    }
+  }
+
+  clearMainDivWithText("All tests passed");
+}
+
+runTests();
+
diff --git a/dhat/dh_view.css b/dhat/dh_view.css
new file mode 100644 (file)
index 0000000..3d2c12c
--- /dev/null
@@ -0,0 +1,130 @@
+
+/*--------------------------------------------------------------------*/
+/*--- DHAT: a Dynamic Heap Analysis Tool               dh_view.css ---*/
+/*--------------------------------------------------------------------*/
+
+/*
+   This file is part of DHAT, a Valgrind tool for profiling the
+   heap usage of programs.
+
+   Copyright (C) 2018 Mozilla Foundation
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2 of the
+   License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+   02111-1307, USA.
+
+   The GNU General Public License is contained in the file COPYING.
+*/
+
+html {
+  background: #cfcfcf; /* pale grey */
+}
+
+.section {
+  border-radius: 10px;
+  background-color: white;
+  padding: 1em;
+  margin: 0.5em 0;
+}
+
+div.header {
+  font-weight: bold;
+  display: inline-block;
+  margin: 0 1.5em 0 0;
+  border-radius: 10px;
+  padding: 0.5em;
+  background-color: #cfcfcf; /* pale grey */
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+
+.hidden {
+  display: none;
+}
+
+.error {
+  color: red;
+}
+
+.invocation {
+  background-color: #bfd7d7; /* pale blue-grey */
+}
+
+.times {
+  background-color: #efdfbf; /* pale brown */
+}
+
+.arrow, .treeline {
+  background-color: white;
+}
+
+.internal {
+  cursor: pointer;
+}
+
+/* increasingly pale shades of green */
+.leaf.lt100 { background-color: #7fff7f; }
+.leaf.lt32  { background-color: #8fff8f; }
+.leaf.lt16  { background-color: #9fff9f; }
+.leaf.lt8   { background-color: #afffaf; }
+.leaf.lt4   { background-color: #bfffbf; }
+.leaf.lt2   { background-color: #cfffcf; }
+.leaf.lt1   { background-color: #dfffdf; }
+.leaf.insig { background-color: #efffef; }
+
+/* increasingly pale shades of yellow */
+.collapsed.lt100 { background-color: #ffff7f; }
+.collapsed.lt32  { background-color: #ffff8f; }
+.collapsed.lt16  { background-color: #ffff9f; }
+.collapsed.lt8   { background-color: #ffffaf; }
+.collapsed.lt4   { background-color: #ffffbf; }
+.collapsed.lt2   { background-color: #ffffcf; }
+.collapsed.lt1   { background-color: #ffffdf; }
+.collapsed.insig { background-color: #ffffef; }
+
+/* increasingly pale shades of blue */
+.expanded.lt100 { background-color: #7f7fff; }
+.expanded.lt32  { background-color: #8f8fff; }
+.expanded.lt16  { background-color: #9f9fff; }
+.expanded.lt8   { background-color: #afafff; }
+.expanded.lt4   { background-color: #bfbfff; }
+.expanded.lt2   { background-color: #cfcfff; }
+.expanded.lt1   { background-color: #dfdfff; }
+.expanded.insig { background-color: #efefff; }
+
+.bold {
+  font-weight: bold;
+}
+
+.threshold {
+  background-color: #dfdfdf; /* pale grey */
+}
+
+.noselect {
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+
+.legend, .timings {
+  font-size: 80%;
+  padding: 0 1em;
+}
+
+.debug {
+  font-size: 80%;
+}
diff --git a/dhat/dh_view.html b/dhat/dh_view.html
new file mode 100644 (file)
index 0000000..72f6b9d
--- /dev/null
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <link rel="stylesheet" href="dh_view.css">
+    <script src="dh_view.js"></script>
+  </head>
+
+  <body onload="onLoad()"></body>
+</html>
diff --git a/dhat/dh_view.js b/dhat/dh_view.js
new file mode 100644 (file)
index 0000000..d0c56b7
--- /dev/null
@@ -0,0 +1,1445 @@
+
+//--------------------------------------------------------------------*/
+//--- DHAT: a Dynamic Heap Analysis Tool                dh_view.js ---*/
+//--------------------------------------------------------------------*/
+
+/*
+   This file is part of DHAT, a Valgrind tool for profiling the
+   heap usage of programs.
+
+   Copyright (C) 2018 Mozilla Foundation
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 2 of the
+   License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+   02111-1307, USA.
+
+   The GNU General Public License is contained in the file COPYING.
+*/
+
+/*
+   Parts of this file are derived from Firefox, copyright Mozilla Foundation,
+   and are subject to the terms of the Mozilla Public License, v. 2.0. A copy
+   copy of the MPL can be obtained at http://mozilla.org/MPL/2.0/.
+*/
+
+// Test this file by loading dh_view.html?test=1. That runs the tests in
+// dh_test.js and gives pass/fail indicators.
+
+"use strict";
+
+//------------------------------------------------------------//
+//--- Globals                                              ---//
+//------------------------------------------------------------//
+
+// Important HTML elements.
+let gInput;
+let gSelect;
+let gHeaderDiv, gTestingDiv, gMainDiv, gLegendDiv, gTimingsDiv;
+
+// The name of the loaded file.
+let gFilename;
+
+// The object extracted from the JSON input.
+let gData;
+
+// The root of the radix tree build from gData. A radix tree is a
+// space-optimized prefix tree in which each node that is the only child is
+// merged with its parent.
+let gRoot;
+
+// Data relating to the sort metrics.
+//
+// - isDefault: True for the default sort metric.
+// - label: Used in the drop-down menu.
+// - bolds: Which fields to highlight in the output.
+// - cmpField: Field used to sort the radix tree.
+// - sig: Significance function used to determine aggregate nodes.
+// - sigLabel: Significance threshold description function.
+//
+const gSelectData = [
+  {
+    label: "Total (bytes)",
+    bolds: { "totalTitle": 1, "totalBytes": 1 },
+    cmpField: "_totalBytes",
+    sig: (aT) => aT._totalBytes >= 0.01 * gRoot._totalBytes,
+    sigLabel: () => `\
+total >= ${bytesAndPerc(0.01 * gRoot._totalBytes, gRoot._totalBytes)}`
+  },
+  {
+    isDefault: true,
+    label: "Total (blocks)",
+    bolds: { "totalTitle": 1, "totalBlocks": 1 },
+    cmpField: "_totalBlocks",
+    sig: (aT) => aT._totalBlocks >= 0.01 * gRoot._totalBlocks,
+    sigLabel: () => `\
+total >= ${blocksAndPerc(0.01 * gRoot._totalBlocks, gRoot._totalBlocks)}`
+  },
+  // No "Total (bytes), tiny" because it's extremely unlikely that an AP with a
+  // tiny average size will take up a significant number of bytes.
+  {
+    label: "Total (blocks), tiny",
+    bolds: { "totalTitle": 1, "totalBlocks": 1, "totalAvgSizeBytes": 1 },
+    cmpField: "_totalBlocks",
+    sig: (aT) => aT._totalBlocks >= 0.005 * gRoot._totalBlocks &&
+                 aT._totalAvgSizeBytes() <= 16,
+    sigLabel: () => `\
+(total >= ${blocksAndPerc(0.005 * gRoot._totalBlocks, gRoot._totalBlocks)}) && \
+(total avg size <= ${bytes(16)})`
+  },
+  // No "Total (bytes), short-lived", because an AP with few large, short-lived
+  // blocks is unlikely. (In contrast, "Total (blocks), short-lived" is useful,
+  // because an AP with many small, short-lived blocks *is* likely.) And if
+  // such an AP existed, it'll probably show up in "Total (bytes), zero reads
+  // or zero writes" or "Total (bytes), low-access" anyway, because there's
+  // little time for accesses in 500 instructions.
+  {
+    label: "Total (blocks), short-lived",
+    bolds: { "totalTitle": 1, "totalBlocks": 1, "totalAvgLifetimeInstrs": 1 },
+    cmpField: "_totalBlocks",
+    sig: (aT) => aT._totalBlocks >= 0.005 * gRoot._totalBlocks &&
+                 aT._totalAvgLifetimeInstrs() <= 500,
+    sigLabel: () => `\
+(total >= ${blocksAndPerc(0.005 * gRoot._totalBlocks, gRoot._totalBlocks)}) && \
+(total avg lifetime <= ${instrs(500)})`
+  },
+  {
+    label: "Total (bytes), zero reads or zero writes",
+    bolds: { "totalTitle": 1, "totalBytes": 1,
+             "readsTitle": 1, "readsBytes": 1,
+             "writesTitle": 1, "writesBytes": 1,
+           },
+    cmpField: "_totalBytes",
+    sig: (aT) => aT._totalBytes >= 0.005 * gRoot._totalBytes &&
+                 (aT._readsBytes === 0 || aT._writesBytes === 0),
+    sigLabel: () => `\
+(total >= ${bytesAndPerc(0.005 * gRoot._totalBytes, gRoot._totalBytes)}) && \
+((reads == ${bytes(0)}) || (writes == ${bytes(0)}))`
+  },
+  {
+    label: "Total (blocks), zero reads or zero writes",
+    bolds: { "totalTitle": 1, "totalBlocks": 1,
+             "readsTitle": 1, "readsBytes": 1,
+             "writesTitle": 1, "writesBytes": 1,
+           },
+    cmpField: "_totalBlocks",
+    sig: (aT) => aT._totalBlocks >= 0.005 * gRoot._totalBlocks &&
+                 (aT._readsBytes === 0 || aT._writesBytes === 0),
+    sigLabel: () => `\
+(total >= ${blocksAndPerc(0.005 * gRoot._totalBlocks, gRoot._totalBlocks)}) && \
+((reads == ${bytes(0)}) || (writes == ${bytes(0)}))`
+  },
+  {
+    label: "Total (bytes), low-access",
+    bolds: { "totalTitle": 1, "totalBytes": 1,
+             "readsTitle": 1, "readsAvgPerByte": 1,
+             "writesTitle": 1, "writesAvgPerByte": 1,
+           },
+    cmpField: "_totalBytes",
+    sig: (aT) => aT._totalBytes >= 0.005 * gRoot._totalBytes &&
+                 aT._readsBytes !== 0 &&
+                 aT._writesBytes !== 0 &&
+                 (aT._readsAvgPerByte() <= 0.4 ||
+                  aT._writesAvgPerByte() <= 0.4),
+    sigLabel: () => `\
+(total >= ${bytesAndPerc(0.005 * gRoot._totalBytes, gRoot._totalBytes)}) && \
+(reads != ${bytes(0)}) && \
+(writes != ${bytes(0)}) && \
+((reads <= ${perByte(0.4)}) || (writes <= ${perByte(0.4)}))`
+  },
+  {
+    label: "Total (blocks), low-access",
+    bolds: { "totalTitle": 1, "totalBlocks": 1,
+             "readsTitle": 1, "readsAvgPerByte": 1,
+             "writesTitle": 1, "writesAvgPerByte": 1,
+           },
+    cmpField: "_totalBlocks",
+    sig: (aT) => aT._totalBlocks >= 0.005 * gRoot._totalBlocks &&
+                 aT._readsBytes !== 0 &&
+                 aT._writesBytes !== 0 &&
+                 (aT._readsAvgPerByte() <= 0.4 ||
+                  aT._writesAvgPerByte() <= 0.4),
+    sigLabel: () => `\
+(total >= ${blocksAndPerc(0.005 * gRoot._totalBlocks, gRoot._totalBlocks)}) && \
+(reads != ${bytes(0)}) && \
+(writes != ${bytes(0)}) && \
+((reads <= ${perByte(0.4)}) || (writes <= ${perByte(0.4)}))`
+  },
+  // No "Total (avg size bytes)": not interesting.
+  // No "Total (avg lifetime instrs)": covered by "Total (blocks), short-lived".
+  // No "Max (bytes)": not interesting, and unclear how to sort.
+  // No "Max (blocks)": not interesting, and unclear how to sort.
+  // No "Max (avg size bytes)": not interesting, and unclear how to sort.
+  {
+    label: "At t-gmax (bytes)",
+    bolds: { "atTGmaxTitle": 1, "atTGmaxBytes": 1 },
+    cmpField: "_atTGmaxBytes",
+    sig: (aT) => aT._atTGmaxBytes >= 0.01 * gRoot._atTGmaxBytes,
+    sigLabel: () => `\
+at-t-gmax >= ${bytesAndPerc(0.01 * gRoot._atTGmaxBytes, gRoot._atTGmaxBytes)}`
+  },
+  // No "At t-gmax (blocks)": not interesting.
+  // No "At t-gmax (avg size bytes)": not interesting.
+  {
+    label: "At t-end (bytes)",
+    bolds: { "atTEndTitle": 1, "atTEndBytes": 1 },
+    cmpField: "_atTEndBytes",
+    sig: (aT) => aT._atTEndBytes >= 0.01 * gRoot._atTEndBytes,
+    sigLabel: () => `\
+at-t-end >= ${bytesAndPerc(0.01 * gRoot._atTEndBytes, gRoot._atTEndBytes)}`
+  },
+  // No "At t-end (blocks)": not interesting.
+  // No "At t-end (avg size bytes)": not interesting.
+  {
+    label: "Reads (bytes)",
+    bolds: { "readsTitle": 1, "readsBytes": 1 },
+    cmpField: "_readsBytes",
+    sig: (aT) => aT._readsBytes >= 0.01 * gRoot._readsBytes,
+    sigLabel: () => `\
+reads >= ${bytesAndPerc(0.01 * gRoot._readsBytes, gRoot._readsBytes)}`
+  },
+  {
+    label: "Reads (bytes), high-access",
+    bolds: { "readsTitle": 1, "readsBytes": 1, "readsAvgPerByte": 1 },
+    cmpField: "_readsBytes",
+    sig: (aT) => aT._readsBytes >= 0.005 * gRoot._readsBytes &&
+                 (aT._readsAvgPerByte() >= 1000 ||
+                  aT._writesAvgPerByte() >= 1000),
+    sigLabel: () => `\
+(reads >= ${bytesAndPerc(0.005 * gRoot._readsBytes, gRoot._readsBytes)}) && \
+((reads >= ${perByte(1000)}) || (writes >= ${perByte(1000)}))`
+  },
+  // No "Reads (avg per byte)": covered by other access-related ones.
+  {
+    label: "Writes (bytes)",
+    bolds: { "writesTitle": 1, "writesBytes": 1 },
+    cmpField: "_writesBytes",
+    sig: (aT) => aT._writesBytes >= 0.01 * gRoot._writesBytes,
+    sigLabel: () => `\
+writes >= ${bytesAndPerc(0.01 * gRoot._writesBytes, gRoot._writesBytes)}`
+  },
+  {
+    label: "Writes (bytes), high-access",
+    bolds: { "writesTitle": 1, "writesBytes": 1, "writesAvgPerByte": 1 },
+    cmpField: "_writesBytes",
+    sig: (aT) => aT._writesBytes >= 0.005 * gRoot._writesBytes &&
+                 (aT._readsAvgPerByte() >= 1000 ||
+                  aT._writesAvgPerByte() >= 1000),
+    sigLabel: () => `\
+(writes >= ${bytesAndPerc(0.005 * gRoot._writesBytes, gRoot._writesBytes)}) && \
+((reads >= ${perByte(1000)}) || (writes >= ${perByte(1000)}))`
+  }
+  // No "Writes (avg per byte)": covered by other access-related ones.
+];
+
+//------------------------------------------------------------//
+//--- Utilities                                            ---//
+//------------------------------------------------------------//
+
+// Assertion. Fails if aMsg is missing.
+function assert(aCond, aMsg) {
+  if (!aCond || !aMsg) {
+    throw new Error(`assertion failed: ${aMsg}`);
+  }
+}
+
+// Division function that returns 0 instead of NaN for 0/0, which is what we
+// always want.
+function div(aNum, aDenom) {
+  return aNum === 0 && aDenom === 0 ? 0 : aNum / aDenom;
+}
+
+// Execute a function, printing any exception to the page.
+function tryFunc(aFunc) {
+  try {
+    aFunc();
+  } catch (ex) {
+    // Clear gRoot, so that any old or partially-built new value doesn't hang
+    // around if after this exception is thrown.
+    gRoot = undefined;
+    clearMainDivWithText(ex.toString(), "error");
+    throw ex;
+  }
+}
+
+// Put some text in a div at the bottom of the page. Useful for debugging.
+function debug(x) {
+  let section = appendElement(document.body, "div", "section");
+  appendElementWithText(section, "div", JSON.stringify(x), "debug noselect");
+}
+
+//------------------------------------------------------------//
+//--- Radix tree building                                  ---//
+//------------------------------------------------------------//
+
+// Notes about the TreeNode kinds:
+//
+// --------------------------------------------------------------------
+//                              Leaf          Internal        Aggregate
+// --------------------------------------------------------------------
+// Has this._kids?              No            Yes             No
+// Has this._max*?              Yes           No              No
+// Has this._accesses?          Maybe         Maybe           No
+// Allowed this._sig values?    Self,None     Self,Desc,None  None
+// How many this._add() calls?  1             1+              1+
+// --------------------------------------------------------------------
+//
+const kLeaf     = 1;
+const kInternal = 2;
+const kAgg      = 3;
+
+function TreeNode(aKind, aFrames) {
+  this._kind = aKind;
+
+  this._totalBytes = 0;
+  this._totalBlocks = 0;
+
+  this._totalLifetimesInstrs = 0;
+
+  // These numbers only make sense for leaf nodes. Unlike total stats, which
+  // can be summed, _maxBytes/_maxBlocks for two APs can't be easily combined
+  // because the maxes may have occurred at different times.
+  if (this._kind === kLeaf) {
+    this._maxBytes = 0;
+    this._maxBlocks = 0;
+  }
+
+  this._atTGmaxBytes = 0;
+  this._atTGmaxBlocks = 0;
+
+  this._atTEndBytes = 0;
+  this._atTEndBlocks = 0;
+
+  this._readsBytes = 0;
+  this._writesBytes = 0;
+
+  // this._accesses is left undefined. It will be added if necessary.
+  // The possible values have the following meanings:
+  // - undefined means "unset accesses" (i.e. new node, never been set)
+  // - length==0 means "no accesses" (i.e. some kids have accesses and some
+  //   don't, or all kids have accesses but in different sizes)
+  // - length>0 means "accesses" (i.e. all kids have accesses and all the same
+  //   size)
+
+  // If a node would only have a single child, we instead effectively inline it
+  // in the parent. Therefore a node can have multiple frames.
+  this._frames = aFrames;
+
+  // this._kids is left undefined. It will be added if necessary.
+
+  // this._sig is added later, by sigTree().
+}
+
+TreeNode.prototype = {
+  _add(aTotalBytes, aTotalBlocks, aTotalLifetimesInstrs, aMaxBytes,
+       aMaxBlocks, aAtTGmaxBytes, aAtTGmaxBlocks, aAtTEndBytes,
+       aAtTEndBlocks, aReadsBytes, aWritesBytes, aAccesses) {
+
+    // We ignore this._kind, this._frames, and this._kids.
+
+    this._totalBytes += aTotalBytes;
+    this._totalBlocks += aTotalBlocks;
+    this._totalLifetimesInstrs += aTotalLifetimesInstrs;
+
+    if (this._kind === kLeaf) {
+      // Leaf nodes should only be added to once, because DHAT currently
+      // produces records with unique locations. If we remove addresses from
+      // frames in the future then something must be done here to sum non-zero
+      // _maxBytes and _maxBlocks values, but it's unclear exactly what. Range
+      // arithmetic is a (complicated) possibility.
+      assert(this._maxBytes === 0, "bad _maxBytes: " + this._maxBytes);
+      assert(this._maxBlocks === 0, "bad _maxBlocks: " + this._maxBlocks);
+      this._maxBytes += aMaxBytes;
+      this._maxBlocks += aMaxBlocks;
+    }
+
+    this._atTGmaxBytes += aAtTGmaxBytes;
+    this._atTGmaxBlocks += aAtTGmaxBlocks;
+
+    this._atTEndBytes += aAtTEndBytes;
+    this._atTEndBlocks += aAtTEndBlocks;
+
+    this._readsBytes += aReadsBytes;
+    this._writesBytes += aWritesBytes;
+
+    if (this._kind !== kAgg) {
+      if (!this._accesses && aAccesses) {
+        // unset accesses += accesses --> has accesses (must clone the array)
+        this._accesses = aAccesses.slice();
+      } else if (this._accesses && aAccesses &&
+                 this._accesses.length === aAccesses.length) {
+        // accesses += accesses (with matching lengths) --> accesses
+        for (let i = 0; i < this._accesses.length; i++) {
+          this._accesses[i] += aAccesses[i];
+        }
+      } else {
+        // any other combination --> no accesses
+        this._accesses = [];
+      }
+    } else {
+      assert(!this._accesses, "agg nodes cannot have accesses");
+    }
+  },
+
+  _addAP(aAP) {
+    this._add(aAP.tb, aAP.tbk, aAP.tli, aAP.mb, aAP.mbk, aAP.gb, aAP.gbk,
+              aAP.fb, aAP.fbk, aAP.rb, aAP.wb, aAP.acc);
+  },
+
+  // This is called in two cases.
+  // - Splitting a node, where we are adding to a fresh node (i.e. effectively
+  //   cloning a node).
+  // - Aggregating multiple nodes.
+  _addNode(aT) {
+    this._add(aT._totalBytes, aT._totalBlocks, aT._totalLifetimesInstrs,
+              aT._maxBytes, aT._maxBlocks, aT._atTGmaxBytes, aT._atTGmaxBlocks,
+              aT._atTEndBytes, aT._atTEndBlocks,
+              aT._readsBytes, aT._writesBytes, aT._accesses);
+  },
+
+  // Split the node after the aTi'th internal frame. The inheriting kid will
+  // get the post-aTi frames; the new kid will get aNewFrames.
+  _split(aTi, aAP, aNewFrames) {
+    // kid1 inherits t's kind and values.
+    let inheritedFrames = this._frames.splice(aTi + 1);
+    let kid1 = new TreeNode(this._kind, inheritedFrames);
+    if (this._kids) {
+      kid1._kids = this._kids;
+    }
+    kid1._addNode(this);
+
+    // Put all remaining frames into kid2.
+    let kid2 = new TreeNode(kLeaf, aNewFrames);
+    kid2._addAP(aAP);
+
+    // Update this.
+    if (this._kind === kLeaf) {
+      // Convert to an internal node.
+      this._kind = kInternal;
+      assert(this.hasOwnProperty("_maxBytes"), "missing _maxBytes");
+      assert(this.hasOwnProperty("_maxBlocks"), "missing _maxBlocks");
+      delete this._maxBytes;
+      delete this._maxBlocks;
+    }
+    this._kids = [kid1, kid2];
+    this._addAP(aAP);
+  },
+
+  _totalAvgSizeBytes() {
+    return div(this._totalBytes, this._totalBlocks);
+  },
+
+  _totalAvgLifetimeInstrs() {
+    return div(this._totalLifetimesInstrs, this._totalBlocks);
+  },
+
+  _maxAvgSizeBytes() {
+    assert(this._kind === kLeaf, "non-leaf node");
+    return div(this._maxBytes, this._maxBlocks);
+  },
+
+  _atTGmaxAvgSizeBytes() {
+    return div(this._atTGmaxBytes, this._atTGmaxBlocks);
+  },
+
+  _atTEndAvgSizeBytes() {
+    return div(this._atTEndBytes, this._atTEndBlocks);
+  },
+
+  _readsAvgPerByte() {
+    return div(this._readsBytes, this._totalBytes);
+  },
+
+  _writesAvgPerByte() {
+    return div(this._writesBytes, this._totalBytes);
+  }
+}
+
+// Check if the fields in `aFields` are present in `aObj`.
+function checkFields(aObj, aFields) {
+  for (let f of aFields) {
+    if (!aObj.hasOwnProperty(f)) {
+      throw new Error(`data file is missing a field: ${f}`);
+    }
+  }
+}
+
+// Do basic checking of an AP read from file.
+function checkAP(aAP) {
+  let fields = ["tb", "tbk", "tli",
+                "mb", "mbk",
+                "gb", "gbk",
+                "fb", "fbk",
+                "rb", "wb",
+                "fs"];
+  checkFields(aAP, fields);
+}
+
+// Access counts latch as 0xffff. Treating 0xffff as Infinity gives us exactly
+// the behaviour we want, e.g. Infinity + 1 = Infinity.
+function normalizeAccess(aAcc) {
+  if (aAcc < 0xffff) {
+    return aAcc;
+  }
+  if (aAcc === 0xffff) {
+    return Infinity;
+  }
+  assert(false, "too-large access value");
+}
+
+const kExpectedFileVersion = 1;
+
+// Build gRoot from gData.
+function buildTree() {
+  // Check global values.
+  let fields = ["dhatFileVersion",
+                "cmd", "pid",
+                "mi", "ei",
+                "aps", "ftbl"];
+  checkFields(gData, fields);
+  if (gData.dhatFileVersion != kExpectedFileVersion) {
+      throw Error(`data file has version number ${gData.dhatFileVersion}, ` +
+                  `expected version number ${kExpectedFileVersion}`);
+  }
+
+  // Build the radix tree. Nodes are in no particular order to start with. The
+  // algorithm is tricky because we need to use internal frames when possible.
+  gRoot = new TreeNode(kLeaf, [0]);   // Frame 0 is always "[root]".
+
+  for (let [i, ap] of gData.aps.entries()) {
+    checkAP(ap);
+
+    // Decompress the run-length encoding in `acc`, if present.
+    if (ap.acc) {
+      let acc = [];
+      for (let i = 0; i < ap.acc.length; i++) {
+        if (ap.acc[i] < 0) {
+          // A negative number encodes a repeat count. The following entry has
+          // the value to be repeated.
+          let reps = -ap.acc[i++];
+          let val = ap.acc[i];
+          for (let j = 0; j < reps; j++) {
+            acc.push(normalizeAccess(val));
+          }
+        } else {
+          acc.push(normalizeAccess(ap.acc[i]));
+        }
+      }
+      ap.acc = acc;
+    }
+
+    // The first AP is a special case, because we have to build gRoot.
+    if (i === 0) {
+      gRoot._frames.push(...ap.fs);
+      gRoot._addAP(ap);
+      continue;
+    }
+
+    let t = gRoot;  // current node
+    let ti = 0;     // current frame index within t
+    let done = false;
+
+    // In the examples below, tree nodes have the form `abcd:N-Xs`, where
+    // `abcd` is a frame sequence (and `-` is an empty sequence), `N` is a node
+    // value, and `Xs` are the node's children.
+
+    for (let [j, kidFrame] of ap.fs.entries()) {
+
+      // Search for kidFrame among internal frames.
+      if (ti + 1 < t._frames.length) {
+        // t has an internal frame at the right index.
+
+        if (t._frames[ti + 1] === kidFrame) {
+          // The internal frame matches. Move to t's next internal frame.
+          ti++;
+        } else {
+          // The internal frame doesn't match. Split the node.
+          //
+          // E.g. abcd:20-[] + abef:10 => ab:30-[cd:20-[], ef:10-[]]
+          t._split(ti, ap, ap.fs.slice(j));
+          done = true;
+          break;
+        }
+
+      } else {
+        // We've run out of internal frames in t. Consider t's kids.
+
+        if (!t._kids) {
+          // No kids; this must be a supersequence of an existing sequence.
+          // Split t; the inheriting kid will get no frames, the new kid will
+          // get the leftover frames.
+          //
+          // E.g. ab:20-[] + abcd:10 => ab:30-[-:20-[], cd:10-[]]
+          t._split(ti, ap, ap.fs.slice(j));
+          done = true;
+          break;
+        }
+
+        t._addAP(ap);
+
+        // Search for the frame among the kids.
+        let kid;
+        for (let k of t._kids) {
+          if (k._frames[0] === kidFrame) {
+            kid = k;
+            break;
+          }
+        }
+        if (kid) {
+          // Found it. Move to it.
+          t = kid;
+          ti = 0;
+        } else {
+          // Didn't find it. Put all remaining frames into a new leaf node.
+          //
+          // E.g. ab:20-[c:10-Xs, d:10-Ys] + abef:10 =>
+          //      ab:30-[c:10-Xs, d:10-Ys, ef:10-[]]
+          kid = new TreeNode(kLeaf, ap.fs.slice(j));
+          kid._addAP(ap);
+          t._kids.push(kid);
+          done = true;
+          break;
+        }
+      }
+    }
+
+    if (!done) {
+      // If we reach here, either:
+      // - ap's frames match an existing frame sequence, in which case we
+      //   just need to _addAP(); or
+      // - ap's frames are a subsequence of an existing sequence, in which
+      //   case we must split.
+
+      if (ti + 1 < t._frames.length) {
+        // A subsequence of an existing sequence that ends within t's internal
+        // frames. Split, creating an empty node.
+        //
+        // E.g. abcd:20-Xs + ab:10 => ab:30-[cd:20-Xs, -:10-[]]
+        t._split(ti, ap, []);
+
+      } else if (!t._kids) {
+        // This is impossible because DHAT currently produces records with
+        // unique locations. If we remove addresses from frames in the future
+        // then duplicate locations will occur, and the following code is how
+        // it must be handled.
+        throw Error(`data file contains a repeated location`);
+
+        // Matches an existing sequence that doesn't end in node with empty
+        // frames. Add the AP.
+        //
+        // E.g. ab:20-[] + ab:10 => ab:30-[]
+        t._addAP(ap);
+
+      } else {
+        // Look for a kid with empty frames.
+        let emptyKid;
+        for (let k of t._kids) {
+          if (k._frames.length === 0) {
+            emptyKid = k;
+            break;
+          }
+        }
+
+        if (emptyKid) {
+          // This is impossible because DHAT currently produces records with
+          // unique locations. If we remove addresses from frames in the future
+          // then duplicate locations will occur, and the following code is how
+          // it must be handled.
+          throw Error(`data file contains a repeated location`);
+
+          // Matches an existing sequence that ends in a node with empty
+          // frames. Add the AP.
+          //
+          // E.g. ab:20-[c:10-Xs, -:10-[]] + ab:10 => ab:30-[c:10-Xs, -:20-[]]
+          t._addAP(ap);
+          emptyKid._addAP(ap);
+
+        } else {
+          // A subsequence of an existing sequence that ends at the end of t's
+          // internal frames. Append an empty node.
+          //
+          // E.g. ab:20-[c:10-Xs, d:10-Ys] + ab:10 =>
+          //      ab:30-[c:10-Xs, d:10-Ys, -:10-[]]
+          let newKid = new TreeNode(kLeaf, []);
+          newKid._addAP(ap);
+
+          t._kids.push(newKid);
+          t._addAP(ap);
+        }
+      }
+    }
+
+  }
+}
+
+//------------------------------------------------------------//
+//--- Pretty printers                                      ---//
+//------------------------------------------------------------//
+
+// Using Intl.NumberFormat makes things faster than using toLocaleString()
+// repeatedly.
+const kPFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, style: "percent" });
+const kDFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }); // decimal
+const kTFormat = new Intl.NumberFormat(); // time
+
+function perc(aNum, aDenom) {
+  return kPFormat.format(div(aNum, aDenom));
+}
+
+function perMinstr(aN) {
+  return `${kDFormat.format(div(1000000 * aN, gData.ei))}/Minstr`;
+}
+
+function bytes(aN) {
+  return `${kDFormat.format(aN)} bytes`;
+}
+
+function bytesAndPerc(aN, aTotalN) {
+  return `${bytes(aN)} (${perc(aN, aTotalN)})`;
+}
+
+function bytesAndPercAndRate(aN, aTotalN) {
+  return `${bytes(aN)} (${perc(aN, aTotalN)}, ${perMinstr(aN)})`;
+}
+
+function blocks(aN) {
+  return `${kDFormat.format(aN)} blocks`;
+}
+
+function blocksAndPerc(aN, aTotalN) {
+  return `${blocks(aN)} (${perc(aN, aTotalN)})`;
+}
+
+function blocksAndPercAndRate(aN, aTotalN) {
+  return `${blocks(aN)} (${perc(aN, aTotalN)}, ${perMinstr(aN)})`;
+}
+
+function avgSizeBytes(aN) {
+  return `avg size ${bytes(aN)}`;
+}
+
+function perByte(aN) {
+  return `${kDFormat.format(aN)}/byte`;
+}
+
+function instrs(aN) {
+  return `${kDFormat.format(aN)} instrs`;
+}
+
+function avgLifetimeInstrs(aN) {
+  return `avg lifetime ${instrs(aN)}`;
+}
+
+function accesses(aAccesses) {
+  // Make zero stand out.
+  if (aAccesses === 0) {
+    return "-";
+  }
+
+  if (aAccesses === Infinity) {
+    return "∞";
+  }
+
+  // Don't use toLocaleString() -- in this case the values rarely reach
+  // 100,000, and the grid formatting means the separators tend to make the
+  // numbers harder to read. (And locales such as fr-FR use ' ' as the
+  // separator, which conflicts with our use of ' ' between values!)
+  return aAccesses.toString();
+}
+
+function ms(aNum) {
+  // This function is called only a handful of times, so there is no need to
+  // use Intl.NumberFormat.
+  return aNum !== undefined ? `${kTFormat.format(aNum)}ms` : "n/a";
+}
+
+//------------------------------------------------------------//
+//--- DOM manipulation                                     ---//
+//------------------------------------------------------------//
+
+const kDocumentTitle = "DHAT Viewer";
+
+document.title = kDocumentTitle;
+
+function appendElement(aP, aTagName, aClassName) {
+  let e = document.createElement(aTagName);
+  if (aClassName) {
+    e.className = aClassName;
+  }
+  aP.appendChild(e);
+  return e;
+}
+
+function appendElementWithText(aP, aTagName, aText, aClassName) {
+  let e = appendElement(aP, aTagName, aClassName);
+  e.textContent = aText;
+  return e;
+}
+
+function appendText(aP, aText) {
+  let e = document.createTextNode(aText);
+  aP.appendChild(e);
+  return e;
+}
+
+function clearDiv(aDiv) {
+  // Replace aDiv with an empty node.
+  assert(aDiv, "no div given");
+  let tmp = aDiv.cloneNode(/* deep = */ false);
+  aDiv.parentNode.replaceChild(tmp, aDiv);
+  return tmp;
+}
+
+function clearMainDiv() {
+  gMainDiv = clearDiv(gMainDiv);
+}
+
+function clearTimingsDiv() {
+  gTimingsDiv = clearDiv(gTimingsDiv);
+}
+
+function clearMainDivWithText(aText, aClassName) {
+  clearMainDiv();
+  appendElementWithText(gMainDiv, "span", aText, aClassName);
+}
+
+function appendInvocationAndTimes(aP) {
+  let v, v1, v2;
+
+  v = "Invocation {\n";
+  v += `  Command: ${gData.cmd}\n`;
+  v += `  PID:     ${gData.pid}\n`;
+  v += "}\n\n";
+
+  appendElementWithText(aP, "span", v, "invocation");
+
+  v = "Times {\n";
+
+  v1 = perc(gData.mi, gData.ei);
+  v += `  t-gmax: ${instrs(gData.mi)} (${v1} of program duration)\n`;
+  v += `  t-end:  ${instrs(gData.ei)}\n`;
+
+  v += "}\n\n";
+
+  appendElementWithText(aP, "span", v, "times");
+}
+
+// Arrows indicating what state a node is in.
+const kNoKidsArrow      = "─ ";     // cannot change
+const kHidingKidsArrow  = "â–¶ ";     // expandible
+const kShowingKidsArrow = "â–¼ ";     // collapsible
+
+// HTML doesn't have a tree element, so we fake one with text. One nice
+// consequence is that you can copy and paste the output. The non-ASCII chars
+// used (for arrows and tree lines) usually reproduce well when pasted into
+// textboxes.
+//
+// - aT: The sub-tree to append.
+// - aP: Parent HTML element to append to.
+// - aBolds: Which fields to highlight in the output.
+// - aPc: The percentage function.
+// - aCmp: The comparison function.
+// - aSig: The significance function.
+// - aNodeIdNums: The node ID numbers, e.g. [1,2,3], which is printed "1.2.3".
+// - aNumSibs: The number of siblings that aT has.
+// - aOldFrames: Frames preceding this node's frames.
+// - aTlFirst: Treeline for the first line of the node.
+// - aTlRest: Treeline for the other lines of the node, and its kids.
+//
+function appendTreeInner(aT, aP, aBolds, aCmp, aPc, aSig, aNodeIdNums,
+                         aNumSibs, aOldFrames, aTlFirst, aTlRest) {
+  // The primary element we'll be appending to.
+  let p;
+
+  // We build up text fragments in up to seven groups:
+  // - pre-Bold1 (multiple)
+  // - Bold1 (single)
+  // - post-Bold1 (multiple)
+  // - Bold2 (single)
+  // - post-Bold2 (multiple)
+  // - Bold3 (single)
+  // - post-Bold3 (multiple)
+  //
+  // This is so that up to 3 bold sequences can be highlighted per line.
+  let frags, fi;
+
+  // Clear the text fragments.
+  function clear() {
+    frags = [[], undefined, [], undefined, [], undefined, []];
+    fi = 0;
+  }
+
+  // Add a fragment.
+  // - aShowIfInsig: should we show this even in an insignificant node?
+  // - aIsBold: if this is shown, should it be bold? If undefined (as is
+  //   common) it takes the same value as aShowIfInsig.
+  function fr(aStr, aShowIfInsig, aIsBold) {
+    if (!aShowIfInsig && aT._sig !== kSigSelf) {
+      return;
+    }
+
+    if (aIsBold === undefined) {
+      aIsBold = aShowIfInsig;
+    }
+
+    if (aIsBold) {
+      assert(fi === 0 || fi === 2 || fi === 4, "bad fragIndex (1)");
+      assert(frags[fi + 1] === undefined, "bold already here");
+      frags[fi + 1] = aStr;
+      fi += 2;
+    } else {
+      assert(fi === 0 || fi === 2 || fi === 4 || fi === 6, "bad fragIndex (2)");
+      frags[fi].push(aStr);
+    }
+  }
+
+  // Add a newline fragment (with a following treeline, unless aIsLast==true).
+  // - aShowIfInsig: should we show this even in an insignificant node?
+  // - aIsLast: is this the last newline for the node?
+  function nl(aShowIfInsig, aIsLast) {
+    assert(fi === 0 || fi === 2 || fi === 4 || fi === 6, "bad fragIndex (3)");
+    if (!aShowIfInsig && aT._sig !== kSigSelf) {
+      return;
+    }
+
+    frags[fi].push("\n");
+
+    // Alternate the non-bold fragments (each in a text node) and bold
+    // fragments (each in a span).
+    if (frags[0].length > 0) {
+      appendText(p, frags[0].join(""));
+    }
+    if (frags[1] !== undefined) {
+      appendElementWithText(p, "span", frags[1], "bold");
+    }
+    if (frags[2].length > 0) {
+      appendText(p, frags[2].join(""));
+    }
+    if (frags[3] !== undefined) {
+      appendElementWithText(p, "span", frags[3], "bold");
+    }
+    if (frags[4].length > 0) {
+      appendText(p, frags[4].join(""));
+    }
+    if (frags[5] !== undefined) {
+      appendElementWithText(p, "span", frags[5], "bold");
+    }
+    if (frags[6].length > 0) {
+      appendText(p, frags[6].join(""));
+    }
+
+    if (!aIsLast) {
+      appendElementWithText(p, "span", aTlRest, "treeline");
+      clear();
+    }
+  }
+
+  clear();
+
+  // Traverse the kids, aggregating insignificant nodes.
+  let kids;
+  if (aT._kids) {
+    kids = [];
+    let agg, nAgg = 0;
+
+    for (let kid of aT._kids) {
+      assert(kid._sig === kSigSelf || kid._sig === kSigDesc ||
+             kid._sig === kSigNone, "kid _sig not set");
+
+      if (kid._sig !== kSigNone) {
+        // `kid` is at least partially significant. Just push it as-is.
+        kids.push(kid);
+      } else {
+        // `kid` is insignificant. Aggregate it.
+        if (!agg) {
+          // We fill in ._frames below, once we know how many kids were
+          // aggregated.
+          agg = new TreeNode(kAgg, undefined);
+          agg._sig = kSigNone;
+          kids.push(agg);
+        }
+        nAgg++;
+        agg._addNode(kid);
+      }
+    }
+
+    if (agg) {
+      // Fill in agg._frames.
+      let insigFrame = `[${nAgg} insignificant]`;
+      agg._frames = [insigFrame];
+    }
+
+    kids.sort(aCmp);
+  }
+  // Note: need to use `kids` for the rest of this function, not `aT._kids`.
+
+  // Put the percentage into a colour band. The obvious way to do this is
+  // with equal-sized bands (e.g. 0--20%, 20--40%, ...) but that doesn't work
+  // well because in practice we have few nodes with mid-to-high percentages,
+  // and many nodes with small percentages. So we use a logarithmic
+  // distribution instead, so small values are better distinguished. (This is
+  // reasonable in a way: a 2% node is twice as important as a 1%, a 4% node
+  // is twice as important as a 2% node, etc.)
+  let pc = aPc(aT);
+  let lt = (aT._sig !== kSigSelf) ? "insig" // insignificant nodes
+         : (pc <  1) ? "lt1"                //  0% to   0.999%
+         : (pc <  2) ? "lt2"                //  1% to   1.999%
+         : (pc <  4) ? "lt4"                //  2% to   3.999%
+         : (pc <  8) ? "lt8"                //  4% to   7.999%
+         : (pc < 16) ? "lt16"               //  8% to  15.999%
+         : (pc < 32) ? "lt32"               // 16% to  31.999%
+         :             "lt100";             // 32% to 100%
+
+  // Append the primary element.
+  let arrow;
+  if (kids) {
+    p = appendElement(aP, "span", lt + " internal expanded");
+    p.onclick = toggleClass;
+    arrow = kShowingKidsArrow;
+  } else {
+    p = appendElement(aP, "span", lt + " leaf");
+    arrow = kNoKidsArrow;
+  }
+
+  // Node start: treeline and arrow.
+  appendElementWithText(p, "span", aTlFirst, "treeline");
+  appendElementWithText(p, "span", arrow, "arrow");
+
+  let v1, v2, v3, v4, v5;
+
+  // "AP" + node ID + kid count.
+  v1 = aNodeIdNums.join('.');
+  v2 = aNumSibs + 1;
+  v3 = kids ? `(${kids.length} children) ` : "";
+  fr(`AP ${v1}/${v2} ${v3}{`, true, false);
+  nl(true);
+
+  // "Total".
+  v1 = bytesAndPercAndRate(aT._totalBytes, gRoot._totalBytes);
+  v2 = blocksAndPercAndRate(aT._totalBlocks, gRoot._totalBlocks);
+  v3 = avgSizeBytes(aT._totalAvgSizeBytes());
+  v4 = avgLifetimeInstrs(aT._totalAvgLifetimeInstrs());
+  v5 = perc(aT._totalAvgLifetimeInstrs(), gData.ei);
+  fr("  Total:     ", aBolds.totalTitle);
+  fr(v1, aBolds.totalBytes);
+  fr(" in ");
+  fr(v2, aBolds.totalBlocks);
+  fr(", ", aBolds.totalAvgSizeBytes, false);
+  fr(v3, aBolds.totalAvgSizeBytes);
+  fr(", ", aBolds.totalAvgLifetimeInstrs, false);
+  fr(`${v4} (${v5} of program duration)`, aBolds.totalAvgLifetimeInstrs);
+  nl(aBolds.totalTitle);
+
+  // "Max".
+  if (aT !== gRoot && aT._kind === kLeaf) {
+    assert(!kids, "leaf node has children");
+    // These percentages are relative to the local totals, not the root
+    // totals.
+    v1 = bytes(aT._maxBytes);
+    v2 = blocks(aT._maxBlocks);
+    v3 = avgSizeBytes(aT._maxAvgSizeBytes());
+    fr(`  Max:       ${v1} in ${v2}, ${v3}`);
+    nl();
+  }
+
+  // "At t-gmax".
+  v1 = bytesAndPerc(aT._atTGmaxBytes, gRoot._atTGmaxBytes);
+  v2 = blocksAndPerc(aT._atTGmaxBlocks, gRoot._atTGmaxBlocks);
+  v3 = avgSizeBytes(aT._atTGmaxAvgSizeBytes());
+  fr("  At t-gmax: ", aBolds.atTGmaxTitle);
+  fr(v1, aBolds.atTGmaxBytes);
+  fr(` in ${v2}, ${v3}`);
+  nl(aBolds.atTGmaxTitle);
+
+  // "At t-end".
+  v1 = bytesAndPerc(aT._atTEndBytes, gRoot._atTEndBytes);
+  v2 = blocksAndPerc(aT._atTEndBlocks, gRoot._atTEndBlocks);
+  v3 = avgSizeBytes(aT._atTEndAvgSizeBytes());
+  fr("  At t-end:  ", aBolds.atTEndTitle);
+  fr(v1, aBolds.atTEndBytes);
+  fr(` in ${v2}, ${v3}`);
+  nl(aBolds.atTEndTitle);
+
+  // "Reads".
+  v1 = bytesAndPercAndRate(aT._readsBytes, gRoot._readsBytes);
+  v2 = perByte(aT._readsAvgPerByte());
+  fr("  Reads:     ", aBolds.readsTitle);
+  fr(v1, aBolds.readsBytes);
+  fr(", ", aBolds.readsBytes && aBolds.readsAvgPerByte, false);
+  fr(v2, aBolds.readsAvgPerByte);
+  nl(aBolds.readsTitle);
+
+  // "Writes".
+  v1 = bytesAndPercAndRate(aT._writesBytes, gRoot._writesBytes);
+  v2 = perByte(aT._writesAvgPerByte());
+  fr("  Writes:    ", aBolds.writesTitle);
+  fr(v1, aBolds.writesBytes);
+  fr(", ", aBolds.writesBytes && aBolds.writesAvgPerByte, false);
+  fr(v2, aBolds.writesAvgPerByte);
+  nl(aBolds.writesTitle);
+
+  // "Accesses". We show 32 per line (but not on aggregate nodes).
+  if (aT._accesses && aT._accesses.length > 0) {
+    let v = "  Accesses: {";
+    let prevN;
+    for (let [i, n] of aT._accesses.entries()) {
+      if ((i % 32) === 0) {
+        fr(v);
+        nl();
+        v1 = i.toString().padStart(3, ' ');
+        v = `    [${v1}]  `;
+        v += `${accesses(n)} `;
+      } else {
+        // Use a ditto mark for repeats.
+        v += (n === prevN && n !== 0) ? "〃 " : `${accesses(n)} `;
+      }
+      prevN = n;
+    }
+    fr(v);
+    nl();
+
+    fr("  }");
+    nl();
+  }
+
+  // "Allocated at".
+  fr("  Allocated at {", true, false);
+  nl(true);
+  if (aT._kind === kAgg) {
+    // Don't print ancestor frames; just print the "insignificant" frame.
+    let isInsigFrame = (aFrm) => aFrm.indexOf(" insignificant]") >= 0;
+    assert(aT._frames.length === 1 && isInsigFrame(aT._frames[0]),
+           "bad aggregate node");
+    fr(`    ${aT._frames[0]}`, true, false);
+    nl(true);
+  } else {
+    // Start numbering frames from #1, unless it's the root node, in which case
+    // we show "#0: [root]".
+    let i = (aT === gRoot) ? 0 : 1;
+
+    // Maybe show frames from ancestor nodes, excluding "[root]" (by starting
+    // at j=1).
+    for (let j = 1; j < aOldFrames.length; j++, i++) {
+      fr(`    ^${i}: ${gData.ftbl[aOldFrames[j]]}`);
+      nl(false);
+    }
+    // Show frames from this node.
+    for (let j = 0; j < aT._frames.length; j++, i++) {
+      fr(`    #${i}: ${gData.ftbl[aT._frames[j]]}`, true, false);
+      nl(true);
+    }
+  }
+  fr("  }", true, false);
+  nl(true);
+
+  // End of node.
+  fr(`}`, true, false);
+  nl(true, true);
+
+  // Do the kids.
+  if (kids) {
+    assert(aT._kind !== kLeaf, "leaf node has children");
+
+    p = appendElement(aP, "span", "kids");
+
+    // tlFirstFor{Most,Last} are shorter than tlRestFor{Most,Last} to allow
+    // space for the arrow.
+    let tlFirstForMost;
+    let tlRestForMost;
+    if (kids.length > 1) {
+      tlFirstForMost = aTlRest + "├─";
+      tlRestForMost  = aTlRest + "│   ";
+    }
+    let tlFirstForLast = aTlRest + "└─";
+    let tlRestForLast  = aTlRest + "    ";
+
+    for (let [i, kid] of kids.entries()) {
+      let n = aT._frames.length;
+      aOldFrames.push(...aT._frames); // append aT._frames to aOldFrames
+      aNodeIdNums.push(i + 1);
+      let isLast = i === kids.length - 1;
+      appendTreeInner(kid, p, aBolds, aCmp, aPc, aSig, aNodeIdNums,
+                      kids.length - 1, aOldFrames,
+                      !isLast ? tlFirstForMost : tlFirstForLast,
+                      !isLast ? tlRestForMost : tlRestForLast);
+      aNodeIdNums.pop(i);
+      aOldFrames.splice(-n);         // remove aT._frames from aOldFrames
+    }
+  }
+}
+
+// Node significance.
+// - kSigSelf: the node itself is significant. It will be shown in full.
+// - kSigDesc: the node itself is insignificant, but it has one or more
+//   significant descendants. (This is not possible for the straightforward
+//   additive sort metrics like total-bytes, but it is possible for the
+//   non-additive ones like "Total (bytes), short-lived", "Total (bytes),
+//   low-access", etc.) It will be shown abbreviated.
+// - kSigNone: the node itself is insignificant, and it has no significant
+//   descendants. It will be aggregated.
+const kSigSelf = 3;
+const kSigDesc = 2;
+const kSigNone = 1;
+
+// Fill in the ._sig field of all tree nodes.
+function sigTree(aT, aSig) {
+  let sig = false;
+  if (aT._kids) {
+    for (let kid of aT._kids) {
+      sig |= sigTree(kid, aSig);
+    }
+  }
+
+  if (aSig(aT)) {
+    aT._sig = kSigSelf;
+    return true;
+  }
+  if (sig) {
+    aT._sig = kSigDesc;
+    return true;
+  }
+  aT._sig = kSigNone;
+  return false;
+}
+
+function appendTree(aP, aBolds, aCmp, aPc, aSig) {
+  sigTree(gRoot, aSig);
+
+  appendTreeInner(gRoot, aP, aBolds, aCmp, aPc, aSig, [1], 0, [], "", "  ");
+}
+
+function appendSignificanceThreshold(aP, aSigLabel) {
+  let v = `\nAP significance threshold: ${aSigLabel()}\n`;
+  appendElementWithText(aP, "span", v, "threshold");
+}
+
+// Check that aElem's class list contains at least one name from aClassNames.
+function classListContains(aElem, aClassNames) {
+  for (let className of aClassNames) {
+    if (aElem.classList.contains(className)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+function assertClassListContains(aElem, aClassNames) {
+  assert(aElem, "undefined elem");
+  assert(classListContains(aElem, aClassNames),
+         `none of ${JSON.stringify(aClassNames)} found in class list`);
+}
+
+// Called when a node with kids is clicked on.
+function toggleClass(aEvent) {
+  let clickedNode = aEvent.target;
+  let hasKidsNode;
+  if (classListContains(clickedNode, ["expanded", "collapsed"])) {
+    // The click must have been on a text node, so clickedNode is the node
+    // to toggle.
+    hasKidsNode = clickedNode;
+  } else {
+    // The click must have been on a span element, so the parent node is
+    // the node to toggle.
+    hasKidsNode = clickedNode.parentNode;
+    assertClassListContains(hasKidsNode, ["expanded", "collapsed"]);
+  }
+  hasKidsNode.classList.toggle("expanded");
+  hasKidsNode.classList.toggle("collapsed");
+
+  // Element order: 0: treeline span, 1: arrow span, ...
+  let arrowSpan = hasKidsNode.childNodes[1];
+  assertClassListContains(arrowSpan, ["arrow"]);
+  if (arrowSpan.textContent === kHidingKidsArrow) {
+    arrowSpan.textContent = kShowingKidsArrow;
+  } else if (arrowSpan.textContent === kShowingKidsArrow) {
+    arrowSpan.textContent = kHidingKidsArrow;
+  } else {
+    assert(false, `bad arrowSpan textContent`);
+  }
+
+  // Toggle visibility of the span containing this node's kids.
+  let kidsSpan = hasKidsNode.nextSibling;
+  assertClassListContains(kidsSpan, ["kids"]);
+  kidsSpan.classList.toggle("hidden");
+}
+
+//------------------------------------------------------------//
+//--- Top-level stuff                                      ---//
+//------------------------------------------------------------//
+
+// These arguments will be `undefined` when displayTree() is called without
+// having read a file (e.g. when redisplaying with a different sort metric).
+function displayTree(aTRead, aTParse, aTBuild) {
+  let tRead = aTRead === undefined ? 0 : aTRead;
+  let tParse = aTParse === undefined ? 0 : aTParse;
+  let tBuild = aTBuild === undefined ? 0 : aTBuild;
+
+  // Get details relating to the chosen sort metrics.
+  let data = gSelectData[gSelect.selectedIndex];
+  let bolds = data.bolds;
+  let label = data.label;
+  let cmpField = data.cmpField;
+  let sig = data.sig;
+  let sigLabel = data.sigLabel;
+  let cmp = (aT1, aT2) => {
+    // Try the specified sort metric. If that doesn't distinguish them, sort by
+    // _totalBytes.
+    let s1 = aT2[cmpField] - aT1[cmpField];
+    return (s1 !== 0) ? s1 : aT2._totalBytes - aT1._totalBytes;
+  };
+  let pc = (aT) => div(aT[cmpField], gRoot[cmpField]) * 100;
+
+  // Update the page title.
+  document.title = `${kDocumentTitle} - ${gFilename} - ${label}`;
+
+  // Build the main part of the page.
+  let now = performance.now();
+  clearMainDiv();
+  let pre = appendElement(gMainDiv, "pre");
+  appendInvocationAndTimes(pre);
+  appendTree(pre, bolds, cmp, pc, sig);
+  appendSignificanceThreshold(pre, sigLabel);
+  let tDisplay = performance.now() - now;
+
+  let tTotal = tRead + tParse + tBuild + tDisplay;
+  clearTimingsDiv();
+  let timings = `\
+Processing time: \
+read:${ms(aTRead)} + \
+parse:${ms(aTParse)} + \
+build:${ms(aTBuild)} + \
+display:${ms(tDisplay)} = \
+total:${ms(tTotal)}\
+`;
+  appendElementWithText(gTimingsDiv, "p", timings);
+}
+
+function loadFile() {
+  clearMainDivWithText("Loading...");
+
+  let now = performance.now();
+  let file = gInput.files[0];
+  gFilename = file.name;
+
+  // Update the title. This will likely be overwritten very shortly, unless
+  // there's a file loading problem, in which case it's nice to have the
+  // correct filename in the title.
+  document.title = `${kDocumentTitle} - ${gFilename}`;
+
+  let reader = new FileReader();
+  reader.onload = function(aEvent) {
+    tryFunc(() => {
+      let tRead = performance.now() - now;
+
+      let data = aEvent.target.result;
+
+      now = performance.now();
+      gData = JSON.parse(data);
+      let tParse = performance.now() - now;
+
+      now = performance.now();
+      buildTree();
+      let tBuild = performance.now() - now;
+
+      displayTree(tRead, tParse, tBuild);
+    });
+  };
+
+  reader.onerror = function(aEvent) {
+    clearMainDivWithText("Error loading file", "error");
+  };
+
+  reader.readAsText(file);
+}
+
+function changeSortMetric() {
+  // If we have a tree, redisplay it for the new sort metric.
+  if (gRoot) {
+    tryFunc(() => {
+      displayTree();
+    });
+  }
+}
+
+// Top-level setup when the page is first loaded.
+function onLoad() {
+  // Check if tests should be run.
+  let params = new URLSearchParams(document.location.search.substring(1));
+  let test = params.get("test");
+
+  // The header div.
+  gHeaderDiv = appendElement(document.body, "div", "section");
+
+  // The (hidden) input element.
+  let inputDiv = appendElement(gHeaderDiv, "div", "header");
+  appendElementWithText(inputDiv, "div", "File");
+  gInput = appendElement(inputDiv, "input", "hidden");
+  gInput.type = "file";
+  gInput.onchange = loadFile;
+
+  // The button that triggers the hidden input element.
+  let b = appendElementWithText(inputDiv, "button", "Load…");
+  b.onclick = () => gInput.click();
+
+  // The sort metric menu.
+  let selectDiv = appendElement(gHeaderDiv, "div", "header");
+  appendElementWithText(selectDiv, "div", "Sort metric");
+  gSelect = appendElement(selectDiv, "select");
+  gSelect.onchange = changeSortMetric;
+  for (let [i, data] of gSelectData.entries()) {
+    let option = appendElementWithText(gSelect, "option", data.label);
+    option.value = i;
+    if (data.isDefault) {
+      option.selected = true;
+    }
+  }
+
+  // The testing div, if necessary.
+  if (test) {
+    gTestingDiv = appendElement(document.body, "div", "testing");
+  }
+
+  // The main div.
+  gMainDiv = appendElement(document.body, "div", "section");
+  appendElementWithText(gMainDiv, "span", "Load a DHAT data file to begin");
+
+  // The legend div. We show it even before loading a file so that new users
+  // are immediately aware that it exists.
+  gLegendDiv = appendElement(document.body, "div", "legend noselect");
+  let p = appendElementWithText(gLegendDiv, "p", "Legend:");
+  let ul = appendElement(p, "ul");
+  appendElementWithText(ul, "li", "'t-gmax': time of global heap maximum " +
+                                  "(as measured in bytes)");
+  appendElementWithText(ul, "li", "'t-end': time of program end");
+  appendElementWithText(ul, "li", "'instrs': instructions");
+  appendElementWithText(ul, "li", "'Minstr': mega-instruction, i.e. one " +
+                                  "million instructions");
+  appendElementWithText(ul, "li", "'AP': allocation point");
+  appendElementWithText(ul, "li", "'avg': average");
+  appendElementWithText(ul, "li", "'-' (in accesses): zero");
+  appendElementWithText(ul, "li", "'∞' (in accesses): leaf AP counts max out " +
+                                  "at 65534; larger counts are treated as " +
+                                  "infinity");
+  appendElementWithText(ul, "li", "'〃' (in accesses): same as previous entry");
+
+  // The timings div.
+  gTimingsDiv = appendElement(document.body, "div", "timings noselect");
+
+  if (test) {
+    appendElementWithText(gHeaderDiv, "div", "TEST MODE", "header");
+    var script = document.createElement("script");
+    script.src = "dh_test.js";
+    document.body.appendChild(script);
+  }
+}
+
diff --git a/dhat/docs/dh-manual.xml b/dhat/docs/dh-manual.xml
new file mode 100644 (file)
index 0000000..56f5b6a
--- /dev/null
@@ -0,0 +1,654 @@
+<?xml version="1.0"?> <!-- -*- sgml -*- -->
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+          "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
+[ <!ENTITY % vg-entities SYSTEM "../../docs/xml/vg-entities.xml"> %vg-entities; ]>
+
+
+<chapter id="dh-manual" 
+         xreflabel="DHAT: a dynamic heap analysis tool">
+  <title>DHAT: a dynamic heap analysis tool</title>
+
+<para>To use this tool, you must specify
+<option>--tool=dhat</option> on the Valgrind command line.</para>
+
+
+
+<sect1 id="dh-manual.overview" xreflabel="Overview">
+<title>Overview</title>
+
+<para>DHAT is a tool for examining how programs use their heap
+allocations.</para>
+
+<para>It tracks the allocated blocks, and inspects every memory access
+to find which block, if any, it is to. It presents, on an allocation point
+basis, information about these blocks such as sizes, lifetimes, numbers of
+reads and writes, and read and write patterns.</para>
+
+<para>Using this information it is possible to identify allocation points with
+the following characteristics:</para>
+
+<itemizedlist>
+
+  <listitem><para>potential process-lifetime leaks: blocks allocated
+   by the point just accumulate, and are freed only at the end of the
+   run.</para></listitem>
+
+ <listitem><para>excessive turnover: points which chew through a lot
+  of heap, even if it is not held onto for very long</para></listitem>
+
+ <listitem><para>excessively transient: points which allocate very
+ short lived blocks</para></listitem>
+
+ <listitem><para>useless or underused allocations: blocks which are
+  allocated but not completely filled in, or are filled in but not
+  subsequently read.</para></listitem>
+
+ <listitem><para>blocks with inefficient layout -- areas never
+  accessed, or with hot fields scattered throughout the
+  block.</para></listitem>
+</itemizedlist>
+
+<para>As with the Massif heap profiler, DHAT measures program progress
+by counting instructions, and so presents all age/time related figures
+as instruction counts. This sounds a little odd at first, but it
+makes runs repeatable in a way which is not possible if CPU time is
+used.</para>
+
+</sect1>
+
+
+
+<sect1 id="dh-manual.profile" xreflabel="Using DHAT">
+<title>Using DHAT</title>
+
+<para>First off, as for normal Valgrind use, you probably want to compile with
+debugging info (the <option>-g</option> option). But by contrast with normal
+Valgrind use, you probably do want to turn optimisation on, since you should
+profile your program as it will be normally run.</para>
+
+<para>Second, you need to run your program under DHAT to gather the profiling
+information.</para>
+
+<para>Finally, you need to use DHAT's viewer (in a web browser) to get a
+detailed presentation of that information.</para>
+
+
+<sect2 id="dh-manual.running-DHAT" xreflabel="Running DHAT">
+<title>Running DHAT</title>
+
+<para>To run DHAT on a program <filename>prog</filename>, run:</para>
+<screen><![CDATA[
+valgrind --tool=dhat prog
+]]></screen>
+
+<para>The program will execute (slowly). Upon completion, summary statistics
+that look like this will be printed:</para>
+
+<programlisting><![CDATA[
+==11514== Total:     823,849,731 bytes in 3,929,133 blocks
+==11514== At t-gmax: 133,485,082 bytes in 436,521 blocks
+==11514== At t-end:  258,002 bytes in 2,129 blocks
+==11514== Reads:     2,807,182,810 bytes
+==11514== Writes:    1,149,617,086 bytes
+]]></programlisting>
+
+<para>The first line shows how many heap blocks and bytes were allocated over
+the entire execution.</para>
+
+<para>The second line shows how many heap blocks and bytes were alive at
+<computeroutput>t-gmax</computeroutput>, i.e. the time when the heap size
+reached its global maximum (as measured in bytes).</para>
+
+<para>The third line shows how many heap blocks and bytes were alive at
+<computeroutput>t-end</computeroutput>, i.e. the end of execution. In other
+words, how many blocks and bytes were not explicitly freed. </para>
+
+<para>The fourth and fifth lines show how many bytes within heap blocks were
+read and written during the entire execution. </para>
+
+<para>These lines are moderately interesting at best. More useful information
+can be seen with DHAT's viewer.</para>
+
+</sect2>
+
+
+<sect2 id="dh-manual.outputfile" xreflabel="Output File">
+<title>Output File</title>
+
+<para>As well as printing summary information, DHAT also writes more detailed
+profiling information to a file. By default this file is named
+<filename>dhat.out.&lt;pid&gt;</filename> (where
+<filename>&lt;pid&gt;</filename> is the program's process ID), but its name can
+be changed with the <option>--dhat-out-file</option> option. This file is JSON,
+and intended to be viewed by DHAT's viewer, which is described in the next
+section.</para>
+
+<para>The default <computeroutput>.&lt;pid&gt;</computeroutput> suffix on the
+output file name serves two purposes. Firstly, it means you don't have to
+rename old log files that you don't want to overwrite. Secondly, and more
+importantly, it allows correct profiling with the
+<option>--trace-children=yes</option> option of programs that spawn child
+processes.</para>
+
+<para>The output file can be big, many megabytes for large applications
+built with full debugging information.</para>
+
+</sect2>
+
+</sect1>
+
+
+
+<sect1 id="dh-manual.viewer" xreflabel="DHAT's viewer">
+<title>DHAT's Viewer</title>
+
+<para>DHAT's viewer can be run in a web browser by loading the file
+<computeroutput>dh_view.html</computeroutput>. Use the "Load" button to choose
+a DHAT output file to view.</para>
+
+
+<sect2><title>The Output Header</title>
+
+<para>The first part of the output shows the program command and process ID.
+For example:</para>
+
+<programlisting><![CDATA[
+Invocation {
+  Command: /home/njn/moz/rust0/build/x86_64-unknown-linux-gnu/stage2/bin/rustc --crate-name tuple_stress src/main.rs
+  PID:     18816
+}
+]]></programlisting>
+
+<para>The second part of the output shows the
+<computeroutput>t-gmax</computeroutput> and
+<computeroutput>t-end</computeroutput> values again. For example:</para>
+
+<programlisting><![CDATA[
+Times {
+  t-gmax: 8,138,210,673 instrs (86.92% of program duration)
+  t-end:  9,362,544,994 instrs
+}
+]]></programlisting>
+
+</sect2>
+
+
+<sect2><title>The AP Tree</title>
+
+<para>The third part of the output is the largest and most interesting part,
+showing the allocation point (AP) tree.</para>
+
+
+<sect3><title>Structure</title>
+
+The following image shows a screenshot of part of an AP tree. The font is very
+small because this screenshot is intended to demonstrate the high-level
+structure of the tree rather than the details within the text.
+
+<graphic fileref="images/dh-tree.png" scalefit="1"/>
+
+<para>Like any tree, it has a root node, leaf nodes, and non-leaf nodes. The
+structure of the tree is shown by the lines connecting nodes. Child nodes are
+beneath their parent and indented one level.</para>
+
+<para>The sub-trees beneath a non-leaf node can be collapsed or expanded by
+clicking on the node. It is useful to collapse sub-trees that you aren't
+interested in.</para>
+
+<para>Colours are meaningful, and are intended to ease tree navigation, but the
+information they represent is also present within the text. (This means that
+colour-blind users are not denied any information.)</para>
+
+<para>Each leaf node is coloured green. Each non-leaf node is coloured blue
+and has a down arrow (<computeroutput>â–¼</computeroutput>) next to it when
+its sub-tree is expanded. Each non-leaf node is coloured yellow and has a
+left arrow (<computeroutput>â–¶</computeroutput>) next to it when its sub-tree
+is collapsed.</para>
+
+<para>The shade of green, blue or yellow used for a node indicate its
+significance. Darker shades represent greater significance (in terms of bytes
+or blocks).</para>
+
+<para>Note that the entire output is text, even the arrows and lines connecting
+nodes. This means you can copy and paste any part of the output easily into an
+email, bug report, etc.</para>
+
+</sect3>
+
+
+<sect3><title>The Root Node</title>
+
+<para>The root node looks like this:</para>
+
+<programlisting><![CDATA[
+AP 1/1 (25 children) {
+  Total:     1,355,253,987 bytes (100%, 67,454.81/Minstr) in 5,943,417 blocks (100%, 295.82/Minstr), avg size 228.03 bytes, avg lifetime 3,134,692,250.67 instrs (15.6% of program duration)
+  At t-gmax: 423,930,307 bytes (100%) in 1,575,682 blocks (100%), avg size 269.05 bytes
+  At t-end:  258,002 bytes (100%) in 2,129 blocks (100%), avg size 121.18 bytes
+  Reads:     5,478,606,988 bytes (100%, 272,685.7/Minstr), 4.04/byte
+  Writes:    2,040,294,800 bytes (100%, 101,551.22/Minstr), 1.51/byte
+  Allocated at {
+    #0: [root]
+  }
+}
+]]></programlisting>
+
+<para>The root node covers the entire execution. The information is a superset
+of the information shown when DHAT ran, adding details such as allocation
+rates, average block sizes, block lifetimes, and read and write ratios. The
+next example will explain these in more detail.</para>
+
+</sect3>
+
+
+<sect3><title>Interior Nodes</title>
+
+<para>AP nodes further down the tree show information about a subset of
+allocations. For example:</para>
+
+<programlisting><![CDATA[
+AP 1.1/25 (2 children) {
+  Total:     54,533,440 bytes (4.02%, 2,714.28/Minstr) in 458,839 blocks (7.72%, 22.84/Minstr), avg size 118.85 bytes, avg lifetime 1,127,259,403.64 instrs (5.61% of program duration)
+  At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  Reads:     15,993,012 bytes (0.29%, 796.02/Minstr), 0.29/byte
+  Writes:    20,974,752 bytes (1.03%, 1,043.97/Minstr), 0.38/byte
+  Allocated at {
+    #1: 0x95CACC9: alloc (alloc.rs:72)
+    #2: 0x95CACC9: alloc (alloc.rs:148)
+    #3: 0x95CACC9: reserve_internal<syntax::tokenstream::TokenStream,alloc::alloc::Global> (raw_vec.rs:669)
+    #4: 0x95CACC9: reserve<syntax::tokenstream::TokenStream,alloc::alloc::Global> (raw_vec.rs:492)
+    #5: 0x95CACC9: reserve<syntax::tokenstream::TokenStream> (vec.rs:460)
+    #6: 0x95CACC9: push<syntax::tokenstream::TokenStream> (vec.rs:989)
+    #7: 0x95CACC9: parse_token_trees_until_close_delim (tokentrees.rs:27)
+    #8: 0x95CACC9: syntax::parse::lexer::tokentrees::<impl syntax::parse::lexer::StringReader<'a>>::parse_token_tree (tokentrees.rs:81)
+  }
+}
+]]></programlisting>
+
+<para>The first line indicates the node's position in the tree. The
+<computeroutput>1.1</computeroutput> is a unique identifier for the node and
+also says that it is the first child node <computeroutput>1</computeroutput>
+(which is the root). The <computeroutput>/25</computeroutput> says that it is
+one of 25 children, i.e. it has 24 siblings. The <computeroutput>(2
+children)</computeroutput> says that this node node has two children of its
+own.</para>
+
+<para>Allocations are aggregated by their allocation stack trace. The
+<computeroutput>Allocated at</computeroutput> section shows the allocation
+stack trace that is shared by all the blocks covered by this node.</para>
+
+<para>The <computeroutput>Total</computeroutput> line shows that this node
+accounts for 4.02% of all bytes allocated during execution, and 7.72% of all
+blocks. These percentages are useful for comparing the significance of
+different nodes within a single profile; an AP that accounts for 10% of bytes
+allocated is likely to be more interesting than one that accounts for
+2%.</para>
+
+<para>The <computeroutput>Total</computeroutput> line also shows allocation
+rates, measured in bytes and blocks per million instructions. These rates are
+useful for comparing the significance of nodes across profiles made with
+different workloads.</para>
+
+<para>Finally, the <computeroutput>Total</computeroutput> line shows the
+average size and lifetimes of these blocks.</para>
+
+<para>The <computeroutput>At t-gmax</computeroutput> line says shows that no
+blocks from this AP were alive when the global heap peak occurred. In other
+words, these blocks do not contribute at all to the global heap peak.</para>
+
+<para>The <computeroutput>At t-end</computeroutput> line shows that no blocks
+were from this AP were alive at shutdown. In other words, all those blocks were
+explicitly freed before termination.</para>
+
+<para>The <computeroutput>Reads</computeroutput> and
+<computeroutput>Writes</computeroutput> lines show how many bytes were read 
+within this AP's blocks, the fraction this represents of all heap reads, and
+the read rate. Finally, it shows the read ratio, which is the number of reads
+per byte. In this case the number is 0.29, which is quite low -- if no byte was
+read twice, then only 29% of the allocated bytes, which means that at least 71%
+of the bytes were never read! This suggests that the blocks are being
+underutilized and might be worth optimizing.</para>
+
+<para>The <computeroutput>Writes</computeroutput> lines is similar to the
+<computeroutput>Reads</computeroutput> line. In this case, at most 38% of the
+bytes are ever written, and at least 62% of the bytes were never written.
+</para>
+
+<para>The <computeroutput>Reads</computeroutput> and
+<computeroutput>Writes</computeroutput> measurements suggest that the blocks
+are being under-utilised and might be worth optimizing. Having said that, this
+kind of under-utilisation is common in data structures that grow, such as
+vectors and hash tables, and isn't always fixable. </para>
+
+</sect3>
+
+
+<sect3><title>Leaf Nodes</title>
+
+<para>This is a leaf node:</para>
+
+<programlisting><![CDATA[
+AP 1.1.1.1/2 {
+  Total:     31,460,928 bytes (2.32%, 1,565.9/Minstr) in 262,171 blocks (4.41%, 13.05/Minstr), avg size 120 bytes, avg lifetime 986,406,885.05 instrs (4.91% of program duration)
+  Max:       16,779,136 bytes in 65,543 blocks, avg size 256 bytes
+  At t-gmax: 0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+  Reads:     5,964,704 bytes (0.11%, 296.88/Minstr), 0.19/byte
+  Writes:    10,487,200 bytes (0.51%, 521.98/Minstr), 0.33/byte
+  Allocated at {
+    ^1: 0x95CACC9: alloc (alloc.rs:72)
+    ^2: 0x95CACC9: alloc (alloc.rs:148)
+    ^3: 0x95CACC9: reserve_internal<syntax::tokenstream::TokenStream,alloc::alloc::Global> (raw_vec.rs:669)
+    ^4: 0x95CACC9: reserve<syntax::tokenstream::TokenStream,alloc::alloc::Global> (raw_vec.rs:492)
+    ^5: 0x95CACC9: reserve<syntax::tokenstream::TokenStream> (vec.rs:460)
+    ^6: 0x95CACC9: push<syntax::tokenstream::TokenStream> (vec.rs:989)
+    ^7: 0x95CACC9: parse_token_trees_until_close_delim (tokentrees.rs:27)
+    ^8: 0x95CACC9: syntax::parse::lexer::tokentrees::<impl syntax::parse::lexer::StringReader<'a>>::parse_token_tree (tokentrees.rs:81)
+    ^9: 0x95CAC39: parse_token_trees_until_close_delim (tokentrees.rs:26)
+    ^10: 0x95CAC39: syntax::parse::lexer::tokentrees::<impl syntax::parse::lexer::StringReader<'a>>::parse_token_tree (tokentrees.rs:81)
+    #11: 0x95CAC39: parse_token_trees_until_close_delim (tokentrees.rs:26)
+    #12: 0x95CAC39: syntax::parse::lexer::tokentrees::<impl syntax::parse::lexer::StringReader<'a>>::parse_token_tree (tokentrees.rs:81)
+  }
+}
+]]></programlisting>
+
+<para>The <computeroutput>1.1.1.1/2</computeroutput> indicates that this node
+is a great-grandchild of the root; is the first grandchild of the node in the
+previous example; and has no children.</para>
+
+<para>Leaf nodes contain an additional <computeroutput>Max</computeroutput>
+line, indicating the peak memory use for the blocks covered by this AP. (This
+peak may have occurred at a time other than
+<computeroutput>t-gmax</computeroutput>.) In this case, 31,460,298 bytes were
+allocated from this AP, but the maximum size alive at once was 16,779,136
+bytes.</para>
+
+<para>Stack frames that begin with a <computeroutput>^</computeroutput> rather
+than a <computeroutput>#</computeroutput> are copied from ancestor nodes.
+(In this example, the first 8 frames are identical to those from the node in
+the previous example.) These frames could be found by tracing back through
+ancestor nodes, but that can be annoying, which is why they are duplicated.
+This also means that each node makes complete sense on its own.</para>
+
+</sect3>
+
+
+<sect3><title>Access Counts</title>
+
+<para>If all blocks covered by an AP node have the same size, an additional
+<computeroutput>Accesses</computeroutput> field will be present. It indicates
+how the reads and writes within these blocks were distributed. For
+example:</para>
+
+<programlisting><![CDATA[
+Total:     8,388,672 bytes (0.62%, 417.53/Minstr) in 262,146 blocks (4.41%, 13.05/Minstr), avg size 32 bytes, avg lifetime 16,726,078,401.51 instrs (83.25% of program duration)
+At t-gmax: 8,388,672 bytes (1.98%) in 262,146 blocks (16.64%), avg size 32 bytes
+At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+Reads:     9,109,682 bytes (0.17%, 453.41/Minstr), 1.09/byte
+Writes:    7,340,088 bytes (0.36%, 365.34/Minstr), 0.88/byte
+Accesses: {
+  [  0]  65547 7 8 4 65529 ã€ƒ ã€ƒ ã€ƒ 16 ã€ƒ ã€ƒ ã€ƒ 12 ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ ã€ƒ 65542 ã€ƒ ã€ƒ ã€ƒ - - - - 
+}
+]]></programlisting>
+
+<para>Every block covered by this AP was 32 bytes. Within all of those blocks,
+byte 0 was accessed (read or written) 65,547 times, byte 1 was accessed 7
+times, byte 2 was accessed 8 times, and so on.</para>
+
+<para>The ditto symbol (<computeroutput>〃</computeroutput>) means "same access
+count as the previous byte".</para>
+
+<para>A dash (<computeroutput>-</computeroutput>) means "zero". (It is used
+instead of <computeroutput>0</computeroutput> because it makes unaccessed
+regions more easily identifiable.)</para>
+
+<para>The infinity symbol (<computeroutput>∞</computeroutput>, not present in
+this example) means "exceeded the maximum tracked count".</para>
+
+<para>Block layout can often be inferred from counts. For example, these blocks
+probably have four separate byte-sized fields, followed by a four-byte field,
+and so on.</para>
+
+<para>Access counts can be useful for identifying data alignment holes or other
+layout inefficiencies.</para>
+
+</sect3>
+
+
+<sect3><title>Aggregate Nodes</title>
+
+<para>The AP tree is very large and many nodes represent tiny numbers of blocks
+and bytes. Therefore, DHAT's viewer aggregates insignificant nodes like
+this:</para>
+
+<programlisting><![CDATA[
+AP 1.14.2/2 {
+  Total:     5,175 blocks (0.09%, 0.26/Minstr)
+  Allocated at {
+    [5 insignificant]
+  }
+}
+]]></programlisting>
+
+<para>Much of the detail is stripped away, leaving only basic measurements,
+along with an indication of how many nodes were aggregated together (5 in this
+case).</para>
+
+</sect3>
+
+</sect2>
+
+
+<sect2><title>The Output Footer</title>
+
+<para>Below the AP tree is a line like this:</para>
+
+<programlisting><![CDATA[
+AP significance threshold: total >= 59,434.17 blocks (1%)
+]]></programlisting>
+
+<para>It shows the function used to determine if an AP node is significant. All
+nodes that don't satisfy this function are aggregated. It is occasionally
+useful if you don't understand why an AP node has been aggregated. The exact
+threshold depends on the sort metric (see below).</para>
+
+<para>Finally, the bottom of the page shows a legend that explains some of the
+terms, abbreviations and symbols used in the output.</para>
+
+</sect2>
+
+
+<sect2><title>Sort Metrics</title>
+
+<para>The order in which sub-trees are sorted can be changed via the "Sort
+metric" drop-down menu at the top of DHAT's viewer. Different sort metrics can
+be useful for finding different things. Some sort metrics also incorporate some
+filtering, so that only nodes meeting a particular criteria are shown.</para>
+
+<!-- start of xi:include in the manpage -->
+<variablelist>
+
+  <varlistentry>
+    <term>Total (bytes)</term>
+    <listitem><para>The total number of bytes allocated during the execution.
+    Highly useful for evaluating heap churn, though not quite as useful as
+    "Total (blocks)".
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Total (blocks)</term>
+    <listitem><para>The total number of blocks allocated during the execution.
+    Highly useful for evaluating heap churn; reducing the number of calls to
+    the allocator can significantly speed up a program. This is the default
+    sort metric.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Total (blocks), tiny</term>
+    <listitem><para>Like "Total (blocks)", but shows only very small blocks.
+    Moderately useful, because such blocks are often easy to avoid allocating.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Total (blocks), short-lived</term>
+    <listitem><para>Like "Total (blocks)", but shows only very short-lived
+    blocks. Moderately useful, because such blocks are often easy to avoid
+    allocating.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Total (bytes), zero reads or zero writes</term>
+    <listitem><para>Like "Total (bytes)", but shows only blocks that are 
+    never read or never written to (or both). Highly useful, because such
+    blocks indicate poor use of memory and are often easy to avoid allocating.
+    For example, sometimes a block is allocated and written to but then only
+    read if a condition C is true; in that case, it may be possible to delay
+    creating the block until condition C is true. Alternatively, sometimes
+    blocks are created and never used; such blocks are trivial to remove.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Total (blocks), zero reads or zero writes</term>
+    <listitem><para>Like "Total (bytes), zero reads or zero writes" but for
+    blocks. Highly useful.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Total (bytes), low-access</term>
+    <listitem><para>Like "Total (bytes)", but shows only blocks that have low
+    numbers of reads or low numbers of writes (or both). Moderately useful,
+    because such blocks indicate poor use of memory.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Total (blocks), low-access</term>
+    <listitem><para>Like "Total (bytes), low-access", but for blocks.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>At t-gmax (bytes)</term>
+    <listitem><para>This shows the breakdown of memory at the point of peak
+    heap memory usage. Highly useful for reducing peak memory usage.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>At t-end (bytes)</term>
+    <listitem><para>This shows the breakdown of memory at program termination.
+    Highly useful for identifying process-lifetime leaks.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Reads (bytes)</term>
+    <listitem><para>The number of bytes read within heap blocks. Occasionally
+    useful.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Reads (bytes), high-access</term>
+    <listitem><para>Like "Reads (bytes)", but only shows blocks with high read
+    ratios. Occasionally useful for identifying hot areas of memory.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Writes (bytes)</term>
+    <listitem><para>Like "Reads (bytes)", but for writes. Occasionally useful.
+    </para></listitem>
+  </varlistentry>
+
+  <varlistentry>
+    <term>Writes (bytes), high-access</term>
+    <listitem><para>Like "Reads (bytes), high-access", but for writes.
+    Occasionally useful.
+    </para></listitem>
+  </varlistentry>
+
+</variablelist>
+
+<para>The values within a node that represent the chosen sort metric are shown
+in bold, so they stand out.</para>
+
+<para>Here is part of an AP node found with "Total (blocks), tiny", showing
+blocks with an average size of only 8.67 bytes:</para>
+
+<programlisting><![CDATA[
+Total:     3,407,848 bytes (0.25%, 169.62/Minstr) in 393,214 blocks (6.62%, 19.57/Minstr), avg size 8.67 bytes, avg lifetime 1,167,795,629.1 instrs (5.81% of program duration)
+]]></programlisting>
+
+<para>Here is part of an AP node found with "Total (blocks), short-lived",
+showing blocks with an average lifetime of only 181.75 instructions:</para>
+
+<programlisting><![CDATA[
+Total:     23,068,584 bytes (1.7%, 1,148.19/Minstr) in 262,143 blocks (4.41%, 13.05/Minstr), avg size 88 bytes, avg lifetime 181.75 instrs (0% of program duration)
+]]></programlisting>
+
+<para>Here is an example of an AP identified with "Total (blocks), zero reads
+or zero writes", showing blocks that are allocated but never touched:</para>
+
+<programlisting><![CDATA[
+Total:     7,339,920 bytes (0.54%, 365.33/Minstr) in 262,140 blocks (4.41%, 13.05/Minstr), avg size 28 bytes, avg lifetime 1,141,103,997.69 instrs (5.68% of program duration)
+Max:       3,669,960 bytes in 131,070 blocks, avg size 28 bytes
+At t-gmax: 3,336,400 bytes (0.79%) in 119,157 blocks (7.56%), avg size 28 bytes
+At t-end:  0 bytes (0%) in 0 blocks (0%), avg size 0 bytes
+Reads:     0 bytes (0%, 0/Minstr), 0/byte
+Writes:    0 bytes (0%, 0/Minstr), 0/byte
+]]></programlisting>
+
+<para>All the blocks identified by these APs are good candidates for
+optimization.</para>
+
+</sect2>
+
+</sect1>
+
+
+
+<sect1 id="dh-manual.options" xreflabel="DHAT Command-line Options">
+<title>DHAT Command-line Options</title>
+
+<para>DHAT-specific command-line options are:</para>
+
+<!-- start of xi:include in the manpage -->
+<variablelist id="dh.opts.list">
+
+  <varlistentry id="opt.dhat-out-file" xreflabel="--dhat-out-file">
+    <term>
+      <option><![CDATA[--dhat-out-file=<file> ]]></option>
+    </term>
+    <listitem>
+      <para>Write the profile data to 
+            <computeroutput>file</computeroutput> rather than to the default
+            output file,
+            <filename>dhat.out.&lt;pid&gt;</filename>. The
+            <option>%p</option> and <option>%q</option> format specifiers
+            can be used to embed the process ID and/or the contents of an
+            environment variable in the name, as is the case for the core
+            option <option><xref linkend="opt.log-file"/></option>.
+      </para>
+    </listitem>
+  </varlistentry>
+
+</variablelist>
+
+<para>Note that stacks by default have 12 frames. This may be more than
+necessary, in which case the <option>--num-callers</option> flag can be used to
+reduce the number, which may make DHAT run slightly faster.
+</para>
+
+<!-- end of xi:include in the manpage -->
+
+</sect1>
+
+</chapter>
diff --git a/dhat/tests/Makefile.am b/dhat/tests/Makefile.am
new file mode 100644 (file)
index 0000000..d1ad7e7
--- /dev/null
@@ -0,0 +1,23 @@
+
+include $(top_srcdir)/Makefile.tool-tests.am
+
+dist_noinst_SCRIPTS = filter_stderr
+
+EXTRA_DIST = \
+       acc.post.exp acc.stderr.exp acc.vgtest \
+       basic.post.exp basic.stderr.exp basic.vgtest \
+       big.post.exp big.stderr.exp big.vgtest \
+       empty.post.exp empty.stderr.exp empty.vgtest \
+       single.post.exp single.stderr.exp single.vgtest
+
+check_PROGRAMS = \
+       acc \
+       basic \
+       big \
+       empty \
+       sig \
+       single
+
+AM_CFLAGS   += $(AM_FLAG_M3264_PRI)
+AM_CXXFLAGS += $(AM_FLAG_M3264_PRI)
+
diff --git a/dhat/tests/acc.c b/dhat/tests/acc.c
new file mode 100644 (file)
index 0000000..ef6693e
--- /dev/null
@@ -0,0 +1,74 @@
+// Testing accesses of blocks.
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+void* m1(size_t n) { return malloc(n); }
+
+void* m2(size_t n) { return malloc(n); }
+
+int main(void)
+{
+   // 0th char is written 0 times, 1st char is written once, etc.
+   char* a = malloc(32);
+   for (int i = 1; i < 32; i++) {
+      for (int j = 0; j < i; j++) {
+         a[i] = 0;
+      }
+   }
+   free(a);
+
+   // Repetition and gaps.
+   int* b = malloc(20);
+   b[0] = 1;
+   b[2] = b[0];
+   for (int i = 0; i < 10; i++) {
+      b[4] = 99;
+   }
+   free(b);
+
+   // 33 bytes, goes onto a second line in dh_view.
+   char* c = calloc(33, 1);
+   c[32] = 0;
+   free(c);
+
+   // 1024 bytes, accesses are shown.
+   char* d = malloc(1024);
+   for (int i = 0; i < 1024; i++) {
+      d[i] = d[1023 - i];
+   }
+   for (int i = 500; i < 600; i++) {
+      d[i] = 0;
+   }
+   free(d);
+
+   // 1025 bytes, accesses aren't shown.
+   char* e = calloc(1025, 1);
+   for (int i = 0; i < 1025; i++) {
+      e[i] += 1;
+   }
+   free(e);
+
+   // Lots of accesses, but fewer than the 0xffff max value.
+   int* f1 = m1(100);
+   int* f2 = m1(100);
+   for (int i = 0; i < 50000; i++) {
+      f1[0] = 0;
+      f2[0] = 0;
+   }
+   free(f1);
+   free(f2);
+
+   // Lots of accesses, more than the 0xffff max value: treated as Infinity.
+   int* g1 = m2(100);
+   int* g2 = m2(100);
+   for (int i = 0; i < 100000; i++) {
+      g1[0] = 0;
+      g2[0] = 0;
+   }
+   free(g1);
+   free(g2);
+
+   return 0;
+}
diff --git a/dhat/tests/acc.stderr.exp b/dhat/tests/acc.stderr.exp
new file mode 100644 (file)
index 0000000..7732270
--- /dev/null
@@ -0,0 +1,7 @@
+
+
+Total:     2,534 bytes in 9 blocks
+At t-gmax: 1,025 bytes in 1 blocks
+At t-end:  0 bytes in 0 blocks
+Reads:     2,053 bytes
+Writes:    1,202,694 bytes
diff --git a/dhat/tests/acc.vgtest b/dhat/tests/acc.vgtest
new file mode 100644 (file)
index 0000000..487011e
--- /dev/null
@@ -0,0 +1,3 @@
+prog: acc
+vgopts: --dhat-out-file=dhat.out
+cleanup: rm dhat.out
diff --git a/dhat/tests/basic.c b/dhat/tests/basic.c
new file mode 100644 (file)
index 0000000..3ac6617
--- /dev/null
@@ -0,0 +1,26 @@
+// Some basic allocations and accesses.
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+int main(void)
+{
+   int64_t* m = malloc(1000);
+   m[0] = 1;                     // write 8 bytes
+   m[10] = m[1];                 // read and write 8 bytes
+
+   char* c = calloc(1, 2000);
+   for (int i = 0; i < 1000; i++) {
+      c[i + 1000] = c[i];        // read and write 1000 bytes
+   }
+
+   char* r = realloc(m, 3000);
+   for (int i = 0; i < 500; i++) {
+      r[i + 2000] = 99;          // write 500 bytes
+   }
+                                 // totals: 1008 read, 1516 write
+   free(c);
+
+   return 0;
+}
diff --git a/dhat/tests/basic.stderr.exp b/dhat/tests/basic.stderr.exp
new file mode 100644 (file)
index 0000000..bcdec65
--- /dev/null
@@ -0,0 +1,7 @@
+
+
+Total:     6,000 bytes in 3 blocks
+At t-gmax: 5,000 bytes in 2 blocks
+At t-end:  3,000 bytes in 1 blocks
+Reads:     1,008 bytes
+Writes:    1,516 bytes
diff --git a/dhat/tests/basic.vgtest b/dhat/tests/basic.vgtest
new file mode 100644 (file)
index 0000000..db1515f
--- /dev/null
@@ -0,0 +1,3 @@
+prog: basic
+vgopts: --dhat-out-file=dhat.out
+cleanup: rm dhat.out
diff --git a/dhat/tests/big.c b/dhat/tests/big.c
new file mode 100644 (file)
index 0000000..12dd677
--- /dev/null
@@ -0,0 +1,61 @@
+// This test implements a moderately complex call tree. The layout of these
+// functions matches the layout of the tree produced by dh_view.js, when
+// sorted by "total bytes".
+
+#include <stdlib.h>
+
+#define F(f, parent)    void* f(size_t n) { return parent(n); }
+
+// Note: don't use j1 -- that is a builtin C function, believe it or not.
+F(a, malloc)
+   F(b1, a)
+      F(c1, b1)
+         F(d1, c1)
+         F(d2, c1)   // insig total-bytes
+      F(c2, b1)
+   F(b2, a)
+   F(b3, a) F(e, b3) F(f, e)
+F(g, malloc) F(h, g) F(i, h)
+   F(j2, i) F(k, j2) F(l, k)
+   F(j3, i) F(m, j3)
+      F(n1, m)
+      F(n2, m) F(o, n2)
+   F(p, i) F(q, p)   // insig total-bytes
+   F(r, i)           // insig total-bytes
+F(s1, malloc) F(s2, s1) F(s3, s2) F(s4, s3) F(s5, s4)
+F(t, malloc)
+F(u, malloc)
+F(v, malloc)
+   F(w, v)           // insig total-bytes
+   F(x, v)           // insig total-bytes
+   F(y, v)           // insig total-bytes
+   F(z, v)           // insig total-bytes
+
+int main(void)
+{
+   // Call all the leaves in the above tree.
+
+   int* d1p = d1(706);
+   free(d1p); // So the t-final numbers differ from the t-gmax/total numbers.
+
+   d2(5);
+   c2(30);
+   b2(20);
+   f(10);
+   l(60);
+   n1(30);
+   o(20);
+   q(7);
+   r(3);
+   s5(30);
+   t(20);
+   u(19);
+   w(9);
+   x(8);
+   y(7);
+   z(5);
+   z(1);
+
+   // And one allocation directly from main().
+   malloc(10);
+}
diff --git a/dhat/tests/big.stderr.exp b/dhat/tests/big.stderr.exp
new file mode 100644 (file)
index 0000000..5733976
--- /dev/null
@@ -0,0 +1,7 @@
+
+
+Total:     1,000 bytes in 19 blocks
+At t-gmax: 706 bytes in 1 blocks
+At t-end:  294 bytes in 18 blocks
+Reads:     0 bytes
+Writes:    0 bytes
diff --git a/dhat/tests/big.vgtest b/dhat/tests/big.vgtest
new file mode 100644 (file)
index 0000000..2d8fb24
--- /dev/null
@@ -0,0 +1,3 @@
+prog: big
+vgopts: --dhat-out-file=dhat.out
+cleanup: rm dhat.out
diff --git a/dhat/tests/empty.c b/dhat/tests/empty.c
new file mode 100644 (file)
index 0000000..1f59d5f
--- /dev/null
@@ -0,0 +1,6 @@
+// No allocations.
+
+int main(void)
+{
+   return 0;
+}
diff --git a/dhat/tests/empty.stderr.exp b/dhat/tests/empty.stderr.exp
new file mode 100644 (file)
index 0000000..6b189fd
--- /dev/null
@@ -0,0 +1,7 @@
+
+
+Total:     0 bytes in 0 blocks
+At t-gmax: 0 bytes in 0 blocks
+At t-end:  0 bytes in 0 blocks
+Reads:     0 bytes
+Writes:    0 bytes
diff --git a/dhat/tests/empty.vgtest b/dhat/tests/empty.vgtest
new file mode 100644 (file)
index 0000000..9934963
--- /dev/null
@@ -0,0 +1,3 @@
+prog: empty
+vgopts: --dhat-out-file=dhat.out
+cleanup: rm dhat.out
diff --git a/dhat/tests/filter_stderr b/dhat/tests/filter_stderr
new file mode 100755 (executable)
index 0000000..6b4058b
--- /dev/null
@@ -0,0 +1,9 @@
+#! /bin/sh
+
+dir=`dirname $0`
+
+$dir/../../tests/filter_stderr_basic |
+
+# Remove "Massif, ..." line and the following copyright line.
+sed "/^DHAT, a dynamic heap analysis tool/ , /./ d"
+
diff --git a/dhat/tests/sig.c b/dhat/tests/sig.c
new file mode 100644 (file)
index 0000000..64b0dbd
--- /dev/null
@@ -0,0 +1,76 @@
+// This test implements sorting of a tree involving a mix of significant and
+// insignificant nodes. The layout of these functions matches the layout of
+// the tree produced by dh_view.js, when sorted by "total bytes".
+
+#include <stdlib.h>
+
+#define F(f, parent)    void* f(size_t n) { return parent(n); }
+
+F(am, malloc)
+   // main
+   F(a2, am) // main
+   F(a3, am)
+      // main
+      // main
+F(bm, malloc)
+   // main
+   F(b2, bm) // main
+   F(b3, bm)
+      // main
+      // main
+F(cm, malloc)
+   // main
+   F(c2, cm) // main
+   F(c3, cm)
+      // main
+      // main
+F(dm, malloc)
+   // main
+   F(d2, dm) // main
+   F(d3, dm)
+      // main
+      // main
+
+char access(char* p, size_t n)
+{
+   for (int i = 0; i < 1499; i++) {
+      for (int j = 0; j < n; j++) {
+         p[j] = j;
+      }
+   }
+   char x = 0;
+   for (int j = 0; j < n; j++) {
+      x += p[j];
+   }
+   return x;
+}
+
+int main(void)
+{
+
+   char* p;
+
+   // Call all the leaves in the above tree. The pointers we pass to access()
+   // become significant in a high-access sort and insignificant in a
+   // zero-reads-or-zero-writes sort, and vice versa.
+
+   p = am(11); access(p, 11);
+   p = a2(10); access(p, 10);
+   p = a3(5);  access(p, 5);
+   p = a3(4);  access(p, 5);
+
+   p = bm(10); access(p, 10);
+   p = b2(9);  access(p, 9);
+   p = b3(5);
+   p = b3(3);
+
+   p = cm(9); access(p, 9);
+   p = c2(8);
+   p = c3(4);
+   p = c3(3);
+
+   p = dm(8);
+   p = d2(7);
+   p = d3(4);
+   p = d3(2);
+}
diff --git a/dhat/tests/sig.stderr.exp b/dhat/tests/sig.stderr.exp
new file mode 100644 (file)
index 0000000..d5a50cf
--- /dev/null
@@ -0,0 +1,7 @@
+
+
+Total:     102 bytes in 16 blocks
+At t-gmax: 102 bytes in 16 blocks
+At t-end:  102 bytes in 16 blocks
+Reads:     58 bytes
+Writes:    86,942 bytes
diff --git a/dhat/tests/sig.vgtest b/dhat/tests/sig.vgtest
new file mode 100644 (file)
index 0000000..9eebdca
--- /dev/null
@@ -0,0 +1,3 @@
+prog: sig
+vgopts: --dhat-out-file=dhat.out
+cleanup: rm dhat.out
diff --git a/dhat/tests/single.c b/dhat/tests/single.c
new file mode 100644 (file)
index 0000000..bc60827
--- /dev/null
@@ -0,0 +1,11 @@
+// A single allocation (so the root node is the only node in the tree).
+
+#include <stdlib.h>
+
+int main() {
+   int* a = (int*)malloc(16);
+   a[0] = 0;
+   a[0] = 1;
+   a[0] = 2;
+   return 0;
+}
diff --git a/dhat/tests/single.stderr.exp b/dhat/tests/single.stderr.exp
new file mode 100644 (file)
index 0000000..87e8185
--- /dev/null
@@ -0,0 +1,7 @@
+
+
+Total:     16 bytes in 1 blocks
+At t-gmax: 16 bytes in 1 blocks
+At t-end:  16 bytes in 1 blocks
+Reads:     0 bytes
+Writes:    12 bytes
diff --git a/dhat/tests/single.vgtest b/dhat/tests/single.vgtest
new file mode 100644 (file)
index 0000000..3c52cca
--- /dev/null
@@ -0,0 +1,3 @@
+prog: single
+vgopts: --dhat-out-file=dhat.out
+cleanup: rm dhat.out
index e848b781d3bb23f31f7b225caee95483238aa018..7a681a5df3d0d6710ee666c602fd1460473e3446 100644 (file)
@@ -20,6 +20,7 @@ EXTRA_DIST = \
        images/prev.png \
        images/up.png \
        images/kcachegrind_xtree.png \
+       images/dh-tree.png \
        internals/3_0_BUGSTATUS.txt \
        internals/3_1_BUGSTATUS.txt \
        internals/3_2_BUGSTATUS.txt \
index afb048d4d4a4d89ac575c64e022960250189d5ed..76245b516454e90d23f55b946575f5e34015f1c0 100644 (file)
@@ -76,10 +76,18 @@ could just build the docs from XML when doing 'make install', which
 would be simpler.
 
 
-Notes on building PDF / PS documents
-------------------------------------
-Below are random notes and recollections about how to build PDF / PS
-documents from the XML source at various times on various Linux distros.
+Notes on building HTML / PDF / PS documents
+-------------------------------------------
+Below are random notes and recollections about how to build documents
+from the XML source at various times on various Linux distros. They're
+mostly about the PDF/PS documents, because they are the hardest to
+build.
+
+Notes [Jan 2019]
+-----------------
+For Ubuntu 18.04, to build HTML docs I had to:
+
+  sudo apt-get install xsltproc
 
 Notes [May 2017]
 ----------------
diff --git a/docs/images/dh-tree.png b/docs/images/dh-tree.png
new file mode 100644 (file)
index 0000000..637c7ad
Binary files /dev/null and b/docs/images/dh-tree.png differ
index d2ab7e28d344c97249879c0b8485f1971fa14df0..cb21089591ad80c6f236e2de8d5e2347d5709254 100644 (file)
@@ -613,7 +613,7 @@ in most cases.  We group the available options by rough categories.</para>
     <listitem>
       <para>Run the Valgrind tool called <varname>toolname</varname>,
       e.g. memcheck, cachegrind, callgrind, helgrind, drd, massif,
-      lackey, none, exp-sgcheck, exp-bbv, exp-dhat, etc.</para>
+      dhat, lackey, none, exp-sgcheck, exp-bbv, etc.</para>
     </listitem>
   </varlistentry>
 
@@ -2562,7 +2562,7 @@ need to use them.</para>
       malloc related functions, using the
       synonym <varname>somalloc</varname>.  This synonym is usable for
       all tools doing standard replacement of malloc related functions
-      (e.g. memcheck, massif, drd, helgrind, exp-dhat, exp-sgcheck).
+      (e.g. memcheck, helgrind, drd, massif, dhat, exp-sgcheck).
       </para>
 
       <itemizedlist>
index 333e5b77866c70d354f8b84853059dc359917426..b90514935a85ca77b58903babbfe3ff9135d40a5 100644 (file)
       xmlns:xi="http://www.w3.org/2001/XInclude" />
   <xi:include href="../../massif/docs/ms-manual.xml" parse="xml"  
       xmlns:xi="http://www.w3.org/2001/XInclude" />
-  <xi:include href="../../exp-dhat/docs/dh-manual.xml" parse="xml"  
+  <xi:include href="../../dhat/docs/dh-manual.xml" parse="xml"  
       xmlns:xi="http://www.w3.org/2001/XInclude" />
-  <xi:include href="../../exp-sgcheck/docs/sg-manual.xml" parse="xml"  
-      xmlns:xi="http://www.w3.org/2001/XInclude" />
-  <xi:include href="../../exp-bbv/docs/bbv-manual.xml" parse="xml"  
-      xmlns:xi="http://www.w3.org/2001/XInclude" />      
   <xi:include href="../../lackey/docs/lk-manual.xml" parse="xml"  
       xmlns:xi="http://www.w3.org/2001/XInclude" />
   <xi:include href="../../none/docs/nl-manual.xml" parse="xml"  
       xmlns:xi="http://www.w3.org/2001/XInclude" />
+  <xi:include href="../../exp-sgcheck/docs/sg-manual.xml" parse="xml"  
+      xmlns:xi="http://www.w3.org/2001/XInclude" />
+  <xi:include href="../../exp-bbv/docs/bbv-manual.xml" parse="xml"  
+      xmlns:xi="http://www.w3.org/2001/XInclude" />      
 
 </book>
diff --git a/exp-dhat/docs/dh-manual.xml b/exp-dhat/docs/dh-manual.xml
deleted file mode 100644 (file)
index 66b7c68..0000000
+++ /dev/null
@@ -1,401 +0,0 @@
-<?xml version="1.0"?> <!-- -*- sgml -*- -->
-<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
-          "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
-[ <!ENTITY % vg-entities SYSTEM "../../docs/xml/vg-entities.xml"> %vg-entities; ]>
-
-
-<chapter id="dh-manual" 
-         xreflabel="DHAT: a dynamic heap analysis tool">
-  <title>DHAT: a dynamic heap analysis tool</title>
-
-<para>To use this tool, you must specify
-<option>--tool=exp-dhat</option> on the Valgrind
-command line.</para>
-
-
-
-<sect1 id="dh-manual.overview" xreflabel="Overview">
-<title>Overview</title>
-
-<para>DHAT is a tool for examining how programs use their heap
-allocations.</para>
-
-<para>It tracks the allocated blocks, and inspects every memory access
-to find which block, if any, it is to.  The following data is
-collected and presented per allocation point (allocation
-stack):</para>
-
-<itemizedlist>
-  <listitem><para>Total allocation (number of bytes and
-  blocks)</para></listitem>
-
-  <listitem><para>maximum live volume (number of bytes and
-  blocks)</para></listitem>
-
-  <listitem><para>average block lifetime (number of instructions
-   between allocation and freeing)</para></listitem>
-
-  <listitem><para>average number of reads and writes to each byte in
-   the block ("access ratios")</para></listitem>
-
-  <listitem><para>for allocation points which always allocate blocks
-   only of one size, and that size is 4096 bytes or less: counts
-   showing how often each byte offset inside the block is
-   accessed.</para></listitem>
-</itemizedlist>
-
-<para>Using these statistics it is possible to identify allocation
-points with the following characteristics:</para>
-
-<itemizedlist>
-
-  <listitem><para>potential process-lifetime leaks: blocks allocated
-   by the point just accumulate, and are freed only at the end of the
-   run.</para></listitem>
-
- <listitem><para>excessive turnover: points which chew through a lot
-  of heap, even if it is not held onto for very long</para></listitem>
-
- <listitem><para>excessively transient: points which allocate very
- short lived blocks</para></listitem>
-
- <listitem><para>useless or underused allocations: blocks which are
-  allocated but not completely filled in, or are filled in but not
-  subsequently read.</para></listitem>
-
- <listitem><para>blocks with inefficient layout -- areas never
-  accessed, or with hot fields scattered throughout the
-  block.</para></listitem>
-</itemizedlist>
-
-<para>As with the Massif heap profiler, DHAT measures program progress
-by counting instructions, and so presents all age/time related figures
-as instruction counts.  This sounds a little odd at first, but it
-makes runs repeatable in a way which is not possible if CPU time is
-used.</para>
-
-</sect1>
-
-
-
-
-<sect1 id="dh-manual.understanding" xreflabel="Understanding DHAT's output">
-<title>Understanding DHAT's output</title>
-
-
-<para>DHAT provides a lot of useful information on dynamic heap usage.
-Most of the art of using it is in interpretation of the resulting
-numbers.  That is best illustrated via a set of examples.</para>
-
-
-<sect2>
-<title>Interpreting the max-live, tot-alloc and deaths fields</title>
-
-<sect3><title>A simple example</title></sect3>
-
-<screen><![CDATA[
-   ======== SUMMARY STATISTICS ========
-
-   guest_insns:  1,045,339,534
-   [...]
-   max-live:    63,490 in 984 blocks
-   tot-alloc:   1,904,700 in 29,520 blocks (avg size 64.52)
-   deaths:      29,520, at avg age 22,227,424
-   acc-ratios:  6.37 rd, 1.14 wr  (12,141,526 b-read, 2,174,460 b-written)
-      at 0x4C275B8: malloc (vg_replace_malloc.c:236)
-      by 0x40350E: tcc_malloc (tinycc.c:6712)
-      by 0x404580: tok_alloc_new (tinycc.c:7151)
-      by 0x40870A: next_nomacro1 (tinycc.c:9305)
-]]></screen>
-
-<para>Over the entire run of the program, this stack (allocation
-point) allocated 29,520 blocks in total, containing 1,904,700 bytes in
-total.  By looking at the max-live data, we see that not many blocks
-were simultaneously live, though: at the peak, there were 63,490
-allocated bytes in 984 blocks.  This tells us that the program is
-steadily freeing such blocks as it runs, rather than hanging on to all
-of them until the end and freeing them all.</para>
-
-<para>The deaths entry tells us that 29,520 blocks allocated by this stack
-died (were freed) during the run of the program.  Since 29,520 is
-also the number of blocks allocated in total, that tells us that
-all allocated blocks were freed by the end of the program.</para>
-
-<para>It also tells us that the average age at death was 22,227,424
-instructions.  From the summary statistics we see that the program ran
-for 1,045,339,534 instructions, and so the average age at death is
-about 2% of the program's total run time.</para>
-
-<sect3><title>Example of a potential process-lifetime leak</title></sect3>
-
-<para>This next example (from a different program than the above)
-shows a potential process lifetime leak.  A process lifetime leak
-occurs when a program keeps allocating data, but only frees the
-data just before it exits.  Hence the program's heap grows constantly
-in size, yet Memcheck reports no leak, because the program has
-freed up everything at exit.  This is particularly a hazard for
-long running programs.</para>
-
-<screen><![CDATA[
-   ======== SUMMARY STATISTICS ========
-   
-   guest_insns:  418,901,537
-   [...]
-   max-live:    32,512 in 254 blocks
-   tot-alloc:   32,512 in 254 blocks (avg size 128.00)
-   deaths:      254, at avg age 300,467,389
-   acc-ratios:  0.26 rd, 0.20 wr  (8,756 b-read, 6,604 b-written)
-      at 0x4C275B8: malloc (vg_replace_malloc.c:236)
-      by 0x4C27632: realloc (vg_replace_malloc.c:525)
-      by 0x56FF41D: QtFontStyle::pixelSize(unsigned short, bool) (qfontdatabase.cpp:269)
-      by 0x5700D69: loadFontConfig() (qfontdatabase_x11.cpp:1146)
-]]></screen>
-
-<para>There are two tell-tale signs that this might be a
-process-lifetime leak.  Firstly, the max-live and tot-alloc numbers
-are identical.  The only way that can happen is if these blocks are
-all allocated and then all deallocated.</para>
-
-<para>Secondly, the average age at death (300 million insns) is 71% of
-the total program lifetime (419 million insns), hence this is not a
-transient allocation-free spike -- rather, it is spread out over a
-large part of the entire run.  One interpretation is, roughly, that
-all 254 blocks were allocated in the first half of the run, held onto
-for the second half, and then freed just before exit.</para>
-
-</sect2>
-
-
-<sect2>
-<title>Interpreting the acc-ratios fields</title>
-
-
-<sect3><title>A fairly harmless allocation point record</title></sect3>
-
-<screen><![CDATA[
-   max-live:    49,398 in 808 blocks
-   tot-alloc:   1,481,940 in 24,240 blocks (avg size 61.13)
-   deaths:      24,240, at avg age 34,611,026
-   acc-ratios:  2.13 rd, 0.91 wr  (3,166,650 b-read, 1,358,820 b-written)
-      at 0x4C275B8: malloc (vg_replace_malloc.c:236)
-      by 0x40350E: tcc_malloc (tinycc.c:6712)
-      by 0x404580: tok_alloc_new (tinycc.c:7151)
-      by 0x4046C4: tok_alloc (tinycc.c:7190)
-]]></screen>
-
-<para>The acc-ratios field tells us that each byte in the blocks
-allocated here is read an average of 2.13 times before the block is
-deallocated.  Given that the blocks have an average age at death of
-34,611,026, that's one read per block per approximately every 15
-million instructions.  So from that standpoint the blocks aren't
-"working" very hard.</para>
-
-<para>More interesting is the write ratio: each byte is written an
-average of 0.91 times.  This tells us that some parts of the allocated
-blocks are never written, at least 9% on average.  To completely
-initialise the block would require writing each byte at least once,
-and that would give a write ratio of 1.0.  The fact that some block
-areas are evidently unused might point to data alignment holes or
-other layout inefficiencies.</para>
-
-<para>Well, at least all the blocks are freed (24,240 allocations,
-24,240 deaths).</para>
-
-<para>If all the blocks had been the same size, DHAT would also show
-the access counts by block offset, so we could see where exactly these
-unused areas are.  However, that isn't the case: the blocks have
-varying sizes, so DHAT can't perform such an analysis.  We can see
-that they must have varying sizes since the average block size, 61.13,
-isn't a whole number.</para>
-
-
-<sect3><title>A more suspicious looking example</title></sect3>
-
-<screen><![CDATA[
-   max-live:    180,224 in 22 blocks
-   tot-alloc:   180,224 in 22 blocks (avg size 8192.00)
-   deaths:      none (none of these blocks were freed)
-   acc-ratios:  0.00 rd, 0.00 wr  (0 b-read, 0 b-written)
-      at 0x4C275B8: malloc (vg_replace_malloc.c:236)
-      by 0x40350E: tcc_malloc (tinycc.c:6712)
-      by 0x40369C: __sym_malloc (tinycc.c:6787)
-      by 0x403711: sym_malloc (tinycc.c:6805)
-]]></screen>
-
-<para>Here, both the read and write access ratios are zero.  Hence
-this point is allocating blocks which are never used, neither read nor
-written.  Indeed, they are also not freed ("deaths: none") and are
-simply leaked.  So, here is 180k of completely useless allocation that
-could be removed.</para>
-
-<para>Re-running with Memcheck does indeed report the same leak.  What
-DHAT can tell us, that Memcheck can't, is that not only are the blocks
-leaked, they are also never used.</para>
-
-<sect3><title>Another suspicious example</title></sect3>
-
-<para>Here's one where blocks are allocated, written to,
-but never read from.  We see this immediately from the zero read
-access ratio.  They do get freed, though:</para>
-
-<screen><![CDATA[
-   max-live:    54 in 3 blocks
-   tot-alloc:   1,620 in 90 blocks (avg size 18.00)
-   deaths:      90, at avg age 34,558,236
-   acc-ratios:  0.00 rd, 1.11 wr  (0 b-read, 1,800 b-written)
-      at 0x4C275B8: malloc (vg_replace_malloc.c:236)
-      by 0x40350E: tcc_malloc (tinycc.c:6712)
-      by 0x4035BD: tcc_strdup (tinycc.c:6750)
-      by 0x41FEBB: tcc_add_sysinclude_path (tinycc.c:20931)
-]]></screen>
-
-<para>In the previous two examples, it is easy to see blocks that are
-never written to, or never read from, or some combination of both.
-Unfortunately, in C++ code, the situation is less clear.  That's
-because an object's constructor will write to the underlying block,
-and its destructor will read from it.  So the block's read and write
-ratios will be non-zero even if the object, once constructed, is never
-used, but only eventually destructed.</para>
-
-<para>Really, what we want is to measure only memory accesses in
-between the end of an object's construction and the start of its
-destruction.  Unfortunately I do not know of a reliable way to
-determine when those transitions are made.</para>
-
-
-</sect2>
-
-<sect2>
-<title>Interpreting "Aggregated access counts by offset" data</title>
-
-<para>For allocation points that always allocate blocks of the same
-size, and which are 4096 bytes or smaller, DHAT counts accesses
-per offset, for example:</para>
-
-<screen><![CDATA[
-   max-live:    317,408 in 5,668 blocks
-   tot-alloc:   317,408 in 5,668 blocks (avg size 56.00)
-   deaths:      5,668, at avg age 622,890,597
-   acc-ratios:  1.03 rd, 1.28 wr  (327,642 b-read, 408,172 b-written)
-      at 0x4C275B8: malloc (vg_replace_malloc.c:236)
-      by 0x5440C16: QDesignerPropertySheetPrivate::ensureInfo (qhash.h:515)
-      by 0x544350B: QDesignerPropertySheet::setVisible (qdesigner_propertysh...)
-      by 0x5446232: QDesignerPropertySheet::QDesignerPropertySheet (qdesigne...)
-   
-   Aggregated access counts by offset:
-   
-   [   0]  28782 28782 28782 28782 28782 28782 28782 28782
-   [   8]  20638 20638 20638 20638 0 0 0 0 
-   [  16]  22738 22738 22738 22738 22738 22738 22738 22738
-   [  24]  6013 6013 6013 6013 6013 6013 6013 6013 
-   [  32]  18883 18883 18883 37422 0 0 0 0
-   [  36]  5668 11915 5668 5668 11336 11336 11336 11336 
-   [  48]  6166 6166 6166 6166 0 0 0 0 
-]]></screen>
-
-<para>This is fairly typical, for C++ code running on a 64-bit
-platform.  Here, we have aggregated access statistics for 5668 blocks,
-all of size 56 bytes.  Each byte has been accessed at least 5668
-times, except for offsets 12--15, 36--39 and 52--55.  These are likely
-to be alignment holes.</para>
-
-<para>Careful interpretation of the numbers reveals useful information.
-Groups of N consecutive identical numbers that begin at an N-aligned
-offset, for N being 2, 4 or 8, are likely to indicate an N-byte object
-in the structure at that point.  For example, the first 32 bytes of
-this object are likely to have the layout</para>
-
-<screen><![CDATA[
-   [0 ]  64-bit type
-   [8 ]  32-bit type
-   [12]  32-bit alignment hole
-   [16]  64-bit type
-   [24]  64-bit type
-]]></screen>
-
-<para>As a counterexample, it's also clear that, whatever is at offset 32,
-it is not a 32-bit value.  That's because the last number of the group
-(37422) is not the same as the first three (18883 18883 18883).</para>
-
-<para>This example leads one to enquire (by reading the source code)
-whether the zeroes at 12--15 and 52--55 are alignment holes, and
-whether 48--51 is indeed a 32-bit type.  If so, it might be possible
-to place what's at 48--51 at 12--15 instead, which would reduce
-the object size from 56 to 48 bytes.</para>
-
-<para>Bear in mind that the above inferences are all only "maybes".  That's
-because they are based on dynamic data, not static analysis of the
-object layout.  For example, the zeroes might not be alignment
-holes, but rather just parts of the structure which were not used
-at all for this particular run.  Experience shows that's unlikely
-to be the case, but it could happen.</para>
-
-</sect2>
-
-</sect1>
-
-
-
-
-
-
-
-<sect1 id="dh-manual.options" xreflabel="DHAT Command-line Options">
-<title>DHAT Command-line Options</title>
-
-<para>DHAT-specific command-line options are:</para>
-
-<!-- start of xi:include in the manpage -->
-<variablelist id="dh.opts.list">
-
-  <varlistentry id="opt.show-top-n" xreflabel="--show-top-n">
-    <term>
-      <option><![CDATA[--show-top-n=<number>
-      [default: 10] ]]></option>
-    </term>
-    <listitem>
-      <para>At the end of the run, DHAT sorts the accumulated
-       allocation points according to some metric, and shows the
-       highest scoring entries.  <varname>--show-top-n</varname>
-       controls how many entries are shown.  The default of 10 is
-       quite small.  For realistic applications you will probably need
-       to set it much higher, at least several hundred.</para>
-    </listitem>
-  </varlistentry>
-
-  <varlistentry id="opt.sort-by" xreflabel="--sort-by=string">
-    <term>
-      <option><![CDATA[--sort-by=<string> [default: max-bytes-live] ]]></option>
-    </term>
-    <listitem>
-      <para>At the end of the run, DHAT sorts the accumulated
-       allocation points according to some metric, and shows the
-       highest scoring entries.  <varname>--sort-by</varname>
-       selects the metric used for sorting:</para>
-      <para><varname>max-bytes-live   </varname>  maximum live bytes [default]</para>
-      <para><varname>tot-bytes-allocd </varname>  bytes allocates in total (turnover)</para>
-      <para><varname>max-blocks-live  </varname>  maximum live blocks</para>
-      <para><varname>tot-blocks-allocd </varname> blocks allocated in total (turnover)</para>
-      <para>This controls the order in which allocation points are
-       displayed.  You can choose to look at allocation points with
-       the highest number of live bytes, or the highest total byte turnover, or
-       by the highest number of live blocks, or the highest total block
-       turnover.  These give usefully different pictures of program behaviour.
-       For example, sorting by maximum live blocks tends to show up allocation
-       points creating large numbers of small objects.</para>
-    </listitem>
-  </varlistentry>
-
-</variablelist>
-
-<para>One important point to note is that each allocation stack counts
-as a separate allocation point.  Because stacks by default have 12
-frames, this tends to spread data out over multiple allocation points.
-You may want to use the flag --num-callers=4 or some such small
-number, to reduce the spreading.</para>
-
-<!-- end of xi:include in the manpage -->
-
-</sect1>
-
-</chapter>
diff --git a/exp-dhat/tests/Makefile.am b/exp-dhat/tests/Makefile.am
deleted file mode 100644 (file)
index 8b13789..0000000
+++ /dev/null
@@ -1 +0,0 @@
-
index ca96f54b5b7a1ac1e8cbad07d65bab7b7df37b2d..3cda7f8494e0eb6a0c1d208d8a794b2df1f79b14 100644 (file)
@@ -7,7 +7,7 @@
    This file is part of Valgrind, a dynamic binary instrumentation
    framework.
 
-   Copyright (C) 2010-2017 Mozilla Inc
+   Copyright (C) 2010-2017 Mozilla Foundation
 
    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
index b15fa5dd216cfad9b17c6063fdcacb4bbf17d84c..9749bd9cf839e7de43298078ca6dd63b430b8f60 100644 (file)
@@ -1,6 +1,6 @@
-//--------------------------------------------------------------------*/
-//--- Massif: a heap profiling tool.                     ms_main.c ---*/
-//--------------------------------------------------------------------*/
+//--------------------------------------------------------------------//
+//--- Massif: a heap profiling tool.                     ms_main.c ---//
+//--------------------------------------------------------------------//
 
 /*
    This file is part of Massif, a Valgrind tool for profiling memory
index 66703355294b076de384a6964f6499297e5bf4d2..7b65b4e93e0e910b708f9f17ec2eceecc789f2de 100644 (file)
@@ -50,12 +50,12 @@ file path=usr/lib/valgrind/cachegrind-x86-solaris                        owner=r
 file path=usr/lib/valgrind/callgrind-amd64-solaris                       owner=root group=bin mode=0755
 file path=usr/lib/valgrind/callgrind-x86-solaris                         owner=root group=bin mode=0755
 file path=usr/lib/valgrind/default.supp                                  owner=root group=bin mode=0644
+file path=usr/lib/valgrind/dhat-amd64-solaris                            owner=root group=bin mode=0755
+file path=usr/lib/valgrind/dhat-x86-solaris                              owner=root group=bin mode=0755
 file path=usr/lib/valgrind/drd-amd64-solaris                             owner=root group=bin mode=0755
 file path=usr/lib/valgrind/drd-x86-solaris                               owner=root group=bin mode=0755
 file path=usr/lib/valgrind/exp-bbv-amd64-solaris                         owner=root group=bin mode=0755
 file path=usr/lib/valgrind/exp-bbv-x86-solaris                           owner=root group=bin mode=0755
-file path=usr/lib/valgrind/exp-dhat-amd64-solaris                        owner=root group=bin mode=0755
-file path=usr/lib/valgrind/exp-dhat-x86-solaris                          owner=root group=bin mode=0755
 file path=usr/lib/valgrind/exp-sgcheck-amd64-solaris                     owner=root group=bin mode=0755
 file path=usr/lib/valgrind/exp-sgcheck-x86-solaris                       owner=root group=bin mode=0755
 file path=usr/lib/valgrind/getoff-amd64-solaris                          owner=root group=bin mode=0755
@@ -75,8 +75,8 @@ file path=usr/lib/valgrind/vgpreload_core-amd64-solaris.so               owner=r
 file path=usr/lib/valgrind/vgpreload_core-x86-solaris.so                 owner=root group=bin mode=0755
 file path=usr/lib/valgrind/vgpreload_drd-amd64-solaris.so                owner=root group=bin mode=0755
 file path=usr/lib/valgrind/vgpreload_drd-x86-solaris.so                  owner=root group=bin mode=0755
-file path=usr/lib/valgrind/vgpreload_exp-dhat-amd64-solaris.so           owner=root group=bin mode=0755
-file path=usr/lib/valgrind/vgpreload_exp-dhat-x86-solaris.so             owner=root group=bin mode=0755
+file path=usr/lib/valgrind/vgpreload_dhat-amd64-solaris.so               owner=root group=bin mode=0755
+file path=usr/lib/valgrind/vgpreload_dhat-x86-solaris.so                 owner=root group=bin mode=0755
 file path=usr/lib/valgrind/vgpreload_exp-sgcheck-amd64-solaris.so        owner=root group=bin mode=0755
 file path=usr/lib/valgrind/vgpreload_exp-sgcheck-x86-solaris.so          owner=root group=bin mode=0755
 file path=usr/lib/valgrind/vgpreload_massif-amd64-solaris.so             owner=root group=bin mode=0755
index 70c06d46f4b75b5a62239376ba422b17a950e4a6..1c3e3f5d845543843fee68e25c96c417a03b1180 100755 (executable)
@@ -42,18 +42,18 @@ my %coregrind_dirs = (
     );
 
 my %tool_dirs = (
-    "none" => 1,
-    "lackey" => 1,
-    "massif" => 1,
     "memcheck" => 1,
-    "drd" => 1,
-    "helgrind", => 1,
-    "callgrind" => 1,
     "cachegrind" => 1,
-    "shared" => 1,
+    "callgrind" => 1,
+    "helgrind", => 1,
+    "drd" => 1,
+    "massif" => 1,
+    "dhat" => 1,
+    "lackey" => 1,
+    "none" => 1,
     "exp-bbv" => 1,
-    "exp-dhat" => 1,
     "exp-sgcheck" => 1
+    "shared" => 1,
     );
 
 my %dirs_to_ignore = (