Revert "[tools] Replace the Chromium binary size tool, which no longer works."

This reverts commit e32d98cd0608adda0bfb9bd983d3a566fa030561.

Reason for revert: Dart rolls into the engine are failing because there are references to run_binary_size_analysis in the "Upload artifacts android-arm64-release" step of the build

Original change's description:
> [tools] Replace the Chromium binary size tool, which no longer works.
>
> Change-Id: Id84717e21a129a392d3bc4e9b4cce84dfb4771e1
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/231066
> Reviewed-by: Ben Konyi <bkonyi@google.com>

# Not skipping CQ checks because original CL landed > 1 day ago.

Change-Id: Ibfbdf1e0a970ad7fae9ec1d39d24722647b38730
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/231600
Reviewed-by: Siva Annamalai <asiva@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Siva Annamalai <asiva@google.com>
diff --git a/runtime/third_party/binary_size/README.dart b/runtime/third_party/binary_size/README.dart
new file mode 100644
index 0000000..0f2124e
--- /dev/null
+++ b/runtime/third_party/binary_size/README.dart
@@ -0,0 +1 @@
+A local copy of tools/binary_size from Chromium project.
\ No newline at end of file
diff --git a/runtime/third_party/binary_size/src/binary_size_utils.py b/runtime/third_party/binary_size/src/binary_size_utils.py
new file mode 100644
index 0000000..8ef283e
--- /dev/null
+++ b/runtime/third_party/binary_size/src/binary_size_utils.py
@@ -0,0 +1,69 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Common utilities for tools that deal with binary size information.
+"""
+
+import logging
+import re
+
+
+def ParseNm(nm_lines):
+    """Parse nm output, returning data for all relevant (to binary size)
+  symbols and ignoring the rest.
+
+  Args:
+      nm_lines: an iterable over lines of nm output.
+
+  Yields:
+      (symbol name, symbol type, symbol size, source file path).
+
+      Path may be None if nm couldn't figure out the source file.
+  """
+
+    # Match lines with size, symbol, optional location, optional discriminator
+    sym_re = re.compile(r'^([0-9a-f]{8,}) '  # address (8+ hex digits)
+                        '([0-9a-f]{8,}) '  # size (8+ hex digits)
+                        '(.) '  # symbol type, one character
+                        '([^\t]+)'  # symbol name, separated from next by tab
+                        '(?:\t(.*):[\d\?]+)?.*$')  # location
+    # Match lines with addr but no size.
+    addr_re = re.compile(r'^[0-9a-f]{8,} (.) ([^\t]+)(?:\t.*)?$')
+    # Match lines that don't have an address at all -- typically external symbols.
+    noaddr_re = re.compile(r'^ {8,} (.) (.*)$')
+    # Match lines with no symbol name, only addr and type
+    addr_only_re = re.compile(r'^[0-9a-f]{8,} (.)$')
+
+    seen_lines = set()
+    for line in nm_lines:
+        line = line.rstrip()
+        if line in seen_lines:
+            # nm outputs identical lines at times. We don't want to treat
+            # those as distinct symbols because that would make no sense.
+            continue
+        seen_lines.add(line)
+        match = sym_re.match(line)
+        if match:
+            address, size, sym_type, sym = match.groups()[0:4]
+            size = int(size, 16)
+            if sym_type in ('B', 'b'):
+                continue  # skip all BSS for now.
+            path = match.group(5)
+            yield sym, sym_type, size, path, address
+            continue
+        match = addr_re.match(line)
+        if match:
+            # sym_type, sym = match.groups()[0:2]
+            continue  # No size == we don't care.
+        match = noaddr_re.match(line)
+        if match:
+            sym_type, sym = match.groups()
+            if sym_type in ('U', 'w'):
+                continue  # external or weak symbol
+        match = addr_only_re.match(line)
+        if match:
+            continue  # Nothing to do.
+
+        # If we reach this part of the loop, there was something in the
+        # line that we didn't expect or recognize.
+        logging.warning('nm output parser failed to parse: %s', repr(line))
diff --git a/runtime/third_party/binary_size/src/elf_symbolizer.py b/runtime/third_party/binary_size/src/elf_symbolizer.py
new file mode 100644
index 0000000..5154a9b
--- /dev/null
+++ b/runtime/third_party/binary_size/src/elf_symbolizer.py
@@ -0,0 +1,490 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import collections
+import datetime
+import logging
+import multiprocessing
+import os
+import posixpath
+import queue
+import re
+import subprocess
+import sys
+import threading
+import time
+
+# addr2line builds a possibly infinite memory cache that can exhaust
+# the computer's memory if allowed to grow for too long. This constant
+# controls how many lookups we do before restarting the process. 4000
+# gives near peak performance without extreme memory usage.
+ADDR2LINE_RECYCLE_LIMIT = 4000
+
+
+class ELFSymbolizer(object):
+    """An uber-fast (multiprocessing, pipelined and asynchronous) ELF symbolizer.
+
+  This class is a frontend for addr2line (part of GNU binutils), designed to
+  symbolize batches of large numbers of symbols for a given ELF file. It
+  supports sharding symbolization against many addr2line instances and
+  pipelining of multiple requests per each instance (in order to hide addr2line
+  internals and OS pipe latencies).
+
+  The interface exhibited by this class is a very simple asynchronous interface,
+  which is based on the following three methods:
+  - SymbolizeAsync(): used to request (enqueue) resolution of a given address.
+  - The |callback| method: used to communicated back the symbol information.
+  - Join(): called to conclude the batch to gather the last outstanding results.
+  In essence, before the Join method returns, this class will have issued as
+  many callbacks as the number of SymbolizeAsync() calls. In this regard, note
+  that due to multiprocess sharding, callbacks can be delivered out of order.
+
+  Some background about addr2line:
+  - it is invoked passing the elf path in the cmdline, piping the addresses in
+    its stdin and getting results on its stdout.
+  - it has pretty large response times for the first requests, but it
+    works very well in streaming mode once it has been warmed up.
+  - it doesn't scale by itself (on more cores). However, spawning multiple
+    instances at the same time on the same file is pretty efficient as they
+    keep hitting the pagecache and become mostly CPU bound.
+  - it might hang or crash, mostly for OOM. This class deals with both of these
+    problems.
+
+  Despite the "scary" imports and the multi* words above, (almost) no multi-
+  threading/processing is involved from the python viewpoint. Concurrency
+  here is achieved by spawning several addr2line subprocesses and handling their
+  output pipes asynchronously. Therefore, all the code here (with the exception
+  of the Queue instance in Addr2Line) should be free from mind-blowing
+  thread-safety concerns.
+
+  The multiprocess sharding works as follows:
+  The symbolizer tries to use the lowest number of addr2line instances as
+  possible (with respect of |max_concurrent_jobs|) and enqueue all the requests
+  in a single addr2line instance. For few symbols (i.e. dozens) sharding isn't
+  worth the startup cost.
+  The multiprocess logic kicks in as soon as the queues for the existing
+  instances grow. Specifically, once all the existing instances reach the
+  |max_queue_size| bound, a new addr2line instance is kicked in.
+  In the case of a very eager producer (i.e. all |max_concurrent_jobs| instances
+  have a backlog of |max_queue_size|), back-pressure is applied on the caller by
+  blocking the SymbolizeAsync method.
+
+  This module has been deliberately designed to be dependency free (w.r.t. of
+  other modules in this project), to allow easy reuse in external projects.
+  """
+
+    def __init__(self,
+                 elf_file_path,
+                 addr2line_path,
+                 callback,
+                 inlines=False,
+                 max_concurrent_jobs=None,
+                 addr2line_timeout=30,
+                 max_queue_size=50,
+                 source_root_path=None,
+                 strip_base_path=None):
+        """Args:
+      elf_file_path: path of the elf file to be symbolized.
+      addr2line_path: path of the toolchain's addr2line binary.
+      callback: a callback which will be invoked for each resolved symbol with
+          the two args (sym_info, callback_arg). The former is an instance of
+          |ELFSymbolInfo| and contains the symbol information. The latter is an
+          embedder-provided argument which is passed to SymbolizeAsync().
+      inlines: when True, the ELFSymbolInfo will contain also the details about
+          the outer inlining functions. When False, only the innermost function
+          will be provided.
+      max_concurrent_jobs: Max number of addr2line instances spawned.
+          Parallelize responsibly, addr2line is a memory and I/O monster.
+      max_queue_size: Max number of outstanding requests per addr2line instance.
+      addr2line_timeout: Max time (in seconds) to wait for a addr2line response.
+          After the timeout, the instance will be considered hung and respawned.
+      source_root_path: In some toolchains only the name of the source file is
+          is output, without any path information; disambiguation searches
+          through the source directory specified by |source_root_path| argument
+          for files whose name matches, adding the full path information to the
+          output. For example, if the toolchain outputs "unicode.cc" and there
+          is a file called "unicode.cc" located under |source_root_path|/foo,
+          the tool will replace "unicode.cc" with
+          "|source_root_path|/foo/unicode.cc". If there are multiple files with
+          the same name, disambiguation will fail because the tool cannot
+          determine which of the files was the source of the symbol.
+      strip_base_path: Rebases the symbols source paths onto |source_root_path|
+          (i.e replace |strip_base_path| with |source_root_path).
+    """
+        assert (os.path.isfile(addr2line_path)), 'Cannot find ' + addr2line_path
+        self.elf_file_path = elf_file_path
+        self.addr2line_path = addr2line_path
+        self.callback = callback
+        self.inlines = inlines
+        self.max_concurrent_jobs = (max_concurrent_jobs or
+                                    min(multiprocessing.cpu_count(), 4))
+        self.max_queue_size = max_queue_size
+        self.addr2line_timeout = addr2line_timeout
+        self.requests_counter = 0  # For generating monotonic request IDs.
+        self._a2l_instances = []  # Up to |max_concurrent_jobs| _Addr2Line inst.
+
+        # If necessary, create disambiguation lookup table
+        self.disambiguate = source_root_path is not None
+        self.disambiguation_table = {}
+        self.strip_base_path = strip_base_path
+        if (self.disambiguate):
+            self.source_root_path = os.path.abspath(source_root_path)
+            self._CreateDisambiguationTable()
+
+        # Create one addr2line instance. More instances will be created on demand
+        # (up to |max_concurrent_jobs|) depending on the rate of the requests.
+        self._CreateNewA2LInstance()
+
+    def SymbolizeAsync(self, addr, callback_arg=None):
+        """Requests symbolization of a given address.
+
+    This method is not guaranteed to return immediately. It generally does, but
+    in some scenarios (e.g. all addr2line instances have full queues) it can
+    block to create back-pressure.
+
+    Args:
+      addr: address to symbolize.
+      callback_arg: optional argument which will be passed to the |callback|."""
+        assert (isinstance(addr, int))
+
+        # Process all the symbols that have been resolved in the meanwhile.
+        # Essentially, this drains all the addr2line(s) out queues.
+        for a2l_to_purge in self._a2l_instances:
+            a2l_to_purge.ProcessAllResolvedSymbolsInQueue()
+            a2l_to_purge.RecycleIfNecessary()
+
+        # Find the best instance according to this logic:
+        # 1. Find an existing instance with the shortest queue.
+        # 2. If all of instances' queues are full, but there is room in the pool,
+        #    (i.e. < |max_concurrent_jobs|) create a new instance.
+        # 3. If there were already |max_concurrent_jobs| instances and all of them
+        #    had full queues, make back-pressure.
+
+        # 1.
+        def _SortByQueueSizeAndReqID(a2l):
+            return (a2l.queue_size, a2l.first_request_id)
+
+        a2l = min(self._a2l_instances, key=_SortByQueueSizeAndReqID)
+
+        # 2.
+        if (a2l.queue_size >= self.max_queue_size and
+                len(self._a2l_instances) < self.max_concurrent_jobs):
+            a2l = self._CreateNewA2LInstance()
+
+        # 3.
+        if a2l.queue_size >= self.max_queue_size:
+            a2l.WaitForNextSymbolInQueue()
+
+        a2l.EnqueueRequest(addr, callback_arg)
+
+    def Join(self):
+        """Waits for all the outstanding requests to complete and terminates."""
+        for a2l in self._a2l_instances:
+            a2l.WaitForIdle()
+            a2l.Terminate()
+
+    def _CreateNewA2LInstance(self):
+        assert (len(self._a2l_instances) < self.max_concurrent_jobs)
+        a2l = ELFSymbolizer.Addr2Line(self)
+        self._a2l_instances.append(a2l)
+        return a2l
+
+    def _CreateDisambiguationTable(self):
+        """ Non-unique file names will result in None entries"""
+        start_time = time.time()
+        logging.info('Collecting information about available source files...')
+        self.disambiguation_table = {}
+
+        for root, _, filenames in os.walk(self.source_root_path):
+            for f in filenames:
+                self.disambiguation_table[f] = os.path.join(
+                    root, f) if (f not in self.disambiguation_table) else None
+        logging.info(
+            'Finished collecting information about '
+            'possible files (took %.1f s).', (time.time() - start_time))
+
+    class Addr2Line(object):
+        """A python wrapper around an addr2line instance.
+
+    The communication with the addr2line process looks as follows:
+      [STDIN]         [STDOUT]  (from addr2line's viewpoint)
+    > f001111
+    > f002222
+                    < Symbol::Name(foo, bar) for f001111
+                    < /path/to/source/file.c:line_number
+    > f003333
+                    < Symbol::Name2() for f002222
+                    < /path/to/source/file.c:line_number
+                    < Symbol::Name3() for f003333
+                    < /path/to/source/file.c:line_number
+    """
+
+        SYM_ADDR_RE = re.compile(r'([^:]+):(\?|\d+).*')
+
+        def __init__(self, symbolizer):
+            self._symbolizer = symbolizer
+            self._lib_file_name = posixpath.basename(symbolizer.elf_file_path)
+
+            # The request queue (i.e. addresses pushed to addr2line's stdin and not
+            # yet retrieved on stdout)
+            self._request_queue = collections.deque()
+
+            # This is essentially len(self._request_queue). It has been optimized to a
+            # separate field because turned out to be a perf hot-spot.
+            self.queue_size = 0
+
+            # Keep track of the number of symbols a process has processed to
+            # avoid a single process growing too big and using all the memory.
+            self._processed_symbols_count = 0
+
+            # Objects required to handle the addr2line subprocess.
+            self._proc = None  # Subprocess.Popen(...) instance.
+            self._thread = None  # Threading.thread instance.
+            self._out_queue = None  # queue.Queue instance (for buffering a2l stdout).
+            self._RestartAddr2LineProcess()
+
+        def EnqueueRequest(self, addr, callback_arg):
+            """Pushes an address to addr2line's stdin (and keeps track of it)."""
+            self._symbolizer.requests_counter += 1  # For global "age" of requests.
+            req_idx = self._symbolizer.requests_counter
+            self._request_queue.append((addr, callback_arg, req_idx))
+            self.queue_size += 1
+            self._WriteToA2lStdin(addr)
+
+        def WaitForIdle(self):
+            """Waits until all the pending requests have been symbolized."""
+            while self.queue_size > 0:
+                self.WaitForNextSymbolInQueue()
+
+        def WaitForNextSymbolInQueue(self):
+            """Waits for the next pending request to be symbolized."""
+            if not self.queue_size:
+                return
+
+            # This outer loop guards against a2l hanging (detecting stdout timeout).
+            while True:
+                start_time = datetime.datetime.now()
+                timeout = datetime.timedelta(
+                    seconds=self._symbolizer.addr2line_timeout)
+
+                # The inner loop guards against a2l crashing (checking if it exited).
+                while (datetime.datetime.now() - start_time < timeout):
+                    # poll() returns !None if the process exited. a2l should never exit.
+                    if self._proc.poll():
+                        logging.warning(
+                            'addr2line crashed, respawning (lib: %s).' %
+                            self._lib_file_name)
+                        self._RestartAddr2LineProcess()
+                        # TODO(primiano): the best thing to do in this case would be
+                        # shrinking the pool size as, very likely, addr2line is crashed
+                        # due to low memory (and the respawned one will die again soon).
+
+                    try:
+                        lines = self._out_queue.get(block=True, timeout=0.25)
+                    except queue.Empty:
+                        # On timeout (1/4 s.) repeat the inner loop and check if either the
+                        # addr2line process did crash or we waited its output for too long.
+                        continue
+
+                    # In nominal conditions, we get straight to this point.
+                    self._ProcessSymbolOutput(lines)
+                    return
+
+                # If this point is reached, we waited more than |addr2line_timeout|.
+                logging.warning('Hung addr2line process, respawning (lib: %s).'
+                                % self._lib_file_name)
+                self._RestartAddr2LineProcess()
+
+        def ProcessAllResolvedSymbolsInQueue(self):
+            """Consumes all the addr2line output lines produced (without blocking)."""
+            if not self.queue_size:
+                return
+            while True:
+                try:
+                    lines = self._out_queue.get_nowait()
+                except queue.Empty:
+                    break
+                self._ProcessSymbolOutput(lines)
+
+        def RecycleIfNecessary(self):
+            """Restarts the process if it has been used for too long.
+
+      A long running addr2line process will consume excessive amounts
+      of memory without any gain in performance."""
+            if self._processed_symbols_count >= ADDR2LINE_RECYCLE_LIMIT:
+                self._RestartAddr2LineProcess()
+
+        def Terminate(self):
+            """Kills the underlying addr2line process.
+
+      The poller |_thread| will terminate as well due to the broken pipe."""
+            try:
+                self._proc.kill()
+                self._proc.communicate(
+                )  # Essentially wait() without risking deadlock.
+            except Exception:  # An exception while terminating? How interesting.
+                pass
+            self._proc = None
+
+        def _WriteToA2lStdin(self, addr):
+            self._proc.stdin.write(('%s\n' % hex(addr)).encode())
+            if self._symbolizer.inlines:
+                # In the case of inlines we output an extra blank line, which causes
+                # addr2line to emit a (??,??:0) tuple that we use as a boundary marker.
+                self._proc.stdin.write('\n')
+            self._proc.stdin.flush()
+
+        def _ProcessSymbolOutput(self, lines):
+            """Parses an addr2line symbol output and triggers the client callback."""
+            (_, callback_arg, _) = self._request_queue.popleft()
+            self.queue_size -= 1
+
+            innermost_sym_info = None
+            sym_info = None
+            for (line1, line2) in lines:
+                prev_sym_info = sym_info
+                name = line1 if not line1.startswith('?') else None
+                source_path = None
+                source_line = None
+                m = ELFSymbolizer.Addr2Line.SYM_ADDR_RE.match(line2)
+                if m:
+                    if not m.group(1).startswith('?'):
+                        source_path = m.group(1)
+                        if not m.group(2).startswith('?'):
+                            source_line = int(m.group(2))
+                else:
+                    logging.warning(
+                        'Got invalid symbol path from addr2line: %s' % line2)
+
+                # In case disambiguation is on, and needed
+                was_ambiguous = False
+                disambiguated = False
+                if self._symbolizer.disambiguate:
+                    if source_path and not posixpath.isabs(source_path):
+                        path = self._symbolizer.disambiguation_table.get(
+                            source_path)
+                        was_ambiguous = True
+                        disambiguated = path is not None
+                        source_path = path if disambiguated else source_path
+
+                    # Use absolute paths (so that paths are consistent, as disambiguation
+                    # uses absolute paths)
+                    if source_path and not was_ambiguous:
+                        source_path = os.path.abspath(source_path)
+
+                if source_path and self._symbolizer.strip_base_path:
+                    # Strip the base path
+                    source_path = re.sub(
+                        '^' + self._symbolizer.strip_base_path,
+                        self._symbolizer.source_root_path or '', source_path)
+
+                sym_info = ELFSymbolInfo(name, source_path, source_line,
+                                         was_ambiguous, disambiguated)
+                if prev_sym_info:
+                    prev_sym_info.inlined_by = sym_info
+                if not innermost_sym_info:
+                    innermost_sym_info = sym_info
+
+            self._processed_symbols_count += 1
+            self._symbolizer.callback(innermost_sym_info, callback_arg)
+
+        def _RestartAddr2LineProcess(self):
+            if self._proc:
+                self.Terminate()
+
+            # The only reason of existence of this Queue (and the corresponding
+            # Thread below) is the lack of a subprocess.stdout.poll_avail_lines().
+            # Essentially this is a pipe able to extract a couple of lines atomically.
+            self._out_queue = queue.Queue()
+
+            # Start the underlying addr2line process in line buffered mode.
+
+            cmd = [
+                self._symbolizer.addr2line_path, '--functions', '--demangle',
+                '--exe=' + self._symbolizer.elf_file_path
+            ]
+            if self._symbolizer.inlines:
+                cmd += ['--inlines']
+            self._proc = subprocess.Popen(
+                cmd,
+                stdout=subprocess.PIPE,
+                stdin=subprocess.PIPE,
+                stderr=sys.stderr,
+                close_fds=True)
+
+            # Start the poller thread, which simply moves atomically the lines read
+            # from the addr2line's stdout to the |_out_queue|.
+            self._thread = threading.Thread(
+                target=ELFSymbolizer.Addr2Line.StdoutReaderThread,
+                args=(self._proc.stdout, self._out_queue,
+                      self._symbolizer.inlines))
+            self._thread.daemon = True  # Don't prevent early process exit.
+            self._thread.start()
+
+            self._processed_symbols_count = 0
+
+            # Replay the pending requests on the new process (only for the case
+            # of a hung addr2line timing out during the game).
+            for (addr, _, _) in self._request_queue:
+                self._WriteToA2lStdin(addr)
+
+        @staticmethod
+        def StdoutReaderThread(process_pipe, queue, inlines):
+            """The poller thread fn, which moves the addr2line stdout to the |queue|.
+
+      This is the only piece of code not running on the main thread. It merely
+      writes to a Queue, which is thread-safe. In the case of inlines, it
+      detects the ??,??:0 marker and sends the lines atomically, such that the
+      main thread always receives all the lines corresponding to one symbol in
+      one shot."""
+            try:
+                lines_for_one_symbol = []
+                while True:
+                    line1 = process_pipe.readline().decode().rstrip('\r\n')
+                    line2 = process_pipe.readline().decode().rstrip('\r\n')
+                    if not line1 or not line2:
+                        break
+                    inline_has_more_lines = inlines and (
+                        len(lines_for_one_symbol) == 0 or
+                        (line1 != '??' and line2 != '??:0'))
+                    if not inlines or inline_has_more_lines:
+                        lines_for_one_symbol += [(line1, line2)]
+                    if inline_has_more_lines:
+                        continue
+                    queue.put(lines_for_one_symbol)
+                    lines_for_one_symbol = []
+                process_pipe.close()
+
+            # Every addr2line processes will die at some point, please die silently.
+            except (IOError, OSError):
+                pass
+
+        @property
+        def first_request_id(self):
+            """Returns the request_id of the oldest pending request in the queue."""
+            return self._request_queue[0][2] if self._request_queue else 0
+
+
+class ELFSymbolInfo(object):
+    """The result of the symbolization passed as first arg. of each callback."""
+
+    def __init__(self,
+                 name,
+                 source_path,
+                 source_line,
+                 was_ambiguous=False,
+                 disambiguated=False):
+        """All the fields here can be None (if addr2line replies with '??')."""
+        self.name = name
+        self.source_path = source_path
+        self.source_line = source_line
+        # In the case of |inlines|=True, the |inlined_by| points to the outer
+        # function inlining the current one (and so on, to form a chain).
+        self.inlined_by = None
+        self.disambiguated = disambiguated
+        self.was_ambiguous = was_ambiguous
+
+    def __str__(self):
+        return '%s [%s:%d]' % (self.name or '??', self.source_path or '??',
+                               self.source_line or 0)
diff --git a/runtime/third_party/binary_size/src/explain_binary_size_delta.py b/runtime/third_party/binary_size/src/explain_binary_size_delta.py
new file mode 100755
index 0000000..13f7c12
--- /dev/null
+++ b/runtime/third_party/binary_size/src/explain_binary_size_delta.py
@@ -0,0 +1,519 @@
+#!/usr/bin/env python3
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Describe the size difference of two binaries.
+
+Generates a description of the size difference of two binaries based
+on the difference of the size of various symbols.
+
+This tool needs "nm" dumps of each binary with full symbol
+information. You can obtain the necessary dumps by running the
+run_binary_size_analysis.py script upon each binary, with the
+"--nm-out" parameter set to the location in which you want to save the
+dumps. Example:
+
+  # obtain symbol data from first binary in /tmp/nm1.dump
+  cd $CHECKOUT1_SRC
+  ninja -C out/Release binary_size_tool
+  tools/binary_size/run_binary_size_analysis \
+      --library <path_to_library>
+      --destdir /tmp/throwaway
+      --nm-out /tmp/nm1.dump
+
+  # obtain symbol data from second binary in /tmp/nm2.dump
+  cd $CHECKOUT2_SRC
+  ninja -C out/Release binary_size_tool
+  tools/binary_size/run_binary_size_analysis \
+      --library <path_to_library>
+      --destdir /tmp/throwaway
+      --nm-out /tmp/nm2.dump
+
+  # cleanup useless files
+  rm -r /tmp/throwaway
+
+  # run this tool
+  explain_binary_size_delta.py --nm1 /tmp/nm1.dump --nm2 /tmp/nm2.dump
+"""
+
+import collections
+from collections import Counter
+from math import ceil
+import operator
+import optparse
+import os
+import sys
+
+import binary_size_utils
+
+
+def CalculateSharedAddresses(symbols):
+    """Checks how many symbols share the same memory space. This returns a
+Counter result where result[address] will tell you how many times address was
+used by symbols."""
+    count = Counter()
+    for _, _, _, _, address in symbols:
+        count[address] += 1
+
+    return count
+
+
+def CalculateEffectiveSize(share_count, address, symbol_size):
+    """Given a raw symbol_size and an address, this method returns the
+  size we should blame on this symbol considering it might share the
+  machine code/data with other symbols. Using the raw symbol_size for
+  each symbol would in those cases over estimate the true cost of that
+  block.
+
+  """
+    shared_count = share_count[address]
+    if shared_count == 1:
+        return symbol_size
+
+    assert shared_count > 1
+    return int(ceil(symbol_size / float(shared_count)))
+
+
+class SymbolDelta(object):
+    """Stores old size, new size and some metadata."""
+
+    def __init__(self, shared):
+        self.old_size = None
+        self.new_size = None
+        self.shares_space_with_other_symbols = shared
+
+    def __eq__(self, other):
+        return (self.old_size == other.old_size and
+                self.new_size == other.new_size and
+                self.shares_space_with_other_symbols == other.
+                shares_space_with_other_symbols)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def copy_symbol_delta(self):
+        symbol_delta = SymbolDelta(self.shares_space_with_other_symbols)
+        symbol_delta.old_size = self.old_size
+        symbol_delta.new_size = self.new_size
+        return symbol_delta
+
+
+class DeltaInfo(SymbolDelta):
+    """Summary of a the change for one symbol between two instances."""
+
+    def __init__(self, file_path, symbol_type, symbol_name, shared):
+        SymbolDelta.__init__(self, shared)
+        self.file_path = file_path
+        self.symbol_type = symbol_type
+        self.symbol_name = symbol_name
+
+    def __eq__(self, other):
+        return (self.file_path == other.file_path and
+                self.symbol_type == other.symbol_type and
+                self.symbol_name == other.symbol_name and
+                SymbolDelta.__eq__(self, other))
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def ExtractSymbolDelta(self):
+        """Returns a copy of the SymbolDelta for this DeltaInfo."""
+        return SymbolDelta.copy_symbol_delta(self)
+
+
+def Compare(symbols1, symbols2):
+    """Executes a comparison of the symbols in symbols1 and symbols2.
+
+  Returns:
+      tuple of lists: (added_symbols, removed_symbols, changed_symbols, others)
+      where each list contains DeltaInfo objects.
+  """
+    added = []  # tuples
+    removed = []  # tuples
+    changed = []  # tuples
+    unchanged = []  # tuples
+
+    cache1 = {}
+    cache2 = {}
+    # Make a map of (file, symbol_type) : (symbol_name, effective_symbol_size)
+    share_count1 = CalculateSharedAddresses(symbols1)
+    share_count2 = CalculateSharedAddresses(symbols2)
+    for cache, symbols, share_count in ((cache1, symbols1, share_count1),
+                                        (cache2, symbols2, share_count2)):
+        for symbol_name, symbol_type, symbol_size, file_path, address in symbols:
+            if 'vtable for ' in symbol_name:
+                symbol_type = '@'  # hack to categorize these separately
+            if file_path:
+                file_path = os.path.normpath(file_path)
+                if sys.platform.startswith('win'):
+                    file_path = file_path.replace('\\', '/')
+            else:
+                file_path = '(No Path)'
+            # Take into consideration that multiple symbols might share the same
+            # block of code.
+            effective_symbol_size = CalculateEffectiveSize(
+                share_count, address, symbol_size)
+            key = (file_path, symbol_type)
+            bucket = cache.setdefault(key, {})
+            size_list = bucket.setdefault(symbol_name, [])
+            size_list.append((effective_symbol_size,
+                              effective_symbol_size != symbol_size))
+
+    # Now diff them. We iterate over the elements in cache1. For each symbol
+    # that we find in cache2, we record whether it was deleted, changed, or
+    # unchanged. We then remove it from cache2; all the symbols that remain
+    # in cache2 at the end of the iteration over cache1 are the 'new' symbols.
+    for key, bucket1 in cache1.items():
+        bucket2 = cache2.get(key)
+        file_path, symbol_type = key
+        if not bucket2:
+            # A file was removed. Everything in bucket1 is dead.
+            for symbol_name, symbol_size_list in bucket1.items():
+                for (symbol_size, shared) in symbol_size_list:
+                    delta_info = DeltaInfo(file_path, symbol_type, symbol_name,
+                                           shared)
+                    delta_info.old_size = symbol_size
+                    removed.append(delta_info)
+        else:
+            # File still exists, look for changes within.
+            for symbol_name, symbol_size_list in bucket1.items():
+                size_list2 = bucket2.get(symbol_name)
+                if size_list2 is None:
+                    # Symbol no longer exists in bucket2.
+                    for (symbol_size, shared) in symbol_size_list:
+                        delta_info = DeltaInfo(file_path, symbol_type,
+                                               symbol_name, shared)
+                        delta_info.old_size = symbol_size
+                        removed.append(delta_info)
+                else:
+                    del bucket2[
+                        symbol_name]  # Symbol is not new, delete from cache2.
+                    if len(symbol_size_list) == 1 and len(size_list2) == 1:
+                        symbol_size, shared1 = symbol_size_list[0]
+                        size2, shared2 = size_list2[0]
+                        delta_info = DeltaInfo(file_path, symbol_type,
+                                               symbol_name, shared1 or shared2)
+                        delta_info.old_size = symbol_size
+                        delta_info.new_size = size2
+                        if symbol_size != size2:
+                            # Symbol has change size in bucket.
+                            changed.append(delta_info)
+                        else:
+                            # Symbol is unchanged.
+                            unchanged.append(delta_info)
+                    else:
+                        # Complex comparison for when a symbol exists multiple times
+                        # in the same file (where file can be "unknown file").
+                        symbol_size_counter = collections.Counter(
+                            symbol_size_list)
+                        delta_counter = collections.Counter(symbol_size_list)
+                        delta_counter.subtract(size_list2)
+                        for delta_counter_key in sorted(delta_counter.keys()):
+                            delta = delta_counter[delta_counter_key]
+                            unchanged_count = symbol_size_counter[
+                                delta_counter_key]
+                            (symbol_size, shared) = delta_counter_key
+                            if delta > 0:
+                                unchanged_count -= delta
+                            for _ in range(unchanged_count):
+                                delta_info = DeltaInfo(file_path, symbol_type,
+                                                       symbol_name, shared)
+                                delta_info.old_size = symbol_size
+                                delta_info.new_size = symbol_size
+                                unchanged.append(delta_info)
+                            if delta > 0:  # Used to be more of these than there is now.
+                                for _ in range(delta):
+                                    delta_info = DeltaInfo(
+                                        file_path, symbol_type, symbol_name,
+                                        shared)
+                                    delta_info.old_size = symbol_size
+                                    removed.append(delta_info)
+                            elif delta < 0:  # More of this (symbol,size) now.
+                                for _ in range(-delta):
+                                    delta_info = DeltaInfo(
+                                        file_path, symbol_type, symbol_name,
+                                        shared)
+                                    delta_info.new_size = symbol_size
+                                    added.append(delta_info)
+
+                    if len(bucket2) == 0:
+                        del cache1[
+                            key]  # Entire bucket is empty, delete from cache2
+
+    # We have now analyzed all symbols that are in cache1 and removed all of
+    # the encountered symbols from cache2. What's left in cache2 is the new
+    # symbols.
+    for key, bucket2 in cache2.items():
+        file_path, symbol_type = key
+        for symbol_name, symbol_size_list in bucket2.items():
+            for (symbol_size, shared) in symbol_size_list:
+                delta_info = DeltaInfo(file_path, symbol_type, symbol_name,
+                                       shared)
+                delta_info.new_size = symbol_size
+                added.append(delta_info)
+    return (added, removed, changed, unchanged)
+
+
+def DeltaStr(number):
+    """Returns the number as a string with a '+' prefix if it's > 0 and
+  a '-' prefix if it's < 0."""
+    result = str(number)
+    if number > 0:
+        result = '+' + result
+    return result
+
+
+def SharedInfoStr(symbol_info):
+    """Returns a string (prefixed by space) explaining that numbers are
+  adjusted because of shared space between symbols, or an empty string
+  if space had not been shared."""
+
+    if symbol_info.shares_space_with_other_symbols:
+        return " (adjusted sizes because of memory sharing)"
+
+    return ""
+
+
+class CrunchStatsData(object):
+    """Stores a summary of data of a certain kind."""
+
+    def __init__(self, symbols):
+        self.symbols = symbols
+        self.sources = set()
+        self.before_size = 0
+        self.after_size = 0
+        self.symbols_by_path = {}
+
+
+def CrunchStats(added, removed, changed, unchanged, showsources, showsymbols):
+    """Outputs to stdout a summary of changes based on the symbol lists."""
+    # Split changed into grown and shrunk because that is easier to
+    # discuss.
+    grown = []
+    shrunk = []
+    for item in changed:
+        if item.old_size < item.new_size:
+            grown.append(item)
+        else:
+            shrunk.append(item)
+
+    new_symbols = CrunchStatsData(added)
+    removed_symbols = CrunchStatsData(removed)
+    grown_symbols = CrunchStatsData(grown)
+    shrunk_symbols = CrunchStatsData(shrunk)
+    sections = [new_symbols, removed_symbols, grown_symbols, shrunk_symbols]
+    for section in sections:
+        for item in section.symbols:
+            section.sources.add(item.file_path)
+            if item.old_size is not None:
+                section.before_size += item.old_size
+            if item.new_size is not None:
+                section.after_size += item.new_size
+            bucket = section.symbols_by_path.setdefault(item.file_path, [])
+            bucket.append((item.symbol_name, item.symbol_type,
+                           item.ExtractSymbolDelta()))
+
+    total_change = sum(s.after_size - s.before_size for s in sections)
+    summary = 'Total change: %s bytes' % DeltaStr(total_change)
+    print(summary)
+    print('=' * len(summary))
+    for section in sections:
+        if not section.symbols:
+            continue
+        if section.before_size == 0:
+            description = (
+                'added, totalling %s bytes' % DeltaStr(section.after_size))
+        elif section.after_size == 0:
+            description = (
+                'removed, totalling %s bytes' % DeltaStr(-section.before_size))
+        else:
+            if section.after_size > section.before_size:
+                type_str = 'grown'
+            else:
+                type_str = 'shrunk'
+            description = (
+                '%s, for a net change of %s bytes '
+                '(%d bytes before, %d bytes after)' %
+                (type_str, DeltaStr(section.after_size - section.before_size),
+                 section.before_size, section.after_size))
+        print('  %d %s across %d sources' % (len(section.symbols), description,
+                                             len(section.sources)))
+
+    maybe_unchanged_sources = set()
+    unchanged_symbols_size = 0
+    for item in unchanged:
+        maybe_unchanged_sources.add(item.file_path)
+        unchanged_symbols_size += item.old_size  # == item.new_size
+    print('  %d unchanged, totalling %d bytes' % (len(unchanged),
+                                                  unchanged_symbols_size))
+
+    # High level analysis, always output.
+    unchanged_sources = maybe_unchanged_sources
+    for section in sections:
+        unchanged_sources = unchanged_sources - section.sources
+    new_sources = (
+        new_symbols.sources - maybe_unchanged_sources - removed_symbols.sources)
+    removed_sources = (
+        removed_symbols.sources - maybe_unchanged_sources - new_symbols.sources)
+    partially_changed_sources = (
+        grown_symbols.sources | shrunk_symbols.sources | new_symbols.sources |
+        removed_symbols.sources) - removed_sources - new_sources
+    allFiles = set()
+    for section in sections:
+        allFiles = allFiles | section.sources
+    allFiles = allFiles | maybe_unchanged_sources
+    print('Source stats:')
+    print('  %d sources encountered.' % len(allFiles))
+    print('  %d completely new.' % len(new_sources))
+    print('  %d removed completely.' % len(removed_sources))
+    print('  %d partially changed.' % len(partially_changed_sources))
+    print('  %d completely unchanged.' % len(unchanged_sources))
+    remainder = (allFiles - new_sources - removed_sources -
+                 partially_changed_sources - unchanged_sources)
+    assert len(remainder) == 0
+
+    if not showsources:
+        return  # Per-source analysis, only if requested
+    print('Per-source Analysis:')
+    delta_by_path = {}
+    for section in sections:
+        for path in section.symbols_by_path:
+            entry = delta_by_path.get(path)
+            if not entry:
+                entry = {'plus': 0, 'minus': 0}
+                delta_by_path[path] = entry
+            for symbol_name, symbol_type, symbol_delta in \
+                  section.symbols_by_path[path]:
+                if symbol_delta.old_size is None:
+                    delta = symbol_delta.new_size
+                elif symbol_delta.new_size is None:
+                    delta = -symbol_delta.old_size
+                else:
+                    delta = symbol_delta.new_size - symbol_delta.old_size
+
+                if delta > 0:
+                    entry['plus'] += delta
+                else:
+                    entry['minus'] += (-1 * delta)
+
+    def delta_sort_key(item):
+        _path, size_data = item
+        growth = size_data['plus'] - size_data['minus']
+        return growth
+
+    for path, size_data in sorted(delta_by_path.items(),
+                                  key=delta_sort_key,
+                                  reverse=True):
+        gain = size_data['plus']
+        loss = size_data['minus']
+        delta = size_data['plus'] - size_data['minus']
+        header = ' %s - Source: %s - (gained %d, lost %d)' % (DeltaStr(delta),
+                                                              path, gain, loss)
+        divider = '-' * len(header)
+        print('')
+        print(divider)
+        print(header)
+        print(divider)
+        if showsymbols:
+
+            def ExtractNewSize(tup):
+                symbol_delta = tup[2]
+                return symbol_delta.new_size
+
+            def ExtractOldSize(tup):
+                symbol_delta = tup[2]
+                return symbol_delta.old_size
+
+            if path in new_symbols.symbols_by_path:
+                print('  New symbols:')
+                for symbol_name, symbol_type, symbol_delta in \
+                    sorted(new_symbols.symbols_by_path[path],
+                           key=ExtractNewSize,
+                           reverse=True):
+                    print('   %8s: %s type=%s, size=%d bytes%s' %
+                          (DeltaStr(symbol_delta.new_size), symbol_name,
+                           symbol_type, symbol_delta.new_size,
+                           SharedInfoStr(symbol_delta)))
+            if path in removed_symbols.symbols_by_path:
+                print('  Removed symbols:')
+                for symbol_name, symbol_type, symbol_delta in \
+                    sorted(removed_symbols.symbols_by_path[path],
+                           key=ExtractOldSize):
+                    print('   %8s: %s type=%s, size=%d bytes%s' %
+                          (DeltaStr(-symbol_delta.old_size), symbol_name,
+                           symbol_type, symbol_delta.old_size,
+                           SharedInfoStr(symbol_delta)))
+            for (changed_symbols_by_path,
+                 type_str) in [(grown_symbols.symbols_by_path, "Grown"),
+                               (shrunk_symbols.symbols_by_path, "Shrunk")]:
+                if path in changed_symbols_by_path:
+                    print('  %s symbols:' % type_str)
+
+                    def changed_symbol_sortkey(item):
+                        symbol_name, _symbol_type, symbol_delta = item
+                        return (symbol_delta.old_size - symbol_delta.new_size,
+                                symbol_name)
+
+                    for symbol_name, symbol_type, symbol_delta in \
+                        sorted(changed_symbols_by_path[path], key=changed_symbol_sortkey):
+                        print(
+                            '   %8s: %s type=%s, (was %d bytes, now %d bytes)%s'
+                            % (DeltaStr(symbol_delta.new_size -
+                                        symbol_delta.old_size), symbol_name,
+                               symbol_type,
+                               symbol_delta.old_size, symbol_delta.new_size,
+                               SharedInfoStr(symbol_delta)))
+
+
+def main():
+    usage = """%prog [options]
+
+  Analyzes the symbolic differences between two binary files
+  (typically, not necessarily, two different builds of the same
+  library) and produces a detailed description of symbols that have
+  been added, removed, or whose size has changed.
+
+  Example:
+       explain_binary_size_delta.py --nm1 /tmp/nm1.dump --nm2 /tmp/nm2.dump
+
+  Options are available via '--help'.
+  """
+    parser = optparse.OptionParser(usage=usage)
+    parser.add_option(
+        '--nm1', metavar='PATH', help='the nm dump of the first library')
+    parser.add_option(
+        '--nm2', metavar='PATH', help='the nm dump of the second library')
+    parser.add_option(
+        '--showsources',
+        action='store_true',
+        default=False,
+        help='show per-source statistics')
+    parser.add_option(
+        '--showsymbols',
+        action='store_true',
+        default=False,
+        help='show all symbol information; implies --showsources')
+    parser.add_option(
+        '--verbose',
+        action='store_true',
+        default=False,
+        help='output internal debugging stuff')
+    opts, _args = parser.parse_args()
+
+    if not opts.nm1:
+        parser.error('--nm1 is required')
+    if not opts.nm2:
+        parser.error('--nm2 is required')
+    symbols = []
+    for path in [opts.nm1, opts.nm2]:
+        with open(path, 'r') as nm_input:
+            if opts.verbose:
+                print('parsing ' + path + '...')
+            symbols.append(list(binary_size_utils.ParseNm(nm_input)))
+    (added, removed, changed, unchanged) = Compare(symbols[0], symbols[1])
+    CrunchStats(added, removed, changed, unchanged,
+                opts.showsources | opts.showsymbols, opts.showsymbols)
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/runtime/third_party/binary_size/src/run_binary_size_analysis.py b/runtime/third_party/binary_size/src/run_binary_size_analysis.py
new file mode 100755
index 0000000..dfd5b97
--- /dev/null
+++ b/runtime/third_party/binary_size/src/run_binary_size_analysis.py
@@ -0,0 +1,710 @@
+#!/usr/bin/env python3
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Generate a spatial analysis against an arbitrary library.
+
+To use, build the 'binary_size_tool' target. Then run this tool, passing
+in the location of the library to be analyzed along with any other options
+you desire.
+"""
+
+import json
+import logging
+import multiprocessing
+import optparse
+import os
+import re
+import shutil
+import struct
+import subprocess
+import sys
+import tempfile
+import time
+
+import binary_size_utils
+import elf_symbolizer
+
+# Node dictionary keys. These are output in json read by the webapp so
+# keep them short to save file size.
+# Note: If these change, the webapp must also change.
+NODE_TYPE_KEY = 'k'
+NODE_NAME_KEY = 'n'
+NODE_CHILDREN_KEY = 'children'
+NODE_SYMBOL_TYPE_KEY = 't'
+NODE_SYMBOL_SIZE_KEY = 'value'
+NODE_MAX_DEPTH_KEY = 'maxDepth'
+NODE_LAST_PATH_ELEMENT_KEY = 'lastPathElement'
+
+# The display name of the bucket where we put symbols without path.
+NAME_NO_PATH_BUCKET = '(No Path)'
+
+# Try to keep data buckets smaller than this to avoid killing the
+# graphing lib.
+BIG_BUCKET_LIMIT = 3000
+
+
+def _MkChild(node, name):
+    child = node[NODE_CHILDREN_KEY].get(name)
+    if child is None:
+        child = {NODE_NAME_KEY: name, NODE_CHILDREN_KEY: {}}
+        node[NODE_CHILDREN_KEY][name] = child
+    return child
+
+
+def SplitNoPathBucket(node):
+    """NAME_NO_PATH_BUCKET can be too large for the graphing lib to
+  handle. Split it into sub-buckets in that case."""
+    root_children = node[NODE_CHILDREN_KEY]
+    if NAME_NO_PATH_BUCKET in root_children:
+        no_path_bucket = root_children[NAME_NO_PATH_BUCKET]
+        old_children = no_path_bucket[NODE_CHILDREN_KEY]
+        count = 0
+        for symbol_type, symbol_bucket in old_children.items():
+            count += len(symbol_bucket[NODE_CHILDREN_KEY])
+        if count > BIG_BUCKET_LIMIT:
+            new_children = {}
+            no_path_bucket[NODE_CHILDREN_KEY] = new_children
+            current_bucket = None
+            index = 0
+            for symbol_type, symbol_bucket in old_children.items():
+                for symbol_name, value in symbol_bucket[
+                        NODE_CHILDREN_KEY].items():
+                    if index % BIG_BUCKET_LIMIT == 0:
+                        group_no = (index / BIG_BUCKET_LIMIT) + 1
+                        current_bucket = _MkChild(
+                            no_path_bucket,
+                            '%s subgroup %d' % (NAME_NO_PATH_BUCKET, group_no))
+                        assert not NODE_TYPE_KEY in node or node[
+                            NODE_TYPE_KEY] == 'p'
+                        node[NODE_TYPE_KEY] = 'p'  # p for path
+                    index += 1
+                    symbol_size = value[NODE_SYMBOL_SIZE_KEY]
+                    AddSymbolIntoFileNode(current_bucket, symbol_type,
+                                          symbol_name, symbol_size)
+
+
+def MakeChildrenDictsIntoLists(node):
+    largest_list_len = 0
+    if NODE_CHILDREN_KEY in node:
+        largest_list_len = len(node[NODE_CHILDREN_KEY])
+        child_list = []
+        for child in node[NODE_CHILDREN_KEY].values():
+            child_largest_list_len = MakeChildrenDictsIntoLists(child)
+            if child_largest_list_len > largest_list_len:
+                largest_list_len = child_largest_list_len
+            child_list.append(child)
+        node[NODE_CHILDREN_KEY] = child_list
+
+    return largest_list_len
+
+
+def AddSymbolIntoFileNode(node, symbol_type, symbol_name, symbol_size):
+    """Puts symbol into the file path node |node|.
+  Returns the number of added levels in tree. I.e. returns 2."""
+
+    # 'node' is the file node and first step is to find its symbol-type bucket.
+    node[NODE_LAST_PATH_ELEMENT_KEY] = True
+    node = _MkChild(node, symbol_type)
+    assert not NODE_TYPE_KEY in node or node[NODE_TYPE_KEY] == 'b'
+    node[NODE_SYMBOL_TYPE_KEY] = symbol_type
+    node[NODE_TYPE_KEY] = 'b'  # b for bucket
+
+    # 'node' is now the symbol-type bucket. Make the child entry.
+    node = _MkChild(node, symbol_name)
+    if NODE_CHILDREN_KEY in node:
+        if node[NODE_CHILDREN_KEY]:
+            logging.warning(
+                'A container node used as symbol for %s.' % symbol_name)
+        # This is going to be used as a leaf so no use for child list.
+        del node[NODE_CHILDREN_KEY]
+    node[NODE_SYMBOL_SIZE_KEY] = symbol_size
+    node[NODE_SYMBOL_TYPE_KEY] = symbol_type
+    node[NODE_TYPE_KEY] = 's'  # s for symbol
+
+    return 2  # Depth of the added subtree.
+
+
+def MakeCompactTree(symbols, symbol_path_origin_dir):
+    result = {
+        NODE_NAME_KEY: '/',
+        NODE_CHILDREN_KEY: {},
+        NODE_TYPE_KEY: 'p',
+        NODE_MAX_DEPTH_KEY: 0
+    }
+    seen_symbol_with_path = False
+    cwd = os.path.abspath(os.getcwd())
+    for symbol_name, symbol_type, symbol_size, file_path, _address in symbols:
+
+        if 'vtable for ' in symbol_name:
+            symbol_type = '@'  # hack to categorize these separately
+        # Take path like '/foo/bar/baz', convert to ['foo', 'bar', 'baz']
+        if file_path and file_path != "??":
+            file_path = os.path.abspath(
+                os.path.join(symbol_path_origin_dir, file_path))
+            # Let the output structure be relative to $CWD if inside $CWD,
+            # otherwise relative to the disk root. This is to avoid
+            # unnecessary click-through levels in the output.
+            if file_path.startswith(cwd + os.sep):
+                file_path = file_path[len(cwd):]
+            if file_path.startswith('/'):
+                file_path = file_path[1:]
+            seen_symbol_with_path = True
+        else:
+            file_path = NAME_NO_PATH_BUCKET
+
+        path_parts = file_path.split('/')
+
+        # Find pre-existing node in tree, or update if it already exists
+        node = result
+        depth = 0
+        while len(path_parts) > 0:
+            path_part = path_parts.pop(0)
+            if len(path_part) == 0:
+                continue
+            depth += 1
+            node = _MkChild(node, path_part)
+            assert not NODE_TYPE_KEY in node or node[NODE_TYPE_KEY] == 'p'
+            node[NODE_TYPE_KEY] = 'p'  # p for path
+
+        depth += AddSymbolIntoFileNode(node, symbol_type, symbol_name,
+                                       symbol_size)
+        result[NODE_MAX_DEPTH_KEY] = max(result[NODE_MAX_DEPTH_KEY], depth)
+
+    if not seen_symbol_with_path:
+        logging.warning('Symbols lack paths. Data will not be structured.')
+
+    # The (no path) bucket can be extremely large if we failed to get
+    # path information. Split it into subgroups if needed.
+    SplitNoPathBucket(result)
+
+    largest_list_len = MakeChildrenDictsIntoLists(result)
+
+    if largest_list_len > BIG_BUCKET_LIMIT:
+        logging.warning('There are sections with %d nodes. '
+                        'Results might be unusable.' % largest_list_len)
+    return result
+
+
+def DumpCompactTree(symbols, symbol_path_origin_dir, outfile):
+    tree_root = MakeCompactTree(symbols, symbol_path_origin_dir)
+    with open(outfile, 'w') as out:
+        out.write('var tree_data=')
+        # Use separators without whitespace to get a smaller file.
+        json.dump(tree_root, out, separators=(',', ':'))
+    print('Writing %d bytes json' % os.path.getsize(outfile))
+
+
+def MakeSourceMap(symbols):
+    sources = {}
+    for _sym, _symbol_type, size, path, _address in symbols:
+        key = None
+        if path:
+            key = os.path.normpath(path)
+        else:
+            key = '[no path]'
+        if key not in sources:
+            sources[key] = {'path': path, 'symbol_count': 0, 'size': 0}
+        record = sources[key]
+        record['size'] += size
+        record['symbol_count'] += 1
+    return sources
+
+
+# Regex for parsing "nm" output. A sample line looks like this:
+# 0167b39c 00000018 t ACCESS_DESCRIPTION_free /path/file.c:95
+#
+# The fields are: address, size, type, name, source location
+# Regular expression explained ( see also: https://xkcd.com/208 ):
+# ([0-9a-f]{8,}+)   The address
+# [\s]+             Whitespace separator
+# ([0-9a-f]{8,}+)   The size. From here on out it's all optional.
+# [\s]+             Whitespace separator
+# (\S?)             The symbol type, which is any non-whitespace char
+# [\s*]             Whitespace separator
+# ([^\t]*)          Symbol name, any non-tab character (spaces ok!)
+# [\t]?             Tab separator
+# (.*)              The location (filename[:linennum|?][ (discriminator n)]
+sNmPattern = re.compile(
+    r'([0-9a-f]{8,})[\s]+([0-9a-f]{8,})[\s]*(\S?)[\s*]([^\t]*)[\t]?(.*)')
+
+
+class Progress():
+
+    def __init__(self):
+        self.count = 0
+        self.skip_count = 0
+        self.collisions = 0
+        self.time_last_output = time.time()
+        self.count_last_output = 0
+        self.disambiguations = 0
+        self.was_ambiguous = 0
+
+
+def RunElfSymbolizer(outfile, library, addr2line_binary, nm_binary, jobs,
+                     disambiguate, src_path):
+    nm_output = RunNm(library, nm_binary)
+    nm_output_lines = nm_output.splitlines()
+    nm_output_lines_len = len(nm_output_lines)
+    address_symbol = {}
+    progress = Progress()
+
+    def map_address_symbol(symbol, addr):
+        progress.count += 1
+        if addr in address_symbol:
+            # 'Collision between %s and %s.' % (str(symbol.name),
+            #                                   str(address_symbol[addr].name))
+            progress.collisions += 1
+        else:
+            if symbol.disambiguated:
+                progress.disambiguations += 1
+            if symbol.was_ambiguous:
+                progress.was_ambiguous += 1
+
+            address_symbol[addr] = symbol
+
+        progress_output()
+
+    def progress_output():
+        progress_chunk = 100
+        if progress.count % progress_chunk == 0:
+            time_now = time.time()
+            time_spent = time_now - progress.time_last_output
+            if time_spent > 1.0:
+                # Only output at most once per second.
+                progress.time_last_output = time_now
+                chunk_size = progress.count - progress.count_last_output
+                progress.count_last_output = progress.count
+                if time_spent > 0:
+                    speed = chunk_size / time_spent
+                else:
+                    speed = 0
+                progress_percent = (100.0 * (
+                    progress.count + progress.skip_count) / nm_output_lines_len)
+                disambiguation_percent = 0
+                if progress.disambiguations != 0:
+                    disambiguation_percent = (100.0 * progress.disambiguations /
+                                              progress.was_ambiguous)
+
+                sys.stdout.write(
+                    '\r%.1f%%: Looked up %d symbols (%d collisions, '
+                    '%d disambiguations where %.1f%% succeeded)'
+                    ' - %.1f lookups/s.' %
+                    (progress_percent, progress.count, progress.collisions,
+                     progress.disambiguations, disambiguation_percent, speed))
+
+    # In case disambiguation was disabled, we remove the source path (which upon
+    # being set signals the symbolizer to enable disambiguation)
+    if not disambiguate:
+        src_path = None
+    symbolizer = elf_symbolizer.ELFSymbolizer(
+        library,
+        addr2line_binary,
+        map_address_symbol,
+        max_concurrent_jobs=jobs,
+        source_root_path=src_path)
+    user_interrupted = False
+    try:
+        for binary_line in nm_output_lines:
+            line = binary_line.decode()
+            match = sNmPattern.match(line)
+            if match:
+                location = match.group(5)
+                if not location:
+                    addr = int(match.group(1), 16)
+                    size = int(match.group(2), 16)
+                    if addr in address_symbol:  # Already looked up, shortcut
+                        # ELFSymbolizer.
+                        map_address_symbol(address_symbol[addr], addr)
+                        continue
+                    elif size == 0:
+                        # Save time by not looking up empty symbols (do they even exist?)
+                        print('Empty symbol: ' + line)
+                    else:
+                        symbolizer.SymbolizeAsync(addr, addr)
+                        continue
+
+            progress.skip_count += 1
+    except KeyboardInterrupt:
+        user_interrupted = True
+        print('Interrupting - killing subprocesses. Please wait.')
+
+    try:
+        symbolizer.Join()
+    except KeyboardInterrupt:
+        # Don't want to abort here since we will be finished in a few seconds.
+        user_interrupted = True
+        print('Patience you must have my young padawan.')
+
+    print('')
+
+    if user_interrupted:
+        print('Skipping the rest of the file mapping. '
+              'Output will not be fully classified.')
+
+    symbol_path_origin_dir = os.path.dirname(os.path.abspath(library))
+
+    with open(outfile, 'w') as out:
+        for binary_line in nm_output_lines:
+            line = binary_line.decode()
+            match = sNmPattern.match(line)
+            if match:
+                location = match.group(5)
+                if not location:
+                    addr = int(match.group(1), 16)
+                    symbol = address_symbol.get(addr)
+                    if symbol is not None:
+                        path = '??'
+                        if symbol.source_path is not None:
+                            path = os.path.abspath(
+                                os.path.join(symbol_path_origin_dir,
+                                             symbol.source_path))
+                        line_number = 0
+                        if symbol.source_line is not None:
+                            line_number = symbol.source_line
+                        out.write('%s\t%s:%d\n' % (line, path, line_number))
+                        continue
+
+            out.write('%s\n' % line)
+
+    print('%d symbols in the results.' % len(address_symbol))
+
+
+def RunNm(binary, nm_binary):
+    cmd = [
+        nm_binary, '-C', '--print-size', '--size-sort', '--reverse-sort', binary
+    ]
+    nm_process = subprocess.Popen(
+        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    (process_output, err_output) = nm_process.communicate()
+
+    if nm_process.returncode != 0:
+        if err_output:
+            raise Exception(err_output)
+        else:
+            raise Exception(process_output)
+
+    return process_output
+
+
+def GetNmSymbols(nm_infile, outfile, library, jobs, verbose, addr2line_binary,
+                 nm_binary, disambiguate, src_path):
+    if nm_infile is None:
+        if outfile is None:
+            outfile = tempfile.NamedTemporaryFile(delete=False).name
+
+        if verbose:
+            print('Running parallel addr2line, dumping symbols to ' + outfile)
+        RunElfSymbolizer(outfile, library, addr2line_binary, nm_binary, jobs,
+                         disambiguate, src_path)
+
+        nm_infile = outfile
+
+    elif verbose:
+        print('Using nm input from ' + nm_infile)
+    with open(nm_infile, 'r') as infile:
+        return list(binary_size_utils.ParseNm(infile))
+
+
+PAK_RESOURCE_ID_TO_STRING = {"inited": False}
+
+
+def LoadPakIdsFromResourceFile(filename):
+    """Given a file name, it loads everything that looks like a resource id
+  into PAK_RESOURCE_ID_TO_STRING."""
+    with open(filename) as resource_header:
+        for line in resource_header:
+            if line.startswith("#define "):
+                line_data = line.split()
+                if len(line_data) == 3:
+                    try:
+                        resource_number = int(line_data[2])
+                        resource_name = line_data[1]
+                        PAK_RESOURCE_ID_TO_STRING[
+                            resource_number] = resource_name
+                    except ValueError:
+                        pass
+
+
+def GetReadablePakResourceName(pak_file, resource_id):
+    """Pak resources have a numeric identifier. It is not helpful when
+  trying to locate where footprint is generated. This does its best to
+  map the number to a usable string."""
+    if not PAK_RESOURCE_ID_TO_STRING['inited']:
+        # Try to find resource header files generated by grit when
+        # building the pak file. We'll look for files named *resources.h"
+        # and lines of the type:
+        #    #define MY_RESOURCE_JS 1234
+        PAK_RESOURCE_ID_TO_STRING['inited'] = True
+        gen_dir = os.path.join(os.path.dirname(pak_file), 'gen')
+        if os.path.isdir(gen_dir):
+            for dirname, _dirs, files in os.walk(gen_dir):
+                for filename in files:
+                    if filename.endswith('resources.h'):
+                        LoadPakIdsFromResourceFile(
+                            os.path.join(dirname, filename))
+    return PAK_RESOURCE_ID_TO_STRING.get(resource_id,
+                                         'Pak Resource %d' % resource_id)
+
+
+def AddPakData(symbols, pak_file):
+    """Adds pseudo-symbols from a pak file."""
+    pak_file = os.path.abspath(pak_file)
+    with open(pak_file, 'rb') as pak:
+        data = pak.read()
+
+    PAK_FILE_VERSION = 4
+    HEADER_LENGTH = 2 * 4 + 1  # Two uint32s. (file version, number of entries)
+    # and one uint8 (encoding of text resources)
+    INDEX_ENTRY_SIZE = 2 + 4  # Each entry is a uint16 and a uint32.
+    version, num_entries, _encoding = struct.unpack('<IIB',
+                                                    data[:HEADER_LENGTH])
+    assert version == PAK_FILE_VERSION, (
+        'Unsupported pak file '
+        'version (%d) in %s. Only '
+        'support version %d' % (version, pak_file, PAK_FILE_VERSION))
+    if num_entries > 0:
+        # Read the index and data.
+        data = data[HEADER_LENGTH:]
+        for _ in range(num_entries):
+            resource_id, offset = struct.unpack('<HI', data[:INDEX_ENTRY_SIZE])
+            data = data[INDEX_ENTRY_SIZE:]
+            _next_id, next_offset = struct.unpack('<HI',
+                                                  data[:INDEX_ENTRY_SIZE])
+            resource_size = next_offset - offset
+
+            symbol_name = GetReadablePakResourceName(pak_file, resource_id)
+            symbol_path = pak_file
+            symbol_type = 'd'  # Data. Approximation.
+            symbol_size = resource_size
+            symbols.append((symbol_name, symbol_type, symbol_size, symbol_path))
+
+
+def _find_in_system_path(binary):
+    """Locate the full path to binary in the system path or return None
+  if not found."""
+    system_path = os.environ["PATH"].split(os.pathsep)
+    for path in system_path:
+        binary_path = os.path.join(path, binary)
+        if os.path.isfile(binary_path):
+            return binary_path
+    return None
+
+
+def CheckDebugFormatSupport(library, addr2line_binary):
+    """Kills the program if debug data is in an unsupported format.
+
+  There are two common versions of the DWARF debug formats and
+  since we are right now transitioning from DWARF2 to newer formats,
+  it's possible to have a mix of tools that are not compatible. Detect
+  that and abort rather than produce meaningless output."""
+    tool_output = subprocess.check_output([addr2line_binary,
+                                           '--version']).decode()
+    version_re = re.compile(r'^GNU [^ ]+ .* (\d+).(\d+).*?$', re.M)
+    parsed_output = version_re.match(tool_output)
+    major = int(parsed_output.group(1))
+    minor = int(parsed_output.group(2))
+    supports_dwarf4 = major > 2 or major == 2 and minor > 22
+
+    if supports_dwarf4:
+        return
+
+    print('Checking version of debug information in %s.' % library)
+    debug_info = subprocess.check_output(
+        ['readelf', '--debug-dump=info', '--dwarf-depth=1', library])
+    dwarf_version_re = re.compile(r'^\s+Version:\s+(\d+)$', re.M)
+    parsed_dwarf_format_output = dwarf_version_re.search(debug_info)
+    version = int(parsed_dwarf_format_output.group(1))
+    if version > 2:
+        print(
+            'The supplied tools only support DWARF2 debug data but the binary\n'
+            + 'uses DWARF%d. Update the tools or compile the binary\n' % version
+            + 'with -gdwarf-2.')
+        sys.exit(1)
+
+
+def main():
+    usage = """%prog [options]
+
+  Runs a spatial analysis on a given library, looking up the source locations
+  of its symbols and calculating how much space each directory, source file,
+  and so on is taking. The result is a report that can be used to pinpoint
+  sources of large portions of the binary, etceteras.
+
+  Under normal circumstances, you only need to pass two arguments, thusly:
+
+      %prog --library /path/to/library --destdir /path/to/output
+
+  In this mode, the program will dump the symbols from the specified library
+  and map those symbols back to source locations, producing a web-based
+  report in the specified output directory.
+
+  Other options are available via '--help'.
+  """
+    parser = optparse.OptionParser(usage=usage)
+    parser.add_option(
+        '--nm-in',
+        metavar='PATH',
+        help='if specified, use nm input from <path> instead of '
+        'generating it. Note that source locations should be '
+        'present in the file; i.e., no addr2line symbol lookups '
+        'will be performed when this option is specified. '
+        'Mutually exclusive with --library.')
+    parser.add_option(
+        '--destdir',
+        metavar='PATH',
+        help='write output to the specified directory. An HTML '
+        'report is generated here along with supporting files; '
+        'any existing report will be overwritten.')
+    parser.add_option(
+        '--library',
+        metavar='PATH',
+        help='if specified, process symbols in the library at '
+        'the specified path. Mutually exclusive with --nm-in.')
+    parser.add_option(
+        '--pak',
+        metavar='PATH',
+        help='if specified, includes the contents of the '
+        'specified *.pak file in the output.')
+    parser.add_option(
+        '--nm-binary',
+        help='use the specified nm binary to analyze library. '
+        'This is to be used when the nm in the path is not for '
+        'the right architecture or of the right version.')
+    parser.add_option(
+        '--addr2line-binary',
+        help='use the specified addr2line binary to analyze '
+        'library. This is to be used when the addr2line in '
+        'the path is not for the right architecture or '
+        'of the right version.')
+    parser.add_option(
+        '--jobs',
+        type='int',
+        help='number of jobs to use for the parallel '
+        'addr2line processing pool; defaults to 1. More '
+        'jobs greatly improve throughput but eat RAM like '
+        'popcorn, and take several gigabytes each. Start low '
+        'and ramp this number up until your machine begins to '
+        'struggle with RAM. '
+        'This argument is only valid when using --library.')
+    parser.add_option(
+        '-v',
+        '--verbose',
+        dest='verbose',
+        action='store_true',
+        help='be verbose, printing lots of status information.')
+    parser.add_option(
+        '--nm-out',
+        metavar='PATH',
+        help='(deprecated) No-op. nm.out is stored in --destdir.')
+    parser.add_option(
+        '--no-nm-out',
+        action='store_true',
+        help='do not keep the nm output file. This file is useful '
+        'if you want to see the fully processed nm output after '
+        'the symbols have been mapped to source locations, or if '
+        'you plan to run explain_binary_size_delta.py. By default '
+        'the file \'nm.out\' is placed alongside the generated '
+        'report. The nm.out file is only created when using '
+        '--library.')
+    parser.add_option(
+        '--disable-disambiguation',
+        action='store_true',
+        help='disables the disambiguation process altogether,'
+        ' NOTE: this may, depending on your toolchain, produce'
+        ' output with some symbols at the top layer if addr2line'
+        ' could not get the entire source path.')
+    parser.add_option(
+        '--source-path',
+        default='./',
+        help='the path to the source code of the output binary, '
+        'default set to current directory. Used in the'
+        ' disambiguation process.')
+    opts, _args = parser.parse_args()
+
+    if ((not opts.library) and
+        (not opts.nm_in)) or (opts.library and opts.nm_in):
+        parser.error('exactly one of --library or --nm-in is required')
+    if opts.nm_out:
+        print('WARNING: --nm-out is deprecated and has no effect.',
+              file=sys.stderr)
+    if (opts.nm_in):
+        if opts.jobs:
+            print('WARNING: --jobs has no effect when used with --nm-in',
+                  file=sys.stderr)
+    if not opts.destdir:
+        parser.error('--destdir is a required argument')
+    if not opts.jobs:
+        # Use the number of processors but cap between 2 and 4 since raw
+        # CPU power isn't the limiting factor. It's I/O limited, memory
+        # bus limited and available-memory-limited. Too many processes and
+        # the computer will run out of memory and it will be slow.
+        opts.jobs = max(2, min(4, multiprocessing.cpu_count()))
+
+    if opts.addr2line_binary:
+        assert os.path.isfile(opts.addr2line_binary)
+        addr2line_binary = opts.addr2line_binary
+    else:
+        addr2line_binary = _find_in_system_path('addr2line')
+        assert addr2line_binary, 'Unable to find addr2line in the path. '\
+            'Use --addr2line-binary to specify location.'
+
+    if opts.nm_binary:
+        assert os.path.isfile(opts.nm_binary)
+        nm_binary = opts.nm_binary
+    else:
+        nm_binary = _find_in_system_path('nm')
+        assert nm_binary, 'Unable to find nm in the path. Use --nm-binary '\
+            'to specify location.'
+
+    if opts.pak:
+        assert os.path.isfile(opts.pak), 'Could not find ' % opts.pak
+
+    print('addr2line: %s' % addr2line_binary)
+    print('nm: %s' % nm_binary)
+
+    if opts.library:
+        CheckDebugFormatSupport(opts.library, addr2line_binary)
+
+    # Prepare output directory and report guts
+    if not os.path.exists(opts.destdir):
+        os.makedirs(opts.destdir, 0o755)
+    nm_out = os.path.join(opts.destdir, 'nm.out')
+    if opts.no_nm_out:
+        nm_out = None
+
+    # Copy report boilerplate into output directory. This also proves that the
+    # output directory is safe for writing, so there should be no problems writing
+    # the nm.out file later.
+    data_js_file_name = os.path.join(opts.destdir, 'data.js')
+    d3_out = os.path.join(opts.destdir, 'd3')
+    if not os.path.exists(d3_out):
+        os.makedirs(d3_out, 0o755)
+    d3_src = os.path.join(os.path.dirname(__file__), '..', '..', 'd3', 'src')
+    template_src = os.path.join(os.path.dirname(__file__), 'template')
+    shutil.copy(os.path.join(d3_src, 'LICENSE'), d3_out)
+    shutil.copy(os.path.join(d3_src, 'd3.js'), d3_out)
+    shutil.copy(os.path.join(template_src, 'index.html'), opts.destdir)
+    shutil.copy(os.path.join(template_src, 'D3SymbolTreeMap.js'), opts.destdir)
+
+    # Run nm and/or addr2line to gather the data
+    symbols = GetNmSymbols(opts.nm_in, nm_out, opts.library, opts.jobs,
+                           opts.verbose is True, addr2line_binary, nm_binary,
+                           opts.disable_disambiguation is None,
+                           opts.source_path)
+
+    # Post-processing
+    if opts.pak:
+        AddPakData(symbols, opts.pak)
+    if opts.library:
+        symbol_path_origin_dir = os.path.dirname(os.path.abspath(opts.library))
+    else:
+        # Just a guess. Hopefully all paths in the input file are absolute.
+        symbol_path_origin_dir = os.path.abspath(os.getcwd())
+    # Dump JSON for the HTML report.
+    DumpCompactTree(symbols, symbol_path_origin_dir, data_js_file_name)
+    print('Report saved to ' + opts.destdir + '/index.html')
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/runtime/third_party/binary_size/src/template/D3SymbolTreeMap.js b/runtime/third_party/binary_size/src/template/D3SymbolTreeMap.js
new file mode 100644
index 0000000..88dd692
--- /dev/null
+++ b/runtime/third_party/binary_size/src/template/D3SymbolTreeMap.js
@@ -0,0 +1,938 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// TODO:
+// 1. Visibility functions: base on boxPadding.t, not 15
+// 2. Track a maxDisplayDepth that is user-settable:
+//    maxDepth == currentRoot.depth + maxDisplayDepth
+function D3SymbolTreeMap(mapWidth, mapHeight, levelsToShow) {
+  this._mapContainer = undefined;
+  this._mapWidth = mapWidth;
+  this._mapHeight = mapHeight;
+  this.boxPadding = {'l': 5, 'r': 5, 't': 20, 'b': 5};
+  this.infobox = undefined;
+  this._maskContainer = undefined;
+  this._highlightContainer = undefined;
+  // Transition in this order:
+  // 1. Exiting items go away.
+  // 2. Updated items move.
+  // 3. New items enter.
+  this._exitDuration=500;
+  this._updateDuration=500;
+  this._enterDuration=500;
+  this._firstTransition=true;
+  this._layout = undefined;
+  this._currentRoot = undefined;
+  this._currentNodes = undefined;
+  this._treeData = undefined;
+  this._maxLevelsToShow = levelsToShow;
+  this._currentMaxDepth = this._maxLevelsToShow;
+}
+
+/**
+ * Make a number pretty, with comma separators.
+ */
+D3SymbolTreeMap._pretty = function(num) {
+  var asString = String(num);
+  var result = '';
+  var counter = 0;
+  for (var x = asString.length - 1; x >= 0; x--) {
+    counter++;
+    if (counter === 4) {
+      result = ',' + result;
+      counter = 1;
+    }
+    result = asString.charAt(x) + result;
+  }
+  return result;
+}
+
+/**
+ * Express a number in terms of KiB, MiB, GiB, etc.
+ * Note that these are powers of 2, not of 10.
+ */
+D3SymbolTreeMap._byteify = function(num) {
+  var suffix;
+  if (num >= 1024) {
+    if (num >= 1024 * 1024 * 1024) {
+      suffix = 'GiB';
+      num = num / (1024 * 1024 * 1024);
+    } else if (num >= 1024 * 1024) {
+      suffix = 'MiB';
+      num = num / (1024 * 1024);
+    } else if (num >= 1024) {
+      suffix = 'KiB'
+      num = num / 1024;
+    }
+    return num.toFixed(2) + ' ' + suffix;
+  }
+  return num + ' B';
+}
+
+D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS = {
+  // Definitions concisely derived from the nm 'man' page
+  'A': 'Global absolute (A)',
+  'B': 'Global uninitialized data (B)',
+  'b': 'Local uninitialized data (b)',
+  'C': 'Global uninitialized common (C)',
+  'D': 'Global initialized data (D)',
+  'd': 'Local initialized data (d)',
+  'G': 'Global small initialized data (G)',
+  'g': 'Local small initialized data (g)',
+  'i': 'Indirect function (i)',
+  'N': 'Debugging (N)',
+  'p': 'Stack unwind (p)',
+  'R': 'Global read-only data (R)',
+  'r': 'Local read-only data (r)',
+  'S': 'Global small uninitialized data (S)',
+  's': 'Local small uninitialized data (s)',
+  'T': 'Global code (T)',
+  't': 'Local code (t)',
+  'U': 'Undefined (U)',
+  'u': 'Unique (u)',
+  'V': 'Global weak object (V)',
+  'v': 'Local weak object (v)',
+  'W': 'Global weak symbol (W)',
+  'w': 'Local weak symbol (w)',
+  '@': 'Vtable entry (@)', // non-standard, hack.
+  '-': 'STABS debugging (-)',
+  '?': 'Unrecognized (?)',
+};
+D3SymbolTreeMap._NM_SYMBOL_TYPES = '';
+for (var symbol_type in D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS) {
+  D3SymbolTreeMap._NM_SYMBOL_TYPES += symbol_type;
+}
+
+/**
+ * Given a symbol type code, look up and return a human-readable description
+ * of that symbol type. If the symbol type does not match one of the known
+ * types, the unrecognized description (corresponding to symbol type '?') is
+ * returned instead of null or undefined.
+ */
+D3SymbolTreeMap._getSymbolDescription = function(type) {
+  var result = D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS[type];
+  if (result === undefined) {
+    result = D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS['?'];
+  }
+  return result;
+}
+
+// Qualitative 12-value pastel Brewer palette.
+D3SymbolTreeMap._colorArray = [
+  'rgb(141,211,199)',
+  'rgb(255,255,179)',
+  'rgb(190,186,218)',
+  'rgb(251,128,114)',
+  'rgb(128,177,211)',
+  'rgb(253,180,98)',
+  'rgb(179,222,105)',
+  'rgb(252,205,229)',
+  'rgb(217,217,217)',
+  'rgb(188,128,189)',
+  'rgb(204,235,197)',
+  'rgb(255,237,111)'];
+
+D3SymbolTreeMap._initColorMap = function() {
+  var map = {};
+  var numColors = D3SymbolTreeMap._colorArray.length;
+  var count = 0;
+  for (var key in D3SymbolTreeMap._NM_SYMBOL_TYPE_DESCRIPTIONS) {
+    var index = count++ % numColors;
+    map[key] = d3.rgb(D3SymbolTreeMap._colorArray[index]);
+  }
+  D3SymbolTreeMap._colorMap = map;
+}
+D3SymbolTreeMap._initColorMap();
+
+D3SymbolTreeMap.getColorForType = function(type) {
+  var result = D3SymbolTreeMap._colorMap[type];
+  if (result === undefined) return d3.rgb('rgb(255,255,255)');
+  return result;
+}
+
+D3SymbolTreeMap.prototype.init = function() {
+  this.infobox = this._createInfoBox();
+  this._mapContainer = d3.select('body').append('div')
+      .style('position', 'relative')
+      .style('width', this._mapWidth)
+      .style('height', this._mapHeight)
+      .style('padding', 0)
+      .style('margin', 0)
+      .style('box-shadow', '5px 5px 5px #888');
+  this._layout = this._createTreeMapLayout();
+  this._setData(tree_data); // TODO: Don't use global 'tree_data'
+}
+
+/**
+ * Sets the data displayed by the treemap and layint out the map.
+ */
+D3SymbolTreeMap.prototype._setData = function(data) {
+  this._treeData = data;
+  console.time('_crunchStats');
+  this._crunchStats(data);
+  console.timeEnd('_crunchStats');
+  this._currentRoot = this._treeData;
+  this._currentNodes = this._layout.nodes(this._currentRoot);
+  this._currentMaxDepth = this._maxLevelsToShow;
+  this._doLayout();
+}
+
+/**
+ * Recursively traverses the entire tree starting from the specified node,
+ * computing statistics and recording metadata as it goes. Call this method
+ * only once per imported tree.
+ */
+D3SymbolTreeMap.prototype._crunchStats = function(node) {
+  var stack = [];
+  stack.idCounter = 0;
+  this._crunchStatsHelper(stack, node);
+}
+
+/**
+ * Invoke the specified visitor function on all data elements currently shown
+ * in the treemap including any and all of their children, starting at the
+ * currently-displayed root and descending recursively. The function will be
+ * passed the datum element representing each node. No traversal guarantees
+ * are made.
+ */
+D3SymbolTreeMap.prototype.visitFromDisplayedRoot = function(visitor) {
+  this._visit(this._currentRoot, visitor);
+}
+
+/**
+ * Helper function for visit functions.
+ */
+D3SymbolTreeMap.prototype._visit = function(datum, visitor) {
+  visitor.call(this, datum);
+  if (datum.children) for (var i = 0; i < datum.children.length; i++) {
+    this._visit(datum.children[i], visitor);
+  }
+}
+
+D3SymbolTreeMap.prototype._crunchStatsHelper = function(stack, node) {
+  // Only overwrite the node ID if it isn't already set.
+  // This allows stats to be crunched multiple times on subsets of data
+  // without breaking the data-to-ID bindings. New nodes get new IDs.
+  if (node.id === undefined) node.id = stack.idCounter++;
+  if (node.children === undefined) {
+    // Leaf node (symbol); accumulate stats.
+    for (var i = 0; i < stack.length; i++) {
+      var ancestor = stack[i];
+      if (!ancestor.symbol_stats) ancestor.symbol_stats = {};
+      if (ancestor.symbol_stats[node.t] === undefined) {
+        // New symbol type we haven't seen before, just record.
+        ancestor.symbol_stats[node.t] = {'count': 1,
+                                         'size': node.value};
+      } else {
+        // Existing symbol type, increment.
+        ancestor.symbol_stats[node.t].count++;
+        ancestor.symbol_stats[node.t].size += node.value;
+      }
+    }
+  } else for (var i = 0; i < node.children.length; i++) {
+    stack.push(node);
+    this._crunchStatsHelper(stack, node.children[i]);
+    stack.pop();
+  }
+}
+
+D3SymbolTreeMap.prototype._createTreeMapLayout = function() {
+  var result = d3.layout.treemap()
+      .padding([this.boxPadding.t, this.boxPadding.r,
+                this.boxPadding.b, this.boxPadding.l])
+      .size([this._mapWidth, this._mapHeight]);
+  return result;
+}
+
+D3SymbolTreeMap.prototype.resize = function(width, height) {
+  this._mapWidth = width;
+  this._mapHeight = height;
+  this._mapContainer.style('width', width).style('height', height);
+  this._layout.size([this._mapWidth, this._mapHeight]);
+  this._currentNodes = this._layout.nodes(this._currentRoot);
+  this._doLayout();
+}
+
+D3SymbolTreeMap.prototype._zoomDatum = function(datum) {
+  if (this._currentRoot === datum) return; // already here
+  this._hideHighlight(datum);
+  this._hideInfoBox(datum);
+  this._currentRoot = datum;
+  this._currentNodes = this._layout.nodes(this._currentRoot);
+  this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow;
+  console.log('zooming into datum ' + this._currentRoot.n);
+  this._doLayout();
+}
+
+D3SymbolTreeMap.prototype.setMaxLevels = function(levelsToShow) {
+  this._maxLevelsToShow = levelsToShow;
+  this._currentNodes = this._layout.nodes(this._currentRoot);
+  this._currentMaxDepth = this._currentRoot.depth + this._maxLevelsToShow;
+  console.log('setting max levels to show: ' + this._maxLevelsToShow);
+  this._doLayout();
+}
+
+/**
+ * Clone the specified tree, returning an independent copy of the data.
+ * Only the original attributes expected to exist prior to invoking
+ * _crunchStatsHelper are retained, with the exception of the 'id' attribute
+ * (which must be retained for proper transitions).
+ * If the optional filter parameter is provided, it will be called with 'this'
+ * set to this treemap instance and passed the 'datum' object as an argument.
+ * When specified, the copy will retain only the data for which the filter
+ * function returns true.
+ */
+D3SymbolTreeMap.prototype._clone = function(datum, filter) {
+  var trackingStats = false;
+  if (this.__cloneState === undefined) {
+    console.time('_clone');
+    trackingStats = true;
+    this.__cloneState = {'accepted': 0, 'rejected': 0,
+                         'forced': 0, 'pruned': 0};
+  }
+
+  // Must go depth-first. All parents of children that are accepted by the
+  // filter must be preserved!
+  var copy = {'n': datum.n, 'k': datum.k};
+  var childAccepted = false;
+  if (datum.children !== undefined) {
+    for (var i = 0; i < datum.children.length; i++) {
+      var copiedChild = this._clone(datum.children[i], filter);
+      if (copiedChild !== undefined) {
+        childAccepted = true; // parent must also be accepted.
+        if (copy.children === undefined) copy.children = [];
+        copy.children.push(copiedChild);
+      }
+    }
+  }
+
+  // Ignore nodes that don't match the filter, when present.
+  var accept = false;
+  if (childAccepted) {
+    // Parent of an accepted child must also be accepted.
+    this.__cloneState.forced++;
+    accept = true;
+  } else if (filter !== undefined && filter.call(this, datum) !== true) {
+    this.__cloneState.rejected++;
+  } else if (datum.children === undefined) {
+    // Accept leaf nodes that passed the filter
+    this.__cloneState.accepted++;
+    accept = true;
+  } else {
+    // Non-leaf node. If no children are accepted, prune it.
+    this.__cloneState.pruned++;
+  }
+
+  if (accept) {
+    if (datum.id !== undefined) copy.id = datum.id;
+    if (datum.lastPathElement !== undefined) {
+      copy.lastPathElement = datum.lastPathElement;
+    }
+    if (datum.t !== undefined) copy.t = datum.t;
+    if (datum.value !== undefined && datum.children === undefined) {
+      copy.value = datum.value;
+    }
+  } else {
+    // Discard the copy we were going to return
+    copy = undefined;
+  }
+
+  if (trackingStats === true) {
+    // We are the fist call in the recursive chain.
+    console.timeEnd('_clone');
+    var totalAccepted = this.__cloneState.accepted +
+                        this.__cloneState.forced;
+    console.log(
+        totalAccepted + ' nodes retained (' +
+        this.__cloneState.forced + ' forced by accepted children, ' +
+        this.__cloneState.accepted + ' accepted on their own merits), ' +
+        this.__cloneState.rejected + ' nodes (and their children) ' +
+                                     'filtered out,' +
+        this.__cloneState.pruned + ' nodes pruned because because no ' +
+                                   'children remained.');
+    delete this.__cloneState;
+  }
+  return copy;
+}
+
+D3SymbolTreeMap.prototype.filter = function(filter) {
+  // Ensure we have a copy of the original root.
+  if (this._backupTree === undefined) this._backupTree = this._treeData;
+  this._mapContainer.selectAll('div').remove();
+  this._setData(this._clone(this._backupTree, filter));
+}
+
+D3SymbolTreeMap.prototype._doLayout = function() {
+  console.time('_doLayout');
+  this._handleInodes();
+  this._handleLeaves();
+  this._firstTransition = false;
+  console.timeEnd('_doLayout');
+}
+
+D3SymbolTreeMap.prototype._highlightElement = function(datum, selection) {
+  this._showHighlight(datum, selection);
+}
+
+D3SymbolTreeMap.prototype._unhighlightElement = function(datum, selection) {
+  this._hideHighlight(datum, selection);
+}
+
+D3SymbolTreeMap.prototype._handleInodes = function() {
+  console.time('_handleInodes');
+  var thisTreeMap = this;
+  var inodes = this._currentNodes.filter(function(datum){
+    return (datum.depth <= thisTreeMap._currentMaxDepth) &&
+            datum.children !== undefined;
+  });
+  var cellsEnter = this._mapContainer.selectAll('div.inode')
+      .data(inodes, function(datum) { return datum.id; })
+      .enter()
+      .append('div').attr('class', 'inode').attr('id', function(datum){
+          return 'node-' + datum.id;});
+
+
+  // Define enter/update/exit for inodes
+  cellsEnter
+      .append('div')
+      .attr('class', 'rect inode_rect_entering')
+      .style('z-index', function(datum) { return datum.id * 2; })
+      .style('position', 'absolute')
+      .style('left', function(datum) { return datum.x; })
+      .style('top', function(datum){ return datum.y; })
+      .style('width', function(datum){ return datum.dx; })
+      .style('height', function(datum){ return datum.dy; })
+      .style('opacity', '0')
+      .style('border', '1px solid black')
+      .style('background-image', function(datum) {
+        return thisTreeMap._makeSymbolBucketBackgroundImage.call(
+               thisTreeMap, datum);
+      })
+      .style('background-color', function(datum) {
+        if (datum.t === undefined) return 'rgb(220,220,220)';
+        return D3SymbolTreeMap.getColorForType(datum.t).toString();
+      })
+      .on('mouseover', function(datum){
+        thisTreeMap._highlightElement.call(
+            thisTreeMap, datum, d3.select(this));
+        thisTreeMap._showInfoBox.call(thisTreeMap, datum);
+      })
+      .on('mouseout', function(datum){
+        thisTreeMap._unhighlightElement.call(
+            thisTreeMap, datum, d3.select(this));
+        thisTreeMap._hideInfoBox.call(thisTreeMap, datum);
+      })
+      .on('mousemove', function(){
+          thisTreeMap._moveInfoBox.call(thisTreeMap, event);
+      })
+      .on('dblclick', function(datum){
+        if (datum !== thisTreeMap._currentRoot) {
+          // Zoom into the selection
+          thisTreeMap._zoomDatum(datum);
+        } else if (datum.parent) {
+          console.log('event.shiftKey=' + event.shiftKey);
+          if (event.shiftKey === true) {
+            // Back to root
+            thisTreeMap._zoomDatum(thisTreeMap._treeData);
+          } else {
+            // Zoom out of the selection
+            thisTreeMap._zoomDatum(datum.parent);
+          }
+        }
+      });
+  cellsEnter
+      .append('div')
+      .attr('class', 'label inode_label_entering')
+      .style('z-index', function(datum) { return (datum.id * 2) + 1; })
+      .style('position', 'absolute')
+      .style('left', function(datum){ return datum.x; })
+      .style('top', function(datum){ return datum.y; })
+      .style('width', function(datum) { return datum.dx; })
+      .style('height', function(datum) { return thisTreeMap.boxPadding.t; })
+      .style('opacity', '0')
+      .style('pointer-events', 'none')
+      .style('-webkit-user-select', 'none')
+      .style('overflow', 'hidden') // required for ellipsis
+      .style('white-space', 'nowrap') // required for ellipsis
+      .style('text-overflow', 'ellipsis')
+      .style('text-align', 'center')
+      .style('vertical-align', 'top')
+      .style('visibility', function(datum) {
+        return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
+      })
+      .text(function(datum) {
+        var sizeish = ' [' + D3SymbolTreeMap._byteify(datum.value) + ']'
+        var text;
+        if (datum.k === 'b') { // bucket
+          if (datum === thisTreeMap._currentRoot) {
+            text = thisTreeMap.pathFor(datum) + ': '
+                + D3SymbolTreeMap._getSymbolDescription(datum.t)
+          } else {
+            text = D3SymbolTreeMap._getSymbolDescription(datum.t);
+          }
+        } else if (datum === thisTreeMap._currentRoot) {
+          // The top-most level should always show the complete path
+          text = thisTreeMap.pathFor(datum);
+        } else {
+          // Anything that isn't a bucket or a leaf (symbol) or the
+          // current root should just show its name.
+          text = datum.n;
+        }
+        return text + sizeish;
+      }
+  );
+
+  // Complicated transition logic:
+  // For nodes that are entering, we want to fade them in in-place AFTER
+  // any adjusting nodes have resized and moved around. That way, new nodes
+  // seamlessly appear in the right spot after their containers have resized
+  // and moved around.
+  // To do this we do some trickery:
+  // 1. Define a '_entering' class on the entering elements
+  // 2. Use this to select only the entering elements and apply the opacity
+  //    transition.
+  // 3. Use the same transition to drop the '_entering' suffix, so that they
+  //    will correctly update in later zoom/resize/whatever operations.
+  // 4. The update transition is achieved by selecting the elements without
+  //    the '_entering_' suffix and applying movement and resizing transition
+  //    effects.
+  this._mapContainer.selectAll('div.inode_rect_entering').transition()
+      .duration(thisTreeMap._enterDuration).delay(
+          this._firstTransition ? 0 : thisTreeMap._exitDuration +
+              thisTreeMap._updateDuration)
+      .attr('class', 'rect inode_rect')
+      .style('opacity', '1')
+  this._mapContainer.selectAll('div.inode_label_entering').transition()
+      .duration(thisTreeMap._enterDuration).delay(
+          this._firstTransition ? 0 : thisTreeMap._exitDuration +
+              thisTreeMap._updateDuration)
+      .attr('class', 'label inode_label')
+      .style('opacity', '1')
+  this._mapContainer.selectAll('div.inode_rect').transition()
+      .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
+      .style('opacity', '1')
+      .style('background-image', function(datum) {
+        return thisTreeMap._makeSymbolBucketBackgroundImage.call(
+            thisTreeMap, datum);
+      })
+      .style('left', function(datum) { return datum.x; })
+      .style('top', function(datum){ return datum.y; })
+      .style('width', function(datum){ return datum.dx; })
+      .style('height', function(datum){ return datum.dy; });
+  this._mapContainer.selectAll('div.inode_label').transition()
+      .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
+      .style('opacity', '1')
+      .style('visibility', function(datum) {
+        return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
+      })
+      .style('left', function(datum){ return datum.x; })
+      .style('top', function(datum){ return datum.y; })
+      .style('width', function(datum) { return datum.dx; })
+      .style('height', function(datum) { return thisTreeMap.boxPadding.t; })
+      .text(function(datum) {
+        var sizeish = ' [' + D3SymbolTreeMap._byteify(datum.value) + ']'
+        var text;
+        if (datum.k === 'b') {
+          if (datum === thisTreeMap._currentRoot) {
+            text = thisTreeMap.pathFor(datum) + ': ' +
+                D3SymbolTreeMap._getSymbolDescription(datum.t)
+          } else {
+            text = D3SymbolTreeMap._getSymbolDescription(datum.t);
+          }
+        } else if (datum === thisTreeMap._currentRoot) {
+          // The top-most level should always show the complete path
+          text = thisTreeMap.pathFor(datum);
+        } else {
+          // Anything that isn't a bucket or a leaf (symbol) or the
+          // current root should just show its name.
+          text = datum.n;
+        }
+        return text + sizeish;
+      });
+  var exit = this._mapContainer.selectAll('div.inode')
+      .data(inodes, function(datum) { return 'inode-' + datum.id; })
+      .exit();
+  exit.selectAll('div.inode_rect').transition().duration(
+      thisTreeMap._exitDuration).style('opacity', 0);
+  exit.selectAll('div.inode_label').transition().duration(
+      thisTreeMap._exitDuration).style('opacity', 0);
+  exit.transition().delay(thisTreeMap._exitDuration + 1).remove();
+
+  console.log(inodes.length + ' inodes layed out.');
+  console.timeEnd('_handleInodes');
+}
+
+D3SymbolTreeMap.prototype._handleLeaves = function() {
+  console.time('_handleLeaves');
+  var color_fn = d3.scale.category10();
+  var thisTreeMap = this;
+  var leaves = this._currentNodes.filter(function(datum){
+    return (datum.depth <= thisTreeMap._currentMaxDepth) &&
+        datum.children === undefined; });
+  var cellsEnter = this._mapContainer.selectAll('div.leaf')
+      .data(leaves, function(datum) { return datum.id; })
+      .enter()
+      .append('div').attr('class', 'leaf').attr('id', function(datum){
+        return 'node-' + datum.id;
+      });
+
+  // Define enter/update/exit for leaves
+  cellsEnter
+      .append('div')
+      .attr('class', 'rect leaf_rect_entering')
+      .style('z-index', function(datum) { return datum.id * 2; })
+      .style('position', 'absolute')
+      .style('left', function(datum){ return datum.x; })
+      .style('top', function(datum){ return datum.y; })
+      .style('width', function(datum){ return datum.dx; })
+      .style('height', function(datum){ return datum.dy; })
+      .style('opacity', '0')
+      .style('background-color', function(datum) {
+        if (datum.t === undefined) return 'rgb(220,220,220)';
+        return D3SymbolTreeMap.getColorForType(datum.t)
+            .darker(0.3).toString();
+      })
+      .style('border', '1px solid black')
+      .on('mouseover', function(datum){
+        thisTreeMap._highlightElement.call(
+            thisTreeMap, datum, d3.select(this));
+        thisTreeMap._showInfoBox.call(thisTreeMap, datum);
+      })
+      .on('mouseout', function(datum){
+        thisTreeMap._unhighlightElement.call(
+            thisTreeMap, datum, d3.select(this));
+        thisTreeMap._hideInfoBox.call(thisTreeMap, datum);
+      })
+      .on('mousemove', function(){ thisTreeMap._moveInfoBox.call(
+        thisTreeMap, event);
+      });
+  cellsEnter
+      .append('div')
+      .attr('class', 'label leaf_label_entering')
+      .style('z-index', function(datum) { return (datum.id * 2) + 1; })
+      .style('position', 'absolute')
+      .style('left', function(datum){ return datum.x; })
+      .style('top', function(datum){ return datum.y; })
+      .style('width', function(datum) { return datum.dx; })
+      .style('height', function(datum) { return datum.dy; })
+      .style('opacity', '0')
+      .style('pointer-events', 'none')
+      .style('-webkit-user-select', 'none')
+      .style('overflow', 'hidden') // required for ellipsis
+      .style('white-space', 'nowrap') // required for ellipsis
+      .style('text-overflow', 'ellipsis')
+      .style('text-align', 'center')
+      .style('vertical-align', 'middle')
+      .style('visibility', function(datum) {
+        return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
+      })
+      .text(function(datum) { return datum.n; });
+
+  // Complicated transition logic: See note in _handleInodes()
+  this._mapContainer.selectAll('div.leaf_rect_entering').transition()
+      .duration(thisTreeMap._enterDuration).delay(
+          this._firstTransition ? 0 : thisTreeMap._exitDuration +
+              thisTreeMap._updateDuration)
+      .attr('class', 'rect leaf_rect')
+      .style('opacity', '1')
+  this._mapContainer.selectAll('div.leaf_label_entering').transition()
+      .duration(thisTreeMap._enterDuration).delay(
+          this._firstTransition ? 0 : thisTreeMap._exitDuration +
+              thisTreeMap._updateDuration)
+      .attr('class', 'label leaf_label')
+      .style('opacity', '1')
+  this._mapContainer.selectAll('div.leaf_rect').transition()
+      .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
+      .style('opacity', '1')
+      .style('left', function(datum){ return datum.x; })
+      .style('top', function(datum){ return datum.y; })
+      .style('width', function(datum){ return datum.dx; })
+      .style('height', function(datum){ return datum.dy; });
+  this._mapContainer.selectAll('div.leaf_label').transition()
+      .duration(thisTreeMap._updateDuration).delay(thisTreeMap._exitDuration)
+      .style('opacity', '1')
+      .style('visibility', function(datum) {
+          return (datum.dx < 15 || datum.dy < 15) ? 'hidden' : 'visible';
+      })
+      .style('left', function(datum){ return datum.x; })
+      .style('top', function(datum){ return datum.y; })
+      .style('width', function(datum) { return datum.dx; })
+      .style('height', function(datum) { return datum.dy; });
+  var exit = this._mapContainer.selectAll('div.leaf')
+      .data(leaves, function(datum) { return 'leaf-' + datum.id; })
+      .exit();
+  exit.selectAll('div.leaf_rect').transition()
+      .duration(thisTreeMap._exitDuration)
+      .style('opacity', 0);
+  exit.selectAll('div.leaf_label').transition()
+      .duration(thisTreeMap._exitDuration)
+      .style('opacity', 0);
+  exit.transition().delay(thisTreeMap._exitDuration + 1).remove();
+
+  console.log(leaves.length + ' leaves layed out.');
+  console.timeEnd('_handleLeaves');
+}
+
+D3SymbolTreeMap.prototype._makeSymbolBucketBackgroundImage = function(datum) {
+  if (!(datum.t === undefined && datum.depth == this._currentMaxDepth)) {
+    return 'none';
+  }
+  var text = '';
+  var lastStop = 0;
+  for (var x = 0; x < D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) {
+    symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x);
+    var stats = datum.symbol_stats[symbol_type];
+    if (stats !== undefined) {
+      if (text.length !== 0) {
+        text += ', ';
+      }
+      var percent = 100 * (stats.size / datum.value);
+      var nowStop = lastStop + percent;
+      var tempcolor = D3SymbolTreeMap.getColorForType(symbol_type);
+      var color = d3.rgb(tempcolor).toString();
+      text += color + ' ' + lastStop + '%, ' + color + ' ' +
+          nowStop + '%';
+      lastStop = nowStop;
+    }
+  }
+  return 'linear-gradient(' + (datum.dx > datum.dy ? 'to right' :
+                               'to bottom') + ', ' + text + ')';
+}
+
+D3SymbolTreeMap.prototype.pathFor = function(datum) {
+  if (datum.__path) return datum.__path;
+  parts=[];
+  node = datum;
+  while (node) {
+    if (node.k === 'p') { // path node
+      if(node.n !== '/') parts.unshift(node.n);
+    }
+    node = node.parent;
+  }
+  datum.__path = '/' + parts.join('/');
+  return datum.__path;
+}
+
+D3SymbolTreeMap.prototype._createHighlight = function(datum, selection) {
+  var x = parseInt(selection.style('left'));
+  var y = parseInt(selection.style('top'));
+  var w = parseInt(selection.style('width'));
+  var h = parseInt(selection.style('height'));
+  datum.highlight = this._mapContainer.append('div')
+      .attr('id', 'h-' + datum.id)
+      .attr('class', 'highlight')
+      .style('pointer-events', 'none')
+      .style('-webkit-user-select', 'none')
+      .style('z-index', '999999')
+      .style('position', 'absolute')
+      .style('top', y-2)
+      .style('left', x-2)
+      .style('width', w+4)
+      .style('height', h+4)
+      .style('margin', 0)
+      .style('padding', 0)
+      .style('border', '4px outset rgba(250,40,200,0.9)')
+      .style('box-sizing', 'border-box')
+      .style('opacity', 0.0);
+}
+
+D3SymbolTreeMap.prototype._showHighlight = function(datum, selection) {
+  if (datum === this._currentRoot) return;
+  if (datum.highlight === undefined) {
+    this._createHighlight(datum, selection);
+  }
+  datum.highlight.transition().duration(200).style('opacity', 1.0);
+}
+
+D3SymbolTreeMap.prototype._hideHighlight = function(datum, selection) {
+  if (datum.highlight === undefined) return;
+  datum.highlight.transition().duration(750)
+      .style('opacity', 0)
+      .each('end', function(){
+        if (datum.highlight) datum.highlight.remove();
+        delete datum.highlight;
+      });
+}
+
+D3SymbolTreeMap.prototype._createInfoBox = function() {
+  return d3.select('body')
+      .append('div')
+      .attr('id', 'infobox')
+      .style('z-index', '2147483647') // (2^31) - 1: Hopefully safe :)
+      .style('position', 'absolute')
+      .style('visibility', 'hidden')
+      .style('background-color', 'rgba(255,255,255, 0.9)')
+      .style('border', '1px solid black')
+      .style('padding', '10px')
+      .style('-webkit-user-select', 'none')
+      .style('box-shadow', '3px 3px rgba(70,70,70,0.5)')
+      .style('border-radius', '10px')
+      .style('white-space', 'nowrap');
+}
+
+D3SymbolTreeMap.prototype._showInfoBox = function(datum) {
+  this.infobox.text('');
+  var numSymbols = 0;
+  var sizeish = D3SymbolTreeMap._pretty(datum.value) + ' bytes (' +
+      D3SymbolTreeMap._byteify(datum.value) + ')';
+  if (datum.k === 'p' || datum.k === 'b') { // path or bucket
+    if (datum.symbol_stats) { // can be empty if filters are applied
+      for (var x = 0; x < D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) {
+        symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x);
+        var stats = datum.symbol_stats[symbol_type];
+        if (stats !== undefined) numSymbols += stats.count;
+      }
+    }
+  } else if (datum.k === 's') { // symbol
+    numSymbols = 1;
+  }
+
+  if (datum.k === 'p' && !datum.lastPathElement) {
+    this.infobox.append('div').text('Directory: ' + this.pathFor(datum))
+    this.infobox.append('div').text('Size: ' + sizeish);
+  } else {
+    if (datum.k === 'p') { // path
+      this.infobox.append('div').text('File: ' + this.pathFor(datum))
+      this.infobox.append('div').text('Size: ' + sizeish);
+    } else if (datum.k === 'b') { // bucket
+      this.infobox.append('div').text('Symbol Bucket: ' +
+          D3SymbolTreeMap._getSymbolDescription(datum.t));
+      this.infobox.append('div').text('Count: ' + numSymbols);
+      this.infobox.append('div').text('Size: ' + sizeish);
+      this.infobox.append('div').text('Location: ' + this.pathFor(datum))
+    } else if (datum.k === 's') { // symbol
+      this.infobox.append('div').text('Symbol: ' + datum.n);
+      this.infobox.append('div').text('Type: ' +
+          D3SymbolTreeMap._getSymbolDescription(datum.t));
+      this.infobox.append('div').text('Size: ' + sizeish);
+      this.infobox.append('div').text('Location: ' + this.pathFor(datum))
+    }
+  }
+  if (datum.k === 'p') {
+    this.infobox.append('div')
+        .text('Number of symbols: ' + D3SymbolTreeMap._pretty(numSymbols));
+    if (datum.symbol_stats) { // can be empty if filters are applied
+      var table = this.infobox.append('table')
+          .attr('border', 1).append('tbody');
+      var header = table.append('tr');
+      header.append('th').text('Type');
+      header.append('th').text('Count');
+      header.append('th')
+          .style('white-space', 'nowrap')
+          .text('Total Size (Bytes)');
+      for (var x = 0; x < D3SymbolTreeMap._NM_SYMBOL_TYPES.length; x++) {
+        symbol_type = D3SymbolTreeMap._NM_SYMBOL_TYPES.charAt(x);
+        var stats = datum.symbol_stats[symbol_type];
+        if (stats !== undefined) {
+          var tr = table.append('tr');
+          tr.append('td')
+              .style('white-space', 'nowrap')
+              .text(D3SymbolTreeMap._getSymbolDescription(
+                  symbol_type));
+          tr.append('td').text(D3SymbolTreeMap._pretty(stats.count));
+          tr.append('td').text(D3SymbolTreeMap._pretty(stats.size));
+        }
+      }
+    }
+  }
+  this.infobox.style('visibility', 'visible');
+}
+
+D3SymbolTreeMap.prototype._hideInfoBox = function(datum) {
+  this.infobox.style('visibility', 'hidden');
+}
+
+D3SymbolTreeMap.prototype._moveInfoBox = function(event) {
+  var element = document.getElementById('infobox');
+  var w = element.offsetWidth;
+  var h = element.offsetHeight;
+  var offsetLeft = 10;
+  var offsetTop = 10;
+
+  var rightLimit = window.innerWidth;
+  var rightEdge = event.pageX + offsetLeft + w;
+  if (rightEdge > rightLimit) {
+    // Too close to screen edge, reflect around the cursor
+    offsetLeft = -1 * (w + offsetLeft);
+  }
+
+  var bottomLimit = window.innerHeight;
+  var bottomEdge = event.pageY + offsetTop + h;
+  if (bottomEdge > bottomLimit) {
+    // Too close ot screen edge, reflect around the cursor
+    offsetTop = -1 * (h + offsetTop);
+  }
+
+  this.infobox.style('top', (event.pageY + offsetTop) + 'px')
+      .style('left', (event.pageX + offsetLeft) + 'px');
+}
+
+D3SymbolTreeMap.prototype.biggestSymbols = function(maxRecords) {
+  var result = undefined;
+  var smallest = undefined;
+  var sortFunction = function(a,b) {
+    var result = b.value - a.value;
+    if (result !== 0) return result; // sort by size
+    var pathA = treemap.pathFor(a); // sort by path
+    var pathB = treemap.pathFor(b);
+    if (pathA > pathB) return 1;
+    if (pathB > pathA) return -1;
+    return a.n - b.n; // sort by symbol name
+  };
+  this.visitFromDisplayedRoot(function(datum) {
+    if (datum.children) return; // ignore non-leaves
+    if (!result) { // first element
+      result = [datum];
+      smallest = datum.value;
+      return;
+    }
+    if (result.length < maxRecords) { // filling the array
+      result.push(datum);
+      return;
+    }
+    if (datum.value > smallest) { // array is already full
+      result.push(datum);
+      result.sort(sortFunction);
+      result.pop(); // get rid of smallest element
+      smallest = result[maxRecords - 1].value; // new threshold for entry
+    }
+  });
+  result.sort(sortFunction);
+  return result;
+}
+
+D3SymbolTreeMap.prototype.biggestPaths = function(maxRecords) {
+  var result = undefined;
+  var smallest = undefined;
+  var sortFunction = function(a,b) {
+    var result = b.value - a.value;
+    if (result !== 0) return result; // sort by size
+    var pathA = treemap.pathFor(a); // sort by path
+    var pathB = treemap.pathFor(b);
+    if (pathA > pathB) return 1;
+    if (pathB > pathA) return -1;
+    console.log('warning, multiple entries for the same path: ' + pathA);
+    return 0; // should be impossible
+  };
+  this.visitFromDisplayedRoot(function(datum) {
+    if (!datum.lastPathElement) return; // ignore non-files
+    if (!result) { // first element
+      result = [datum];
+      smallest = datum.value;
+      return;
+    }
+    if (result.length < maxRecords) { // filling the array
+      result.push(datum);
+      return;
+    }
+    if (datum.value > smallest) { // array is already full
+      result.push(datum);
+      result.sort(sortFunction);
+      result.pop(); // get rid of smallest element
+      smallest = result[maxRecords - 1].value; // new threshold for entry
+    }
+  });
+  result.sort(sortFunction);
+  return result;
+}
diff --git a/runtime/third_party/binary_size/src/template/index.html b/runtime/third_party/binary_size/src/template/index.html
new file mode 100644
index 0000000..7e1a1fc
--- /dev/null
+++ b/runtime/third_party/binary_size/src/template/index.html
@@ -0,0 +1,525 @@
+<!--
+  Copyright 2014 The Chromium Authors. All rights reserved.
+  Use of this source code is governed by a BSD-style license that can be
+  found in the LICENSE file.
+-->
+<html>
+<head>
+<title>Binary Size Analysis</title>
+<script src="d3/d3.js" charset="utf-8"></script>
+<script src="D3SymbolTreeMap.js" charset="utf-8"></script>
+<script src="data.js" charset="utf-8"></script>
+<style>
+body {
+    margin: 0px;
+    padding: 5px;
+}
+.swatch {
+    border: 1px solid rgb(100,100,100);
+    -webkit-user-select: none;
+    cursor: default;
+}
+</style>
+<script>
+var treemap;
+var filterChanging = false;
+var savedSettings = {};
+
+function init() {
+    if (window.metadata !== undefined && window.metadata.subtitle) {
+        document.getElementById('subtitle').innerHTML = ': ' + escape(metadata.subtitle);
+    }
+    initFilterOptions();
+    treemap = new D3SymbolTreeMap(
+        savedSettings.width,
+        savedSettings.height,
+        savedSettings.maxLevels);
+    treemap.init();
+}
+
+function getIdealSizes() {
+    var width = window.innerWidth - 20;
+    var height = window.innerHeight - 70;
+    return {'width': width, 'height': height};
+}
+
+function showReport(title, data, headers, dataFunction, styleFunction) {
+    var div =  d3.select('body').append('div')
+        .style('margin', '0')
+        .style('padding', '5px')
+        .style('position', 'absolute')
+        .style('top', '10%')
+        .style('left', '10%')
+        .style('background-color', 'rgba(255,255,255,0.9)')
+        .style('width', '80%')
+        .style('height', '80%')
+        .style('z-index', '2147483647')
+        .style('border', '3px ridge grey')
+        .style('box-shadow', '10px 10px 5px rgba(80,80,80,0.7)')
+        .style('text-align', 'center')
+        .style('border-radius', '10px');
+    var titlebar = div.append('div')
+        .style('margin', '0')
+        .style('padding', '5px')
+        .style('position', 'absolute')
+        .style('top', '0%')
+        .style('left', '0%')
+        .style('width', '100%')
+        .style('height', '10%')
+        .style('font-size', 'x-large');
+    titlebar.text(title);
+    var controls = div.append('div')
+        .style('margin', '0')
+        .style('padding', '5px')
+        .style('position', 'absolute')
+        .style('top', '90%')
+        .style('left', '0%')
+        .style('width', '100%')
+        .style('height', '10%');
+    controls.append('input').attr('type', 'button')
+        .attr('value', 'Dismiss')
+        .on('click', function(){div.remove();});
+
+    var tableDiv = div.append('div')
+        .style('overflow', 'auto')
+        .style('position', 'absolute')
+        .style('top', '10%')
+        .style('left', '0%')
+        .style('width', '100%')
+        .style('height', '80%')
+        .style('border-top', '1px solid rgb(230,230,230)')
+        .style('border-bottom', '1px solid rgb(230,230,230)');
+    var table = tableDiv.append('table')
+        .attr('border', '1')
+        .attr('cellspacing', '0')
+        .attr('cellpadding', '2')
+        .style('margin-left', 'auto')
+        .style('margin-right', 'auto');
+    var header = table.append('tr');
+    for (var i = 0; i < headers.length; i++) {
+        header.append('th').text(headers[i]);
+    }
+
+    for (var i = 0; i < data.length; i++) {
+        var row = table.append('tr');
+        for (j = 0; j < headers.length; j++) {
+            var td = row.append('td');
+            if (styleFunction) {
+                styleFunction.call(this, td, j);
+            }
+            dataFunction.call(this, data[i], j, td);
+        }
+    }
+}
+
+function bigSymbolsReport() {
+    var list = treemap.biggestSymbols(100);
+    var headers = ['Rank', 'Size (Bytes)', 'Type', 'Location'];
+    var styleFunction = function(selection, index) {
+        if (index === 3) {
+            selection.style('font-family', 'monospace');
+        }
+    };
+    var recordIndex = 1;
+    var dataFunction = function(record, index, cell) {
+        if (index === 0) {
+            cell.text(recordIndex++);
+        } else if (index === 1) {
+            cell.text(D3SymbolTreeMap._pretty(record.value));
+        } else if (index === 2) {
+            cell.text(record.t);
+        } else {
+            if (treemap.pathFor(record).indexOf('/out') == 0) {
+                cell.append('span').text(treemap.pathFor(record));
+                cell.append('br');
+                cell.append('span').text('Symbol: ');
+                cell.append('span').text(record.n);
+            } else {
+                var href = 'https://code.google.com/p/chromium/codesearch#chromium/src'
+                    + treemap.pathFor(record)
+                    + '&q='
+                    + record.n;
+                cell.append('a')
+                    .attr('href', href)
+                    .attr('target', '_blank')
+                    .text(treemap.pathFor(record));
+                cell.append('br');
+                cell.append('span').text('Symbol: ');
+                cell.append('span').text(record.n);
+            }
+        }
+    };
+    showReport('100 Largest Symbols', list, headers, dataFunction, styleFunction);
+}
+
+function bigPathsReport() {
+    var list = treemap.biggestPaths(100);
+    var headers = ['Rank', 'Size (Bytes)', 'Location'];
+    var styleFunction = function(selection, index) {
+        if (index === 2) {
+            selection.style('font-family', 'monospace');
+        }
+    };
+    var recordIndex = 1;
+    var dataFunction = function(record, index, cell) {
+        if (index === 0) {
+            cell.text(recordIndex++);
+        } else if (index === 1) {
+            cell.text(D3SymbolTreeMap._pretty(record.value));
+        } else if (index === 2) {
+            if (treemap.pathFor(record).indexOf('/out') == 0) {
+                cell.text(treemap.pathFor(record));
+            } else {
+                var href = 'https://code.google.com/p/chromium/codesearch#chromium/src' + treemap.pathFor(record);
+                cell.append('a')
+                    .attr('href', href)
+                    .attr('target', '_blank')
+                    .text(treemap.pathFor(record));
+            }
+
+        }
+    };
+    showReport('100 Largest Paths', list, headers, dataFunction, styleFunction);
+}
+
+function symbolFilterTextChanged() {
+    if (filterChanging) return true;
+    filterChanging = true;
+    var enabled = document.getElementById('symbol_types_filter').value;
+    for (var x=0; x<=25; x++) {
+        var checkBox = document.getElementById('check_' + x);
+        checkBox.checked = (enabled.indexOf(checkBox.value) != -1);
+    }
+    filterChanging = false;
+}
+
+function updateFilterText() {
+    if (filterChanging) return true;
+    filterChanging = true;
+    var text = '';
+    for (var x=0; x<=25; x++) {
+        var checkBox = document.getElementById('check_' + x);
+        if (checkBox.checked) {
+            text += checkBox.value;
+        }
+    }
+    document.getElementById('symbol_types_filter').value=text;
+    filterChanging = false;
+}
+
+function initFilterOptions() {
+    updateFilterText();
+    for (var x=0; x<=25; x++) {
+        var checkBox = document.getElementById('check_' + x);
+        checkBox.onchange=updateFilterText;
+        var swatch = document.getElementById('swatch_' + x);
+        swatch.style.backgroundColor = D3SymbolTreeMap.getColorForType(checkBox.value).toString();
+    }
+    var gteCheckbox = document.getElementById('check_gte');
+    gteCheckbox.onchange = function() {
+        document.getElementById('symbol_filter_gte').disabled = !gteCheckbox.checked;
+    }
+    var regexCheckbox = document.getElementById('check_regex');
+    regexCheckbox.onchange = function() {
+        document.getElementById('symbol_filter_regex').disabled = !regexCheckbox.checked;
+    }
+    var excludeRegexCheckbox = document.getElementById('check_exclude_regex');
+    excludeRegexCheckbox.onchange = function() {
+        document.getElementById('symbol_filter_exclude_regex').disabled = !excludeRegexCheckbox.checked;
+    }
+    var idealSizes = getIdealSizes();
+    document.getElementById('width').value = idealSizes.width;
+    document.getElementById('height').value = idealSizes.height;
+    saveFilterSettings();
+}
+
+function filterSetAll(enabled) {
+    for (var x=0; x<=25; x++) {
+        var checkBox = document.getElementById('check_' + x);
+        checkBox.checked = enabled;
+    }
+    updateFilterText();
+}
+
+function showOptions() {
+    loadFilterSettings();
+    var container = document.getElementById('options_container');
+    var w = container.offsetWidth;
+    var h = container.offsetHeight;
+    container.style.margin = '-' + (h/2) + 'px 0 0 -' + (w/2) + 'px';
+    container.style.visibility = 'visible';
+}
+
+function hideOptions() {
+    var container = document.getElementById('options_container');
+    container.style.visibility = 'hidden';
+}
+
+function applySettings() {
+    hideOptions();
+    var oldWidth = savedSettings.width;
+    var oldHeight = savedSettings.height;
+    var oldSymbols = savedSettings.symbolTypes;
+    var oldRegex = savedSettings.regex;
+    var oldExcludeRegex = savedSettings.excludeRegex;
+    var oldGte = savedSettings.gte;
+    var oldMaxLevels = savedSettings.maxLevels;
+    saveFilterSettings();
+    var resizeNeeded = oldWidth !== savedSettings.width || oldHeight !== savedSettings.height;
+    var regexChanged = oldRegex !== savedSettings.regex;
+    var excludeRegexChanged = oldExcludeRegex !== savedSettings.excludeRegex;
+    var symbolsChanged = oldSymbols !== savedSettings.symbolTypes;
+    var gteChanged = oldGte !== savedSettings.gte;
+    var filterChanged = regexChanged || excludeRegexChanged || symbolsChanged || gteChanged;
+    var maxLevelsChanged = oldMaxLevels !== savedSettings.maxLevels;
+
+    if (filterChanged) {
+        // Type filters
+        typeFilter = function(datum) {
+            if (datum.depth === 0) return true; // root node
+            if (datum.t === undefined) return true;
+            return savedSettings.symbolTypes !== undefined &&
+                savedSettings.symbolTypes.indexOf(datum.t) !== -1;
+        }
+
+        // Regex filter
+        var regexFilter = undefined;
+        if (savedSettings.regex !== undefined && savedSettings.regex.length > 0) {
+            console.log('filter: regex is "' + savedSettings.regex + '"');
+            var regex = new RegExp(savedSettings.regex);
+            regexFilter = function(datum) {
+                if (datum.depth === 0) return true; // root node
+                var fullName = this.pathFor(datum);
+                if (datum.children === undefined) { // it is a leaf node (symbol)
+                    fullName += ':' + datum.n;
+                }
+                return regex.test(fullName);
+            }
+        }
+
+        // Exclude regex filter
+        var excludeRegexFilter = undefined;
+        if (savedSettings.excludeRegex !== undefined && savedSettings.excludeRegex.length > 0) {
+            console.log('filter: exclude-regex is "' + savedSettings.excludeRegex + '"');
+            var excludeRegex = new RegExp(savedSettings.excludeRegex);
+            excludeRegexFilter = function(datum) {
+                if (datum.depth === 0) return true; // root node
+                var fullName = this.pathFor(datum);
+                if (datum.children === undefined) { // it is a leaf node (symbol)
+                    fullName += ':' + datum.n;
+                }
+                return !excludeRegex.test(fullName);
+            }
+        }
+
+        // Size filter
+        var sizeFilter = undefined;
+        if (savedSettings.gte !== undefined) {
+            console.log('filter: minimum size is ' + savedSettings.gte + ' bytes');
+            sizeFilter = function(datum) {
+                if (datum.children !== undefined) return true; // non-leaf
+                if (datum.value === undefined) console.log('whoops');
+                return datum.value >= savedSettings.gte;
+            }
+        }
+
+        // Make a filter to apply to the tree
+        var filter = function(datum) {
+            if (typeFilter && !typeFilter.call(this, datum)) return false;
+            if (regexFilter && !regexFilter.call(this, datum)) return false;
+            if (excludeRegexFilter && !excludeRegexFilter.call(this, datum)) return false;
+            if (sizeFilter && !sizeFilter.call(this, datum)) return false;
+            return true;
+        };
+        treemap.filter(filter);
+    }
+
+    // Adjust levels if needed.
+    if (maxLevelsChanged) {
+        treemap.setMaxLevels(savedSettings.maxLevels);
+    }
+
+    // Resize map if necessary.
+    if (resizeNeeded) {
+        console.log('desired treemap dimensions have changed, requesting resize');
+        treemap.resize(savedSettings.width, savedSettings.height);
+    }
+}
+
+function cancelSettings() {
+    hideOptions();
+    loadFilterSettings();
+}
+
+function saveFilterSettings() {
+    savedSettings.symbolTypes = document.getElementById('symbol_types_filter').value;
+    if (document.getElementById('check_regex').checked) {
+        savedSettings.regex = document.getElementById('symbol_filter_regex').value;
+    } else {
+        savedSettings.regex = undefined;
+    }
+    if (document.getElementById('check_exclude_regex').checked) {
+        savedSettings.excludeRegex = document.getElementById('symbol_filter_exclude_regex').value;
+    } else {
+        savedSettings.excludeRegex = undefined;
+    }
+    if (document.getElementById('check_gte').checked) {
+        savedSettings.gte = parseInt(document.getElementById('symbol_filter_gte').value);
+    } else {
+        savedSettings.gte = undefined;
+    }
+    savedSettings.width = parseInt(document.getElementById('width').value);
+    savedSettings.height = parseInt(document.getElementById('height').value);
+    savedSettings.maxLevels = parseInt(document.getElementById('max_levels').value);
+}
+
+function loadFilterSettings() {
+    document.getElementById('symbol_types_filter').value = savedSettings.symbolTypes;
+    symbolFilterTextChanged();
+    if (savedSettings.regex !== undefined) {
+        document.getElementById('check_regex').checked = true;
+        document.getElementById('symbol_filter_regex').value = savedSettings.regex;
+    } else {
+        document.getElementById('check_regex').checked = false;
+    }
+    if (savedSettings.excludeRegex !== undefined) {
+        document.getElementById('check_exclude_regex').checked = true;
+        document.getElementById('symbol_filter_exclude_regex').value = savedSettings.excludeRegex;
+    } else {
+        document.getElementById('check_exclude_regex').checked = false;
+    }
+    if (savedSettings.gte !== undefined) {
+        document.getElementById('check_gte').checked = true;
+        document.getElementById('symbol_filter_gte').value = savedSettings.gte;
+    } else {
+        document.getElementById('check_gte').checked = false;
+    }
+    document.getElementById('width').value = savedSettings.width;
+    document.getElementById('height').value = savedSettings.height;
+    document.getElementById('max_levels').value = savedSettings.maxLevels;
+}
+
+function escape(str) {
+    return str.replace(/&/g, '&amp;')
+              .replace(/"/g, '&quot;')
+              .replace(/</g, '&lt;')
+              .replace(/>/g, '&gt;');
+}
+</script>
+</head>
+<body onload='init()'>
+<div style='position: absolute; top: 5px; left: 5px;'>
+  <input type='button' onclick='showOptions()' value='Options &amp; Legend...'>
+  <span style='-webkit-user-select: none; cursor: help;' title='Click to view the symbol legend or to configure filters and options for the treemap'>[?]</span>
+</div>
+<div style='position: absolute; right: 5px; top: 5px; white-space: nowrap;'>
+    Reports:
+    <input type='button' onclick='bigSymbolsReport()' value='Large Symbols' title='Click to view a report of the largest 100 symbols that are with the bounds of the treemap that is currently displayed.'>
+    <input type='button' onclick='bigPathsReport()' value='Large Files' title='Click to view a report of the largest 100 source files that are with the bounds of the treemap that is currently displayed.'>
+</div>
+<div style='text-align: center; margin-bottom: 5px;'>
+    <span style='font-size: x-large; font-weight: bold; font-variant: small-caps'>Binary Size Analysis<span id='subtitle'></span></span>
+    <br><span style='font-size: small; font-style: italic;'>Double-click a box to zoom in, double-click outermost title to zoom out.</span>
+</div>
+<table id='options_container' style='visibility: hidden; border: 3px ridge grey; padding: 0px; top: 50%; left: 50%; position: fixed; z-index: 2147483646; overflow: auto; background-color: rgba(255,255,255,0.9); border-radius: 10px; box-shadow: 10px 10px 5px rgba(80,80,80,0.7);'><tr><td style='vertical-align: top'>
+    <table cellspacing=0 cellborder=0 style='width:100%'>
+        <tr><th colspan=3 style='padding-bottom: .25em; text-decoration: underline;'>Symbol Types To Show</th></tr>
+        <tr>
+            <td style='width: 33%; white-space: nowrap; vertical-align: top;'>
+                    <span class='swatch' id='swatch_0'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_0' value='A'>Global absolute (A)
+                <br><span class='swatch' id='swatch_1'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_1' value='B'>Global uninitialized data (B)
+                <br><span class='swatch' id='swatch_2'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_2' value='b'>Local uninitialized data (b)
+                <br><span class='swatch' id='swatch_3'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_3' value='C'>Global uninitialized common (C)
+                <br><span class='swatch' id='swatch_4'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_4' value='D'>Global initialized data (D)
+                <br><span class='swatch' id='swatch_5'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_5' value='d'>Local initialized data (d)
+                <br><span class='swatch' id='swatch_6'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_6' value='G'>Global small initialized data (G)
+                <br><span class='swatch' id='swatch_7'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_7' value='g'>Local small initialized data (g)
+                <br><span class='swatch' id='swatch_8'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_8' value='i'>Indirect function (i)
+            </td>
+            <td style='width: 33%; white-space: nowrap; vertical-align: top;'>
+                    <span class='swatch' id='swatch_9'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_9' value='N'>Debugging (N)
+                <br><span class='swatch' id='swatch_10'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_10' value='p'>Stack unwind (p)
+                <br><span class='swatch' id='swatch_11'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_11' value='R'>Global read-only data (R)
+                <br><span class='swatch' id='swatch_12'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_12' value='r'>Local read-only data (r)
+                <br><span class='swatch' id='swatch_13'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_13' value='S'>Global small uninitialized data (S)
+                <br><span class='swatch' id='swatch_14'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_14' value='s'>Local small uninitialized data (s)
+                <br><span class='swatch' id='swatch_15'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_15' value='T'>Global code (T)
+                <br><span class='swatch' id='swatch_16'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_16' value='t'>Local code (t)
+                <br><span class='swatch' id='swatch_17'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_17' value='U'>Undefined (U)
+            </td>
+            <td style='width: 33%; white-space: nowrap; vertical-align: top;'>
+                    <span class='swatch' id='swatch_18'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_18' value='u'>Unique (u)
+                <br><span class='swatch' id='swatch_19'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_19' value='V'>Global weak object (V)
+                <br><span class='swatch' id='swatch_20'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_20' value='v'>Local weak object (v)
+                <br><span class='swatch' id='swatch_21'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_21' value='W'>Global weak symbol (W)
+                <br><span class='swatch' id='swatch_22'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_22' value='w'>Local weak symbol (w)
+                <br><span class='swatch' id='swatch_23'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_23' value='@'>Vtable entry (@)
+                <br><span class='swatch' id='swatch_24'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_24' value='-'>STABS debugging (-)
+                <br><span class='swatch' id='swatch_25'>&nbsp;&nbsp;&nbsp;</span><input checked type='checkbox' id='check_25' value='?'>Unrecognized (?)
+            </td>
+        </tr>
+        <tr><td colspan=3 style='text-align: center; white-space: nowrap; padding-top: 1em;'>
+            Select <input type='button' onclick='filterSetAll(true)' value='All'>,
+            <input type='button' onclick='filterSetAll(false)' value='None'>,
+            or type a string: <input id='symbol_types_filter' size=30 value='' onkeyup='symbolFilterTextChanged()' onblur='updateFilterText()'>
+            <span style='-webkit-user-select: none; cursor: help;' title='Enter codes from the list above for the symbols you want to see. The checkboxes will update automatically to match the string that you enter.'>[?]</span>
+        </td></tr>
+   </table>
+</td></tr><tr><td style='vertical-align: top; padding-top: 10px; border-top: 1px solid grey;'>
+    <table cellspacing=0 cellborder=0 style='width: 100%'>
+        <tr><th colspan=2 style='padding-bottom: .25em; text-decoration: underline;'>Advanced Options</th></tr>
+        <tr>
+            <td style='white-space: nowrap; vertical-align: top;'>
+                <input type='checkbox' id='check_regex'>
+                Only include symbols matching this regex:
+            </td>
+            <td style='text-align: right; vertical-align: top;'>
+                <input disabled id='symbol_filter_regex' size=30 value='' style='text-align: right;'>
+                <span style='-webkit-user-select: none; cursor: help;' title='Enter a javascript regex. Only symbols that match this regex will be shown. This filter applies before any exclusion regex specified below. The format of each symbol is [path]:[symbol_name]'>[?]</span>
+            </td>
+        </tr>
+        <tr>
+            <td style='white-space: nowrap; vertical-align: top;'>
+                <input type='checkbox' id='check_exclude_regex'>
+                Exclude all symbols matching this regex:
+            </td>
+            <td style='text-align: right; vertical-align: top;'>
+                <input disabled id='symbol_filter_exclude_regex' size=30 value='' style='text-align: right;'>
+                <span style='-webkit-user-select: none; cursor: help;' title='Enter a javascript regex. Symbols that match this tegex will not be shown. This filter applies after any inclusion filter specified above. The format of each symbol is [path]:[symbol_name]'>[?]</span>
+            </td>
+        </tr>
+        <tr>
+            <td style='white-space: nowrap; vertical-align: top;'>
+                <input type='checkbox' id='check_gte'>
+                Only include symbols that are at least <span style='font-style: italic;'>n</span> bytes:
+            </td>
+            <td style='text-align: right; vertical-align: top;'>
+                <input disabled id='symbol_filter_gte' size=8 value='' style='text-align: right;'>
+                <span style='-webkit-user-select: none; cursor: help;' title='Symbols whose size is less than this value will be hidden.'>[?]</span>
+            </td>
+        </tr>
+        <tr>
+            <td style='white-space: nowrap vertical-align: top;;'>
+                Show at most <span style='font-style: italic;'>n</span> levels of detail at a time:
+            </td>
+            <td style='text-align: right; vertical-align: top;'>
+                <input id='max_levels' size=4 value='2' style='text-align: right;'><span style='-webkit-user-select: none; cursor: help;' title='Increasing this value shows more detail without the need to zoom, but uses more computing power.'>[?]</span>
+            </td>
+        </tr>
+        <tr>
+            <td style='white-space: nowrap vertical-align: top;;'>
+                Set the size of the treemap to <span style='font-style: italic;'>W x H</span> pixels:
+            </td>
+            <td style='text-align: right; vertical-align: top;'>
+                <input id='width' size=4 value='' style='text-align: right;'>
+                &nbsp;x&nbsp;<input id='height' size=4 value='' style='text-align: right;'>
+            </td>
+        </tr>
+    </table>
+</td></tr>
+<tr><td style='padding-top: 10px; text-align: right; border-top: 1px solid grey'>
+    <input type='button' value='Apply' onclick='applySettings()'>
+    <input type='button' value='Cancel' onclick='cancelSettings()'>
+</td></tr></table>
+</body>
+</html>
diff --git a/runtime/third_party/binary_size/src/template/test-data-generator.html b/runtime/third_party/binary_size/src/template/test-data-generator.html
new file mode 100644
index 0000000..9c6790a
--- /dev/null
+++ b/runtime/third_party/binary_size/src/template/test-data-generator.html
@@ -0,0 +1,157 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2014 The Chromium Authors. All rights reserved.
+  Use of this source code is governed by a BSD-style license that can be
+  found in the LICENSE file.
+-->
+<html>
+<head>
+<script>
+function rnd(max) {
+    return Math.round(Math.random()*max);
+}
+
+function gen() {
+  var dirs1=['usr1', 'etc1', 'var1'];
+  var dirs2=['aaa2', 'bbb2', 'ccc2', 'ddd2', 'eee2', 'fff2', 'ggg2', 'hhh2',
+             'frobozz2', 'kazaam2', 'shazam2'];
+  var dirs3=['iii3', 'jjj3', 'kkk3', 'lll3', 'mmm3', 'nnn3', 'ooo3', 'ppp3',
+             'wonderllama3', 'excelsior3', 'src3'];
+  var filenames=['awesome.cc', 'rad.h', 'tubular.cxx', 'cool.cc', 'groovy.h',
+                 'excellent.c', 'gnarly.h', 'green.C', 'articulate.cc'];
+  //All possible types (we only see a subset in practice): 'ABbCDdGgiNpRrSsTtUuVvWw-?';
+  var nm_symbol_types = 'trd';
+  var minSize = 4;
+  var maxSize = 10000;
+  var numGen = 300000;
+  var text = 'var nm_data=[\n';
+  var vtablePercent = 5;
+  for (var x=0; x<numGen; x++) {
+    var path = '/' +
+        dirs1[rnd(dirs1.length - 1)] + '/' +
+        dirs2[rnd(dirs2.length - 1)] + '/' +
+        dirs3[rnd(dirs3.length - 1)] + '/' +
+        filenames[rnd(filenames.length - 1)];
+    var isVtable = Math.floor((Math.random()*100)+1) <= vtablePercent;
+    var size = rnd(maxSize);
+    var symbol_name;
+    var type;
+    if (!isVtable) {
+      symbol_name = 'sym' + x.toString(16);
+      type = nm_symbol_types.charAt(rnd(nm_symbol_types.length - 1));
+    } else {
+      symbol_name = 'vtable for ' + x.toString(16);
+      type = '@'
+    }
+    text = text + "{'n': '" + symbol_name +
+        "', 't': '" + type +
+        "', 's': " + size +
+        ", 'p': '" + path + "'},\n";
+  }
+  text += '];';
+
+  eval(text);
+  var treeified = to_d3_tree(nm_data);
+  generateDownloadLink('tree_data=' + JSON.stringify(treeified));
+}
+
+function generateDownloadLink(content) {
+  var blob = new Blob([content], {type: 'text/plain'});
+  var link = document.createElement('a');
+  link.download = 'generated-content.txt';
+  link.href = window.URL.createObjectURL(blob);
+  link.textContent = 'Download ready, click here.';
+  link.dataset.downloadurl = ['text/plain', link.download, link.href].join(':');
+  link.onclick = function(e) {
+    if ('disabled' in this.dataset) { return false; }
+    link.dataset.disabled = true;
+    setTimeout(function() { window.URL.revokeObjectURL(link.href); }, 1500);
+  };
+  document.getElementById('linkcontainer').innerHTML = '';
+  document.getElementById('linkcontainer').appendChild(link);
+}
+
+/**
+ * This function takes in an array of nm records and converts them into a
+ * hierarchical data structure suitable for use in a d3-base treemap layout.
+ * Leaves are individual symbols. The parents of the leaves are logical
+ * groupings by common symbol-type (for BSS, read-only data, code, etc).
+ * Above this, each node represents part of a filesystem path relative
+ * to the parent node. The root node has the name '/', and represents
+ * a root (though not necessarily THE root) of a file system traversal.
+ * The root node also has a special property, 'maxDepth', to which is bound
+ * the deepest level of nesting that was found during conversion: for the
+ * record at path /a/b/c/d.foo, the maxDepth will be 6; the file 'd.foo'
+ * is at depth 4, the type-bucket is depth 5 and the symbols are depth 6.
+ */
+function to_d3_tree(records) {
+  var result = {'n': '/', 'children': [], 'k': 'p'};
+  var maxDepth = 0;
+  //{'n': 'symbol1', 't': 'b', 's': 1000, 'p': '/usr/local/foo/foo.cc'},
+  for (index in records) {
+    var record = records[index];
+    var parts = record.p.split("/");
+    var node = result;
+    var depth = 0;
+    // Walk the tree and find the file that is named by the "location"
+    // field of the record. We create any intermediate nodes required.
+    // This is directly analogous to "mkdir -p".
+    while(parts.length > 0) {
+      var part = parts.shift();
+      if (part.length == 0) continue;
+      depth++;
+      node = _mk_child(node, part, record.s);
+      node.k = 'p'; // p for path
+    }
+    node.lastPathElement = true;
+
+    // 'node' is now the file node. Find the symbol-type bucket.
+    node = _mk_child(node, record.t, record.s);
+    node.t = record.t;
+    node.k = 'b'; // b for bucket
+    depth++;
+    // 'node' is now the symbol-type bucket. Make the child entry.
+    node = _mk_child(node, record.n, record.s);
+    delete node.children;
+    node.value = record.s;
+    node.t = record.t;
+    node.k = 's'; // s for symbol
+    depth++;
+
+    maxDepth = Math.max(maxDepth, depth);
+  }
+  result.maxDepth = maxDepth;
+  return result;
+}
+
+/**
+ * Given a node and a name, return the child within node.children whose
+ * name matches the specified name. If necessary, a new child node is
+ * created and appended to node.children.
+ * If this method creates a new node, the 'name' attribute is set to the
+ * specified name and the 'children' attribute is an empty array, and
+ * total_size is the specified size. Otherwise, the existing node is
+ * returned and its total_size value is incremented by the specified size.
+ */
+function _mk_child(node, name, size) {
+  var child = undefined;
+  for (child_index in node.children) {
+    if (node.children[child_index].n == name) {
+      child = node.children[child_index];
+    }
+  }
+  if (child === undefined) {
+    child = {'n': name, 'children': []};
+    node.children.push(child);
+  }
+  return child;
+}
+</script>
+</head>
+<body style='white-space: pre; font-family: monospace;'>
+This script generates sample data for use in D3SymbolTreeMap, and can be used
+for testing.
+<input type=button onclick='gen();' value='Generate data'></input>
+<div id='linkcontainer'></div>
+</body>
+</html>
diff --git a/runtime/tools/binary_size b/runtime/tools/binary_size
deleted file mode 100755
index 5621bf5..0000000
--- a/runtime/tools/binary_size
+++ /dev/null
@@ -1,402 +0,0 @@
-#!/usr/bin/env dart
-// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import "dart:io";
-
-class Symbol {
-  String? name;
-  String? type;
-  int? shallowSize;
-  int? retainedSize;
-  List<Symbol> children = <Symbol>[];
-
-  Symbol compressTrivialPaths() {
-    for (var i = 0; i < children.length; i++) {
-      children[i] = children[i].compressTrivialPaths();
-    }
-    if ((type == "path") && (children.length == 1)) {
-      return children[0];
-    }
-    return this;
-  }
-
-  int computeRetainedSize() {
-    var s = shallowSize!;
-    for (var child in children) {
-      s += child.computeRetainedSize();
-    }
-    retainedSize = s;
-    return s;
-  }
-
-  writeJson(StringBuffer out) {
-    out.write("{");
-    out.write('"name": "$name",');
-    out.write('"shallowSize": $shallowSize,');
-    out.write('"retainedSize": $retainedSize,');
-    out.write('"type": "$type",');
-    out.write('"children": [');
-    bool first = true;
-    for (var child in children) {
-      if (first) {
-        first = false;
-      } else {
-        out.write(",");
-      }
-      child.writeJson(out);
-    }
-    out.write("]}");
-  }
-}
-
-const filteredPathComponents = <String>[
-  "",
-  ".",
-  "..",
-  "out",
-  "src",
-  "source",
-  "third_party",
-  "DebugX64",
-  "ReleaseX64",
-  "ProductX64",
-];
-
-String prettyPath(String path) {
-  return path
-      .split("/")
-      .where((component) => !filteredPathComponents.contains(component))
-      .join("/");
-}
-
-main(List<String> args) {
-  if (args.isEmpty) {
-    print("Usage: binary_size <binaries>");
-    exit(1);
-  }
-
-  for (var arg in args) {
-    analyze(arg);
-  }
-}
-
-analyze(String binaryPath) {
-  var nmExec = "nm";
-  var nmArgs = ["--demangle", "--line-numbers", "--print-size", binaryPath];
-  var nmResult = Process.runSync(nmExec, nmArgs);
-  if (nmResult.exitCode != 0) {
-    print("+ ${nmExec} ${nmArgs.join(' ')}");
-    print(nmResult.exitCode);
-    print(nmResult.stdout);
-    print(nmResult.stderr);
-    exit(1);
-  }
-
-  var root = new Symbol();
-  root.name = binaryPath;
-  root.type = "path";
-  root.shallowSize = 0;
-  var paths = new Map<String, Symbol>();
-  paths[""] = root;
-  addToPath(Symbol s, String path) {
-    Symbol? p = paths[path];
-    if (p == null) {
-      p = new Symbol();
-      p.name = path;
-      p.type = "path";
-      p.shallowSize = 0;
-      paths[path] = p;
-
-      var i = path.lastIndexOf("/");
-      if (i != -1) {
-        p.name = path.substring(i + 1);
-        addToPath(p, path.substring(0, i));
-      } else {
-        root.children.add(p);
-      }
-    }
-    p.children.add(s);
-  }
-
-  var lines = nmResult.stdout.split("\n");
-  var regex = new RegExp("([0-9a-f]+) ([0-9a-f]+) ([a-zA-z]) (.*)");
-  for (var line in lines) {
-    var match = regex.firstMatch(line);
-    if (match == null) {
-      continue;
-    }
-
-    var address = int.parse(match[1]!, radix: 16);
-    var size = int.parse(match[2]!, radix: 16);
-    var type = match[3];
-    if (type == "b" || type == "B") {
-      // Uninitialized data does not contribute to file size.
-      continue;
-    }
-
-    var nameAndPath = match[4]!.split("\t");
-    var name = nameAndPath[0].trim();
-    var path = nameAndPath.length == 1
-        ? ""
-        : prettyPath(nameAndPath[1].trim().split(":")[0]);
-
-    var s = new Symbol();
-    s.name = name;
-    s.type = type;
-    s.shallowSize = size;
-    addToPath(s, path);
-  }
-
-  root.compressTrivialPaths();
-  root.computeRetainedSize();
-
-  var json = new StringBuffer();
-  root.writeJson(json);
-
-  var html = viewer.replaceAll("__DATA__", json.toString());
-  new File("${binaryPath}.html").writeAsStringSync(html);
-
-  // This written as a URL instead of path because some terminals will
-  // automatically recognize it and make it a link.
-  var url = Directory.current.uri.resolve("${binaryPath}.html");
-  print("Wrote $url");
-}
-
-var viewer = """
-<html>
-<head>
-<style>
-.treemapTile {
-    position: absolute;
-    box-sizing: border-box;
-    border: solid 1px;
-    font-size: 10;
-    text-align: center;
-    overflow: hidden;
-    white-space: nowrap;
-    cursor: default;
-}
-</style>
-</head>
-<body>
-<script>
-var root = __DATA__;
-
-function hash(string) {
-  // Jenkin's one_at_a_time.
-  let h = string.length;
-  for (let i = 0; i < string.length; i++) {
-    h += string.charCodeAt(i);
-    h += h << 10;
-    h ^= h >> 6;
-  }
-  h += h << 3;
-  h ^= h >> 11;
-  h += h << 15;
-  return h;
-}
-
-function color(string) {
-  let hue = hash(string) % 360;
-  return "hsl(" + hue + ",60%,60%)";
-}
-
-function prettySize(size) {
-  if (size < 1024) return size + "B";
-  size /= 1024;
-  if (size < 1024) return size.toFixed(1) + "KiB";
-  size /= 1024;
-  if (size < 1024) return size.toFixed(1) + "MiB";
-  size /= 1024;
-  return size.toFixed(1) + "GiB";
-}
-
-function createTreemapTile(v, width, height, depth) {
-  let div = document.createElement("div");
-  div.className = "treemapTile";
-  div.style["background-color"] = color(v.type);
-  div.ondblclick = function(event) {
-    event.stopPropagation();
-    if (depth == 0) {
-      let parent = v.parent;
-      if (parent === undefined) {
-        // Already at root.
-      } else {
-        showDominatorTree(parent);  // Zoom out.
-      }
-    } else {
-      showDominatorTree(v);  // Zoom in.
-    }
-  };
-
-  let left = 0;
-  let top = 0;
-
-  const kPadding = 5;
-  const kBorder = 1;
-  left += kPadding - kBorder;
-  top += kPadding - kBorder;
-  width -= 2 * kPadding;
-  height -= 2 * kPadding;
-
-  div.title =
-    v.name +
-    " \\ntype: " + v.type +
-    " \\nretained: " + v.retainedSize +
-    " \\nshallow: " + v.shallowSize;
-
-  if (width < 10 || height < 10) {
-    // Too small: don't render label or children.
-    return div;
-  }
-
-  let label = v.name + " [" + prettySize(v.retainedSize) + "]";
-  div.appendChild(document.createTextNode(label));
-  const kLabelHeight = 9;
-  top += kLabelHeight;
-  height -= kLabelHeight;
-
-  if (depth > 2) {
-    // Too deep: don't render children.
-    return div;
-  }
-  if (width < 4 || height < 4) {
-    // Too small: don't render children.
-    return div;
-  }
-
-  let children = new Array();
-  v.children.forEach(function(c) {
-    // Size 0 children seem to confuse the layout algorithm (accumulating
-    // rounding errors?).
-    if (c.retainedSize > 0) {
-      children.push(c);
-    }
-  });
-  children.sort(function (a, b) {
-    return b.retainedSize - a.retainedSize;
-  });
-
-  const scale = width * height / v.retainedSize;
-
-  // Bruls M., Huizing K., van Wijk J.J. (2000) Squarified Treemaps. In: de
-  // Leeuw W.C., van Liere R. (eds) Data Visualization 2000. Eurographics.
-  // Springer, Vienna.
-  for (let rowStart = 0;  // Index of first child in the next row.
-       rowStart < children.length;) {
-    // Prefer wider rectangles, the better to fit text labels.
-    const GOLDEN_RATIO = 1.61803398875;
-    let verticalSplit = (width / height) > GOLDEN_RATIO;
-
-    let space;
-    if (verticalSplit) {
-      space = height;
-    } else {
-      space = width;
-    }
-
-    let rowMin = children[rowStart].retainedSize * scale;
-    let rowMax = rowMin;
-    let rowSum = 0;
-    let lastRatio = 0;
-
-    let rowEnd;  // One after index of last child in the next row.
-    for (rowEnd = rowStart; rowEnd < children.length; rowEnd++) {
-      let size = children[rowEnd].retainedSize * scale;
-      if (size < rowMin) rowMin = size;
-      if (size > rowMax) rowMax = size;
-      rowSum += size;
-
-      let ratio = Math.max((space * space * rowMax) / (rowSum * rowSum),
-                           (rowSum * rowSum) / (space * space * rowMin));
-      if ((lastRatio != 0) && (ratio > lastRatio)) {
-        // Adding the next child makes the aspect ratios worse: remove it and
-        // add the row.
-        rowSum -= size;
-        break;
-      }
-      lastRatio = ratio;
-    }
-
-    let rowLeft = left;
-    let rowTop = top;
-    let rowSpace = rowSum / space;
-
-    for (let i = rowStart; i < rowEnd; i++) {
-      let child = children[i];
-      let size = child.retainedSize * scale;
-
-      let childWidth;
-      let childHeight;
-      if (verticalSplit) {
-        childWidth = rowSpace;
-        childHeight = size / childWidth;
-      } else {
-        childHeight = rowSpace;
-        childWidth = size / childHeight;
-      }
-
-      let childDiv = createTreemapTile(child, childWidth, childHeight, depth + 1);
-      childDiv.style.left = rowLeft + "px";
-      childDiv.style.top = rowTop + "px";
-      // Oversize the final div by kBorder to make the borders overlap.
-      childDiv.style.width = (childWidth + kBorder) + "px";
-      childDiv.style.height = (childHeight + kBorder) + "px";
-      div.appendChild(childDiv);
-
-      if (verticalSplit)
-        rowTop += childHeight;
-      else
-        rowLeft += childWidth;
-    }
-
-    if (verticalSplit) {
-      left += rowSpace;
-      width -= rowSpace;
-    } else {
-      top += rowSpace;
-      height -= rowSpace;
-    }
-
-    rowStart = rowEnd;
-  }
-
-  return div;
-}
-
-function showDominatorTree(v) {
-  // Add the content div to the document first so the browser will calculate
-  // the available width and height.
-  let w = document.body.offsetWidth;
-  let h = document.body.offsetHeight;
-
-  let topTile = createTreemapTile(v, w, h, 0);
-  topTile.style.width = w;
-  topTile.style.height = h;
-  setBody(topTile);
-}
-
-function setBody(div) {
-  let body = document.body;
-  while (body.firstChild) {
-    body.removeChild(body.firstChild);
-  }
-  body.appendChild(div);
-}
-
-function setParents(v) {
-  v.children.forEach(function (child) {
-    child.parent = v;
-    setParents(child);
-  });
-}
-
-setParents(root);
-showDominatorTree(root);
-
-</script>
-</body>
-</html>
-""";