Test Failed
Push — master ( c8b2d2...36ef11 )
by Jan
01:40 queued 16s
created

utils.build_profiler_report   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 206
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 124
dl 0
loc 206
ccs 0
cts 116
cp 0
rs 8.96
c 0
b 0
f 0
wmc 43

7 Functions

Rating   Name   Duplication   Size   Complexity  
A format_time() 0 9 3
A generate_webtreemap() 0 10 5
A get_total_time() 0 6 2
A get_total_time_intersect() 0 7 3
F print_report() 0 100 22
A load_log_file() 0 22 5
A main() 0 28 3

How to fix   Complexity   

Complexity

Complex classes like utils.build_profiler_report often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/python3
2
"""Compare and present build times to user and generate an HTML interactive graph"""
3
import sys
4
import argparse
5
import re
6
7
8
def load_log_file(file) -> dict:
9
    """Loads Targets and their durations from ninja logfile `file` and returns them in a dict"""
10
11
    with open(file, 'r') as file_pointer:
12
        lines = file_pointer.read().splitlines()
13
14
    # {Target: duration} dict
15
    target_duration_dict = {}
16
    for line in lines:
17
        line = line.strip()
18
19
        # pattern to match target names that are an absolute path - these should be skipped
20
        # --> an issue appeared with new versions of cmake where the targets are duplicated for some
21
        # reason and therefore they must be filtered here
22
        duplicate_pattern = re.compile("[0-9]+\s+[0-9]+\s+[0-9]+\s+/.*")
23
        if not line.startswith('#') and not duplicate_pattern.match(line):
24
            # calculate target compilation duration and add it to dict
25
            line = line.split()
26
            duration = int(line[1]) - int(line[0])
27
            target_duration_dict[line[3]] = duration
28
29
    return target_duration_dict
30
31
32
def format_time(time):
33
    """Converts a time into a human-readable format"""
34
35
    time /= 1000
36
    if time < 60:
37
        return '%.1fs' % time
38
    if time < 60 * 60:
39
        return '%dm%.1fs' % (time / 60, time % 60)
40
    return '%dh%dm%.1fs' % (time / (60 * 60), time % (60 * 60) / 60, time % 60)
41
42
43
def generate_webtreemap(current_dict: dict, baseline_dict: dict, logfile: str) -> None:
44
    """Create file for webtreemap to generate an HTML from; if target is new, append _NEW"""
45
    with open(logfile + ".webtreemap", 'w') as file_pointer:
46
        for target in current_dict.keys():
47
            new_tag = ''
48
            if baseline_dict and target not in baseline_dict.keys():
49
                new_tag = '_NEW'
50
51
            file_pointer.write(str(current_dict[target]) + ' ' + target + '_'
52
                               + format_time(current_dict[target]) + new_tag + '\n')
53
54
55
def get_total_time(target_duration_dict: dict) -> int:
56
    """Return sum of durations for all targets in dict"""
57
    total_time = 0
58
    for target in target_duration_dict.keys():
59
        total_time += target_duration_dict[target]
60
    return total_time
61
62
63
def get_total_time_intersect(target_duration_dict_a: dict, target_duration_dict_b: dict) -> int:
64
    """Return sum of durations for targets in A that are also in B"""
65
    total_time = 0
66
    for target in target_duration_dict_a.keys():
67
        if target in target_duration_dict_b.keys():
68
            total_time += target_duration_dict_a[target]
69
    return total_time
70
71
72
def print_report(current_dict: dict, baseline_dict: dict = None) -> None:
73
    """Print report with results of profiling to stdout"""
74
75
    # If the targets/outputfiles have changed between baseline and current, we are using
76
    # total_time_intersect to calculate delta (ratio of durations of targets) instead of total_time
77
    if baseline_dict and baseline_dict.keys() != current_dict.keys():
78
        msg = ("Warning: the targets in the current logfile differ from those in the baseline"
79
               "logfile; therefore the time and time percentage deltas TD and %TD for each target"
80
               "as well as for the entire build are calculated without taking the added/removed"
81
               "targets into account, but the total build time at the end does take them into"
82
               "account. If the added/removed targets modify the behavior of targets in both"
83
               "logfiles, the D delta may not make sense.\n-----\n")
84
        print(msg)
85
        target_mismatch = True
86
        total_time_current_intersect = get_total_time_intersect(current_dict, baseline_dict)
87
        total_time_baseline_intesect = get_total_time_intersect(baseline_dict, current_dict)
88
    else:
89
        target_mismatch = False
90
91
    header = [f'{"Target:":60}', f"{'%':4}", f"{'D':5}", f"{'T':8}",
92
              f"{'TD':8}", f"{'%TD':5}", "Note"]
93
    print(' | '.join(header))
94
95
    total_time_current = get_total_time(current_dict)
96
    if baseline_dict:
97
        total_time_baseline = get_total_time(baseline_dict)
98
99
    # sort targets/outputfiles by % taken of build time
100
    current_dict = dict(sorted(current_dict.items(), key=lambda item: item[1], reverse=True))
101
102
    for target in current_dict.keys():
