1#!/usr/bin/env python
2
3"""Calculate the costs of compiling headers; by considering how many
4times each header is included compared to the time to compile it.
5
6Requires two files:
7
81. Dependancy information for each target, so we can determine how
9   many times each header is included. For example if using Ninja:
10
11       $ ninja -t deps > deps.txt
12
132. List of costs of compiling each target. For example if using Ninja:
14
15       show_ninja_build_stats < .ninja_log > costs.txt
16
17Output:
18
19Prints a list of all headers which have both cost and dependancy
20information, of the form:
21
22   total_time, header, count, cost_to_compile
23
24total_time: 'count' * 'cost'. This attempts to measure the overall
25            cost of this header across the whole build.
26header: Path to header
27count: Number of targets which depend on this header (i.e. how many
28       times it is included)
29cost_to_compile: time in seconds to compile this header.
30
31
32By sorting by the first column one can identify potential build
33hotspots. Either reduce the number of times they are included; or the
34cost to compile to minimise the overall impact.
35"""
36
37from __future__ import print_function
38import collections
39import os
40import re
41import sys
42
43if len(sys.argv) != 3:
44    print("Usage: <deps_file> <header costs>", file=sys.stderr)
45    sys.exit(1)
46
47headers = collections.defaultdict(dict)
48
49with open(sys.argv[1]) as deps:
50    for line in deps:
51        # File consists of a paragraph for each target. Each paragraph
52        # is of the form:
53        #
54        # relative/path/to/target.cc.o: #deps ... extra details
55        #     ../path/to/dependancy1.cc
56        #     /path/to/dependancy2.h
57        # <blank line>
58        if '#deps' in line:
59            # Target name
60            (target, _x) = line.split(':', 1)
61            # Ignore targets which we don't want to count dependancies
62            # for - such as the '.h.cc' fake targets.
63            if target.endswith('.h.cc.o'):
64                target = None
65        elif line[0] != '\n':
66            # Dependancy name
67            dep = line.strip()
68            # Remove any '../XXX/' prefix (due to source -> build path
69            # conversion).
70            # Assumes that the build directory is located inside the source.
71            if dep.startswith('../'):
72                dep = dep[3:]
73            if 'count' not in headers[dep]:
74                headers[dep]['count'] = 0
75            headers[dep]['count'] += 1
76            if (dep.startswith('/usr/include/') or
77                dep.startswith('/Applications/Xcode.app')):
78                headers[dep]['system'] = True
79        else:
80            # Paragraph (target) separator.
81            target = None
82
83with open(sys.argv[2]) as costs:
84    for line in costs:
85        (cost, target) = line.split()
86        # Ignore the building of the .h.cc - that's just the cost to
87        # create a symlink.
88        if target.endswith('.h.cc'):
89            continue
90        # Fixup name of the .h.cc.o fake targets -> .h
91        if target.endswith('.h.cc.o'):
92            target = target[:-5]
93            target = re.sub('CMakeFiles/.*_obj.dir/', '', target)
94        headers[target]['cost'] = float(cost)
95
96for k,v in headers.items():
97    if 'count' in v:
98        if 'cost' in v:
99            total_cost = v['count'] * v['cost']
100            print(total_cost, k, v['count'], v['cost'])
101        else:
102            # No cost value - print warming if this is a high count
103            # header (and not system)
104            if v['count'] > 100 and 'system' not in v:
105                print(("Warning: No cost value for '{}' but has #include " +
106                       "count of {}").format(k, v['count']),
107                      file=sys.stderr)
108