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, '&')
+ .replace(/"/g, '"')
+ .replace(/</g, '<')
+ .replace(/>/g, '>');
+}
+</script>
+</head>
+<body onload='init()'>
+<div style='position: absolute; top: 5px; left: 5px;'>
+ <input type='button' onclick='showOptions()' value='Options & 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'> </span><input checked type='checkbox' id='check_0' value='A'>Global absolute (A)
+ <br><span class='swatch' id='swatch_1'> </span><input checked type='checkbox' id='check_1' value='B'>Global uninitialized data (B)
+ <br><span class='swatch' id='swatch_2'> </span><input checked type='checkbox' id='check_2' value='b'>Local uninitialized data (b)
+ <br><span class='swatch' id='swatch_3'> </span><input checked type='checkbox' id='check_3' value='C'>Global uninitialized common (C)
+ <br><span class='swatch' id='swatch_4'> </span><input checked type='checkbox' id='check_4' value='D'>Global initialized data (D)
+ <br><span class='swatch' id='swatch_5'> </span><input checked type='checkbox' id='check_5' value='d'>Local initialized data (d)
+ <br><span class='swatch' id='swatch_6'> </span><input checked type='checkbox' id='check_6' value='G'>Global small initialized data (G)
+ <br><span class='swatch' id='swatch_7'> </span><input checked type='checkbox' id='check_7' value='g'>Local small initialized data (g)
+ <br><span class='swatch' id='swatch_8'> </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'> </span><input checked type='checkbox' id='check_9' value='N'>Debugging (N)
+ <br><span class='swatch' id='swatch_10'> </span><input checked type='checkbox' id='check_10' value='p'>Stack unwind (p)
+ <br><span class='swatch' id='swatch_11'> </span><input checked type='checkbox' id='check_11' value='R'>Global read-only data (R)
+ <br><span class='swatch' id='swatch_12'> </span><input checked type='checkbox' id='check_12' value='r'>Local read-only data (r)
+ <br><span class='swatch' id='swatch_13'> </span><input checked type='checkbox' id='check_13' value='S'>Global small uninitialized data (S)
+ <br><span class='swatch' id='swatch_14'> </span><input checked type='checkbox' id='check_14' value='s'>Local small uninitialized data (s)
+ <br><span class='swatch' id='swatch_15'> </span><input checked type='checkbox' id='check_15' value='T'>Global code (T)
+ <br><span class='swatch' id='swatch_16'> </span><input checked type='checkbox' id='check_16' value='t'>Local code (t)
+ <br><span class='swatch' id='swatch_17'> </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'> </span><input checked type='checkbox' id='check_18' value='u'>Unique (u)
+ <br><span class='swatch' id='swatch_19'> </span><input checked type='checkbox' id='check_19' value='V'>Global weak object (V)
+ <br><span class='swatch' id='swatch_20'> </span><input checked type='checkbox' id='check_20' value='v'>Local weak object (v)
+ <br><span class='swatch' id='swatch_21'> </span><input checked type='checkbox' id='check_21' value='W'>Global weak symbol (W)
+ <br><span class='swatch' id='swatch_22'> </span><input checked type='checkbox' id='check_22' value='w'>Local weak symbol (w)
+ <br><span class='swatch' id='swatch_23'> </span><input checked type='checkbox' id='check_23' value='@'>Vtable entry (@)
+ <br><span class='swatch' id='swatch_24'> </span><input checked type='checkbox' id='check_24' value='-'>STABS debugging (-)
+ <br><span class='swatch' id='swatch_25'> </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;'>
+ x <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>
-""";