103
        # percentage of build time that the target took
104
        perc = current_dict[target]/total_time_current * 100
105
106
        # difference between perc in current and in baseline
107
        delta = 0
108
        if baseline_dict:
109
            if target_mismatch:
110
                if target in baseline_dict.keys():
111
                    delta = current_dict[target]/total_time_current_intersect * 100 - \
0 ignored issues
show
introduced by
The variable total_time_current_intersect does not seem to be defined for all execution paths.
Loading history...
112
                        baseline_dict[target]/total_time_baseline_intesect * 100
0 ignored issues
show
introduced by
The variable total_time_baseline_intesect does not seem to be defined for all execution paths.
Loading history...
113
            else:
114
                delta = perc - (baseline_dict[target]/total_time_baseline * 100)
0 ignored issues
show
introduced by
The variable total_time_baseline does not seem to be defined in case baseline_dict on line 96 is False. Are you sure this can never be the case?
Loading history...
115
            if abs(delta) < 0.1:
116
                delta = 0
117
118
        # time is the formatted build time of the target
119
        time = format_time(current_dict[target])
120
121
        # time_delta is the formatted time difference between current and baseline
122
        if baseline_dict and target in baseline_dict.keys():
123
            time_delta = current_dict[target] - baseline_dict[target]
124
            if abs(time_delta) < 60:
125
                time_delta = 0
126
            time_delta = format_time(time_delta)
127
        else:
128
            time_delta = 0
129
130
        # perc_time_delta is a percentage difference of before and after build times
131
        if baseline_dict and target in baseline_dict.keys():
132
            perc_time_delta = (current_dict[target]/baseline_dict[target]) * 100 - 100
133
        else:
134
            perc_time_delta = 0
135
136
        line = [f'{target:60}', f"{perc:4.1f}", f"{delta:5.1f}", f"{time:>8}",
137
                f"{time_delta:>8}", f"{perc_time_delta:5.1f}"]
138
        # if target was not in baseline, append note
139
        if baseline_dict and target not in baseline_dict.keys():
140
            line.append("Not in baseline")
141
        print(' | '.join(line))
142
143
    # Print time and % delta of the whole build time
144
    if baseline_dict:
145
        # total_perc_time_delta is the percentage change of build times between current and baseline
146
        total_time_delta = total_time_current - total_time_baseline
147
        if abs(total_time_delta) < 60:
148
            total_time_delta = 0
149
        total_time_delta = format_time(total_time_delta)
150
        total_perc_time_delta = (total_time_current / total_time_baseline) * 100 - 100
151
        line = ["-----\nTotal time:", format_time(total_time_current),
152
                "| TD", f'{total_time_delta:>8}', "| %TD", f'{total_perc_time_delta:+5.1f}']
153
        # if there are different targets in current and baseline log, add intersect deltas,
154
        # which compare build times while omitting conficting build targets
155
        if target_mismatch:
156
            intersect_time_delta = total_time_current_intersect - total_time_baseline_intesect
157
            if abs(intersect_time_delta) < 60:
158
                intersect_time_delta = 0
159
            intersect_time_delta = format_time(intersect_time_delta)
160
            line.append(f'| intersect TD {intersect_time_delta:>8}')
161
            intersect_perc_time_delta = (total_time_current_intersect /
162
                                         total_time_baseline_intesect) * 100 - 100
163
            line.append(f'| intersect %TD {intersect_perc_time_delta:+5.1f}')
164
        print(' '.join(line))
165
    else:
166
        print("-----\nTotal time:", format_time(total_time_current))
167
168
    # Print targets which are present in baseline but not in current log
169
    if baseline_dict:
170
        removed = [target for target in baseline_dict.keys() if target not in current_dict.keys()]
171
        print("-----\nTargets omitted from baseline:\n", '\n'.join(removed))
172
173
174
def main() -> None:
175
    """Parse args, check for python version, then generate webtreemap HTML and print report"""
176
177
    # Dict key order used by print_report only specified in 3.7.0+
178
    if sys.version_info < (3, 7, 0):
179
        sys.stderr.write("You need python 3.7 or later to run this script\n")
180
        sys.exit(1)
181
182
    # Parse arguments
183
    parser = argparse.ArgumentParser(description='Create .webtreemap and print profiling report',
184
                                     usage='./build_profiler_report.py <logflie> [--baseline]')
185
    parser.add_argument('logfile', type=str, help='Ninja logfile to compare against baseline')
186
    parser.add_argument('--baseline', dest='skipBaseline', action='store_const',
187
                        const=True, default=False, help='Do not compare logfile with baseline log \
188
                        (default: compare baseline logfile with current logfile)')
189
190
    args = parser.parse_args()
191
192
    if args.skipBaseline:
193
        baseline_dict = None
194
    else:
195
        baseline_dict = load_log_file("0.ninja_log")
196
197
    logfile = sys.argv[1]
198
    current_dict = load_log_file(sys.argv[1])
199
200
    generate_webtreemap(current_dict, baseline_dict, logfile)
201
    print_report(current_dict, baseline_dict)
202
203
204
if __name__ == "__main__":
205
    main()
206