utils/build_profiler_report.py
#!/usr/bin/python3"""Compare and present build times to user and generate an HTML interactive graph"""import sysimport argparse def load_log_file(file) -> dict: """Loads Targets and their durations from ninja logfile `file` and returns them in a dict""" with open(file, 'r') as file_pointer: lines = file_pointer.read().splitlines() # {Target: duration} dict target_duration_dict = {} # an issue appeared with new versions of cmake where the targets are duplicated with a relative # and absolute path and therefore they must be filtered here; filters comments too lines = [line for line in lines if not line.strip().split()[3].startswith('/') and not line.startswith('#')] splitLines = [line.strip().split() for line in lines] # calculate target compilation duration and add it to dict hash_target_duration_dict = {} for line in splitLines: duration = int(line[1]) - int(line[0]) hash = line[2] target = line[3] # filter out lines with duplicate times and concatenate target names if hash not in hash_target_duration_dict: target_duration_dict[target] = duration # add target,duration with new hash hash_target_duration_dict[hash] = (target, duration) else: previous_target = hash_target_duration_dict[hash][0] # concatenate previous target with same hash to this one concated_target = previous_target + ";" + target # remove old target entry and add concatenated target = duraiton target_duration_dict[concated_target] = \ target_duration_dict.pop(previous_target) # update target name in hash dict hash_target_duration_dict[hash] = (concated_target, duration) return target_duration_dict def format_time(time): """Converts a time into a human-readable format""" time /= 1000 if time < 60: return '%.1fs' % time if time < 60 * 60: return '%dm%.1fs' % (time / 60, time % 60) return '%dh%dm%.1fs' % (time / (60 * 60), time % (60 * 60) / 60, time % 60) def generate_webtreemap(current_dict: dict, baseline_dict: dict, logfile: str) -> None: """Create file for webtreemap to generate an HTML from; if target is new, append _NEW""" with open(logfile + ".webtreemap", 'w') as file_pointer: for target in current_dict.keys(): new_tag = '' if baseline_dict and target not in baseline_dict.keys(): new_tag = '_NEW' file_pointer.write(str(current_dict[target]) + ' ' + target + '_' + format_time(current_dict[target]) + new_tag + '\n') def get_total_time(target_duration_dict: dict) -> int: """Return sum of durations for all targets in dict""" total_time = 0 for target in target_duration_dict.keys(): total_time += target_duration_dict[target] return total_time def get_total_time_intersect(target_duration_dict_a: dict, target_duration_dict_b: dict) -> int: """Return sum of durations for targets in A that are also in B""" total_time = 0 for target in target_duration_dict_a.keys(): if target in target_duration_dict_b.keys(): total_time += target_duration_dict_a[target] return total_time Function `print_report` has a Cognitive Complexity of 42 (exceeds 7 allowed). Consider refactoring.
Cyclomatic complexity is too high in function print_report. (24)
Refactor this function to reduce its Cognitive Complexity from 44 to the 15 allowed.def print_report(current_dict: dict, baseline_dict: dict = None) -> None: """Print report with results of profiling to stdout""" # If the targets/outputfiles have changed between baseline and current, we are using # total_time_intersect to calculate delta (ratio of durations of targets) instead of total_time if baseline_dict and baseline_dict.keys() != current_dict.keys(): msg = ("Warning: the targets in the current logfile differ from those in the baseline" "logfile; therefore the time and time percentage deltas TD and %TD for each target" "as well as for the entire build are calculated without taking the added/removed" "targets into account, but the total build time at the end does take them into" "account. If the added/removed targets modify the behavior of targets in both" "logfiles, the D delta may not make sense.\n-----\n") print(msg) target_mismatch = True total_time_current_intersect = get_total_time_intersect(current_dict, baseline_dict) total_time_baseline_intesect = get_total_time_intersect(baseline_dict, current_dict) else: target_mismatch = False header = [f'{"Target:":60}', f"{'%':4}", f"{'D':5}", f"{'T':8}", f"{'TD':8}", f"{'%TD':5}", "Note"] print(' | '.join(header)) total_time_current = get_total_time(current_dict) if baseline_dict: total_time_baseline = get_total_time(baseline_dict) # sort targets/outputfiles by % taken of build time current_dict = dict(sorted(current_dict.items(), key=lambda item: item[1], reverse=True)) for target in current_dict.keys(): # percentage of build time that the target took perc = current_dict[target]/total_time_current * 100 # difference between perc in current and in baseline delta = 0 if baseline_dict: if target_mismatch: if target in baseline_dict.keys(): delta = current_dict[target]/total_time_current_intersect * 100 - \ baseline_dict[target]/total_time_baseline_intesect * 100 else: delta = perc - (baseline_dict[target]/total_time_baseline * 100) if abs(delta) < 0.1: delta = 0 # time is the formatted build time of the target time = format_time(current_dict[target]) # time_delta is the formatted time difference between current and baseline if baseline_dict and target in baseline_dict.keys(): time_delta = current_dict[target] - baseline_dict[target] if abs(time_delta) < 60: time_delta = 0 time_delta = format_time(time_delta) else: time_delta = 0 # perc_time_delta is a percentage difference of before and after build times if baseline_dict and target in baseline_dict.keys(): perc_time_delta = (current_dict[target]/baseline_dict[target]) * 100 - 100 else: perc_time_delta = 0 line = [f'{target:60}', f"{perc:4.1f}", f"{delta:5.1f}", f"{time:>8}", f"{time_delta:>8}", f"{perc_time_delta:5.1f}"] # if target was not in baseline, append note if baseline_dict and target not in baseline_dict.keys(): line.append("Not in baseline") # if target has multiple output files, print them on separate lines # (times only on the last line) if(';' in target): print("\n".join(target.rsplit(';', 1)[0].split(';'))) split_target = target.rsplit(';', 1)[1] line[0] = f'{split_target:60}' print(' | '.join(line)) # Print time and % delta of the whole build time if baseline_dict:Line too long (100 > 99 characters) # total_perc_time_delta is the percentage change of build times between current and baseline total_time_delta = total_time_current - total_time_baseline if abs(total_time_delta) < 60: total_time_delta = 0 total_time_delta = format_time(total_time_delta) total_perc_time_delta = (total_time_current / total_time_baseline) * 100 - 100 line = ["-----\nTotal time:", format_time(total_time_current), "| TD", f'{total_time_delta:>8}', "| %TD", f'{total_perc_time_delta:+5.1f}'] # if there are different targets in current and baseline log, add intersect deltas, # which compare build times while omitting conficting build targets if target_mismatch: intersect_time_delta = total_time_current_intersect - total_time_baseline_intesect if abs(intersect_time_delta) < 60: intersect_time_delta = 0 intersect_time_delta = format_time(intersect_time_delta) line.append(f'| intersect TD {intersect_time_delta:>8}') intersect_perc_time_delta = (total_time_current_intersect / total_time_baseline_intesect) * 100 - 100 line.append(f'| intersect %TD {intersect_perc_time_delta:+5.1f}') print(' '.join(line)) else: print("-----\nTotal time:", format_time(total_time_current)) # Print targets which are present in baseline but not in current log if baseline_dict: removed = [target for target in baseline_dict.keys() if target not in current_dict.keys()] print("-----\nTargets omitted from baseline:\n", '\n'.join(removed)) def main() -> None: """Parse args, check for python version, then generate webtreemap HTML and print report""" # Dict key order used by print_report only specified in 3.7.0+ if sys.version_info < (3, 7, 0): sys.stderr.write("You need python 3.7 or later to run this script\n") sys.exit(1) # Parse arguments parser = argparse.ArgumentParser(description='Create .webtreemap and print profiling report', usage='./build_profiler_report.py <logflie> [--baseline]') parser.add_argument('logfile', type=str, help='Ninja logfile to compare against baseline') parser.add_argument('--baseline', dest='skipBaseline', action='store_const', const=True, default=False, help='Do not compare logfile with baseline log \ (default: compare baseline logfile with current logfile)') args = parser.parse_args() if args.skipBaseline: baseline_dict = None else: baseline_dict = load_log_file("0.ninja_log") logfile = sys.argv[1] current_dict = load_log_file(sys.argv[1]) generate_webtreemap(current_dict, baseline_dict, logfile) print_report(current_dict, baseline_dict) if __name__ == "__main__": main()