chromium/tools/unused-symbols-report.py

#!/usr/bin/env python
# Copyright 2011 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Prints a report of symbols stripped by the linker due to being unused.

To use, build with these linker flags:
  -Wl,--gc-sections
  -Wl,--print-gc-sections
the first one is the default in Release; search build/common.gypi for it
and to see where to add the other.

Then build, saving the output into a file:
  make chrome 2>&1 | tee buildlog
and run this script on it:
  ./tools/unused-symbols-report.py buildlog > report.html
"""

from __future__ import print_function

import cgi
import optparse
import os
import re
import subprocess
import sys

cppfilt_proc = None
def Demangle(sym):
  """Demangle a C++ symbol by passing it through c++filt."""
  global cppfilt_proc
  if cppfilt_proc is None:
    cppfilt_proc = subprocess.Popen(['c++filt'], stdin=subprocess.PIPE,
                                    stdout=subprocess.PIPE)
  print(sym, file=cppfilt_proc.stdin)
  return cppfilt_proc.stdout.readline().strip()


def Unyuck(sym):
  """Attempt to prettify a C++ symbol by some basic heuristics."""
  sym = sym.replace('std::basic_string<char, std::char_traits<char>, '
                    'std::allocator<char> >', 'std::string')
  sym = sym.replace('std::basic_string<wchar_t, std::char_traits<wchar_t>, '
                    'std::allocator<wchar_t> >', 'std::wstring')
  sym = sym.replace(
      'std::basic_string<char16_t, std::char_traits<char16_t>, '
      'std::allocator<char16_t> >', 'std::u16string')
  sym = re.sub(r', std::allocator<\S+\s+>', '', sym)
  return sym


def Parse(input, skip_paths=None, only_paths=None):
  """Parse the --print-gc-sections build output.

  Args:
    input: iterable over the lines of the build output

  Yields:
    (target name, path to .o file, demangled symbol)
  """
  symbol_re = re.compile(r"'\.text\.(\S+)' in file '(\S+)'$")
  path_re = re.compile(r"^out/[^/]+/[^/]+/([^/]+)/(.*)$")
  for line in input:
    match = symbol_re.search(line)
    if not match:
      continue
    symbol, path = match.groups()
    symbol = Unyuck(Demangle(symbol))
    path = os.path.normpath(path)
    if skip_paths and skip_paths in path:
      continue
    if only_paths and only_paths not in path:
      continue
    match = path_re.match(path)
    if not match:
      print("Skipping weird path", path, file=sys.stderr)
      continue
    target, path = match.groups()
    yield target, path, symbol


# HTML header for our output page.
TEMPLATE_HEADER = """<!DOCTYPE html>
<head>
<style>
body {
  font-family: sans-serif;
  font-size: 0.8em;
}
h1, h2 {
  font-weight: normal;
  margin: 0.5em 0;
}
h2 {
  margin-top: 1em;
}
tr:hover {
  background: #eee;
}
.permalink {
  padding-left: 1ex;
  font-size: 80%;
  text-decoration: none;
  color: #ccc;
}
.symbol {
  font-family: WebKitWorkAround, monospace;
  margin-left: 4ex;
  text-indent: -4ex;
  padding: 0.5ex 1ex;
}
.file {
  padding: 0.5ex 1ex;
  padding-left: 2ex;
  font-family: WebKitWorkAround, monospace;
  font-size: 90%;
  color: #777;
}
</style>
</head>
<body>
<h1>chrome symbols deleted at link time</h1>
"""


def Output(iter):
  """Print HTML given an iterable of (target, path, symbol) tuples."""
  targets = {}
  for target, path, symbol in iter:
    entries = targets.setdefault(target, [])
    entries.append((symbol, path))

  print(TEMPLATE_HEADER)
  print("<p>jump to target:")
  print("<select onchange='document.location.hash = this.value'>")
  for target in sorted(targets.keys()):
    print("<option>%s</option>" % target)
  print("</select></p>")

  for target in sorted(targets.keys()):
    print("<h2>%s" % target)
    print("<a class=permalink href='#%s' name='%s'>#</a>" % (target, target))
    print("</h2>")
    print("<table width=100% cellspacing=0>")
    for symbol, path in sorted(targets[target]):
      htmlsymbol = cgi.escape(symbol).replace('::', '::<wbr>')
      print("<tr><td><div class=symbol>%s</div></td>" % htmlsymbol)
      print("<td valign=top><div class=file>%s</div></td></tr>" % path)
    print("</table>")


def main():
  parser = optparse.OptionParser(usage='%prog [options] buildoutput\n\n' +
                                 __doc__)
  parser.add_option("--skip-paths", metavar="STR", default="third_party",
                    help="skip paths matching STR [default=%default]")
  parser.add_option("--only-paths", metavar="STR",
                    help="only include paths matching STR [default=%default]")
  opts, args = parser.parse_args()

  if len(args) < 1:
    parser.print_help()
    sys.exit(1)

  iter = Parse(open(args[0]),
               skip_paths=opts.skip_paths,
               only_paths=opts.only_paths)
  Output(iter)


if __name__ == '__main__':
  main()