mirror of
https://github.com/Zenithsiz/ftmemsim-valgrind.git
synced 2026-02-03 18:13:01 +00:00
- Move it to `auxprogs/`, alongside `pybuild.sh`. - Disable the annoying design lints, instead of just modifying the values (which often requires modifying them again later).
852 lines
28 KiB
Python
Executable File
852 lines
28 KiB
Python
Executable File
#! /usr/bin/env python3
|
|
# pyright: strict
|
|
|
|
# --------------------------------------------------------------------
|
|
# --- Cachegrind's annotator. cg_annotate.in ---
|
|
# --------------------------------------------------------------------
|
|
|
|
# This file is part of Cachegrind, a Valgrind tool for cache
|
|
# profiling programs.
|
|
#
|
|
# Copyright (C) 2002-2023 Nicholas Nethercote
|
|
# njn@valgrind.org
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License as
|
|
# published by the Free Software Foundation; either version 2 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# The GNU General Public License is contained in the file COPYING.
|
|
|
|
"""
|
|
This script reads Cachegrind output files and produces human-readable reports.
|
|
"""
|
|
|
|
# Use `make pyann` to "build" this script with `auxprogs/pybuild.rs` every time
|
|
# it is changed. This runs the formatters, type-checkers, and linters on
|
|
# `cg_annotate.in` and then generates `cg_annotate`.
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from argparse import ArgumentParser, BooleanOptionalAction, Namespace
|
|
from collections import defaultdict
|
|
from typing import DefaultDict, NewType, NoReturn, TextIO
|
|
|
|
|
|
class Args(Namespace):
|
|
"""
|
|
A typed wrapper for parsed args.
|
|
|
|
None of these fields are modified after arg parsing finishes.
|
|
"""
|
|
|
|
show: list[str]
|
|
sort: list[str]
|
|
threshold: float # a percentage
|
|
show_percs: bool
|
|
annotate: bool
|
|
context: int
|
|
include: list[str]
|
|
cgout_filename: list[str]
|
|
|
|
@staticmethod
|
|
def parse() -> Args:
|
|
def comma_separated_list(values: str) -> list[str]:
|
|
return values.split(",")
|
|
|
|
def threshold(n: str) -> float:
|
|
f = float(n)
|
|
if 0 <= f <= 20:
|
|
return f
|
|
raise ValueError
|
|
|
|
def add_bool_argument(
|
|
p: ArgumentParser, new_name: str, old_name: str, help_: str
|
|
) -> None:
|
|
"""
|
|
Add a bool argument that defaults to true.
|
|
|
|
Supports these forms: `--foo`, `--no-foo`, `--foo=yes`, `--foo=no`.
|
|
The latter two were the forms supported by the old Perl version of
|
|
`cg_annotate`, and are now deprecated.
|
|
"""
|
|
new_flag = "--" + new_name
|
|
old_flag = "--" + old_name
|
|
dest = new_name.replace("-", "_")
|
|
|
|
# Note: the default value is always printed with `BooleanOptionalAction`,
|
|
# due to an argparse bug: https://github.com/python/cpython/issues/83137.
|
|
p.add_argument(
|
|
new_flag,
|
|
default=True,
|
|
action=BooleanOptionalAction,
|
|
help=help_,
|
|
)
|
|
p.add_argument(
|
|
f"{old_flag}=yes",
|
|
dest=dest,
|
|
action="store_true",
|
|
help=f"(deprecated) same as --{new_name}",
|
|
)
|
|
p.add_argument(
|
|
f"{old_flag}=no",
|
|
dest=dest,
|
|
action="store_false",
|
|
help=f"(deprecated) same as --no-{new_name}",
|
|
)
|
|
|
|
p = ArgumentParser(description="Process a Cachegrind output file.")
|
|
|
|
p.add_argument("--version", action="version", version="%(prog)s-@VERSION@")
|
|
|
|
p.add_argument(
|
|
"--show",
|
|
type=comma_separated_list,
|
|
metavar="A,B,C",
|
|
help="only show figures for events A,B,C (default: all events)",
|
|
)
|
|
|
|
p.add_argument(
|
|
"--sort",
|
|
type=comma_separated_list,
|
|
metavar="A,B,C",
|
|
help="sort functions by events A,B,C (default: event column order)",
|
|
)
|
|
|
|
p.add_argument(
|
|
"--threshold",
|
|
type=threshold,
|
|
default=0.1,
|
|
metavar="N:[0,20]",
|
|
help="only show functions with more than N%% of primary sort event "
|
|
"counts (default: %(default)s)",
|
|
)
|
|
add_bool_argument(
|
|
p,
|
|
"show-percs",
|
|
"show-percs",
|
|
"show a percentage for each non-zero count",
|
|
)
|
|
add_bool_argument(
|
|
p,
|
|
"annotate",
|
|
"auto",
|
|
"annotate all source files containing functions that reached the "
|
|
"event count threshold",
|
|
)
|
|
p.add_argument(
|
|
"--context",
|
|
type=int,
|
|
default=8,
|
|
metavar="N",
|
|
help="print N lines of context before and after annotated lines "
|
|
"(default: %(default)s)",
|
|
)
|
|
p.add_argument(
|
|
"-I",
|
|
"--include",
|
|
action="append",
|
|
default=[],
|
|
metavar="D",
|
|
help="add D to the list of searched source file directories",
|
|
)
|
|
p.add_argument(
|
|
"cgout_filename",
|
|
nargs=1,
|
|
metavar="cachegrind-out-file",
|
|
help="file produced by Cachegrind",
|
|
)
|
|
|
|
return p.parse_args(namespace=Args())
|
|
|
|
|
|
# Args are stored in a global for easy access.
|
|
args = Args.parse()
|
|
|
|
|
|
# A single instance of this class is constructed, from `args` and the `events:`
|
|
# line in the cgout file.
|
|
class Events:
|
|
# The event names.
|
|
events: list[str]
|
|
|
|
# The order in which we must traverse events for --show. Can be shorter
|
|
# than `events`.
|
|
show_events: list[str]
|
|
|
|
# Like `show_events`, but indices into `events`, rather than names.
|
|
show_indices: list[int]
|
|
|
|
# The order in which we must traverse events for --sort. Can be shorter
|
|
# than `events`.
|
|
sort_events: list[str]
|
|
|
|
# Like `sort_events`, but indices into `events`, rather than names.
|
|
sort_indices: list[int]
|
|
|
|
def __init__(self, text: str) -> None:
|
|
self.events = text.split()
|
|
self.num_events = len(self.events)
|
|
|
|
# A temporary dict mapping events to indices, [0, n-1].
|
|
event_indices = {event: n for n, event in enumerate(self.events)}
|
|
|
|
# If --show is given, check it is valid. If --show is not given,
|
|
# default to all events in the standard order.
|
|
if args.show:
|
|
for event in args.show:
|
|
if event not in event_indices:
|
|
die(f"--show event `{event}` did not appear in `events:` line")
|
|
self.show_events = args.show
|
|
else:
|
|
self.show_events = self.events
|
|
|
|
self.show_indices = [event_indices[event] for event in self.show_events]
|
|
|
|
# Likewise for --sort.
|
|
if args.sort:
|
|
for event in args.sort:
|
|
if event not in event_indices:
|
|
die(f"--sort event `{event}` did not appear in `events:` line")
|
|
self.sort_events = args.sort
|
|
else:
|
|
self.sort_events = self.events
|
|
|
|
self.sort_indices = [event_indices[event] for event in self.sort_events]
|
|
|
|
def mk_cc(self, text: str) -> Cc:
|
|
"""Raises a `ValueError` exception on syntax error."""
|
|
# This is slightly faster than a list comprehension.
|
|
counts = list(map(int, text.split()))
|
|
|
|
if len(counts) == self.num_events:
|
|
pass
|
|
elif len(counts) < self.num_events:
|
|
# Add zeroes at the end for any missing numbers.
|
|
counts.extend([0] * (self.num_events - len(counts)))
|
|
else:
|
|
raise ValueError
|
|
|
|
return Cc(counts)
|
|
|
|
def mk_empty_cc(self) -> Cc:
|
|
# This is much faster than a list comprehension.
|
|
return Cc([0] * self.num_events)
|
|
|
|
|
|
class Cc:
|
|
"""
|
|
This is a dumb container for counts.
|
|
|
|
It doesn't know anything about events, i.e. what each count means. It can
|
|
do basic operations like `__iadd__` and `__eq__`, and anything more must be
|
|
done elsewhere. `Events.mk_cc` and `Events.mk_empty_cc` are used for
|
|
construction.
|
|
"""
|
|
|
|
# Always the same length as `Events.events`.
|
|
counts: list[int]
|
|
|
|
def __init__(self, counts: list[int]) -> None:
|
|
self.counts = counts
|
|
|
|
def __repr__(self) -> str:
|
|
return str(self.counts)
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if not isinstance(other, Cc):
|
|
return NotImplemented
|
|
return self.counts == other.counts
|
|
|
|
def __iadd__(self, other: Cc) -> Cc:
|
|
for i, other_count in enumerate(other.counts):
|
|
self.counts[i] += other_count
|
|
return self
|
|
|
|
|
|
# A paired filename and function name.
|
|
Flfn = NewType("Flfn", tuple[str, str])
|
|
|
|
# Per-function CCs.
|
|
DictFlfnCc = DefaultDict[Flfn, Cc]
|
|
|
|
# Per-line CCs, organised by filename and line number.
|
|
DictLineCc = DefaultDict[int, Cc]
|
|
DictFlDictLineCc = DefaultDict[str, DictLineCc]
|
|
|
|
|
|
def die(msg: str) -> NoReturn:
|
|
print("cg_annotate: error:", msg, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def read_cgout_file() -> tuple[str, str, Events, DictFlfnCc, DictFlDictLineCc, Cc]:
|
|
# The file format is described in Cachegrind's manual.
|
|
try:
|
|
cgout_file = open(args.cgout_filename[0], "r", encoding="utf-8")
|
|
except OSError as err:
|
|
die(f"{err}")
|
|
|
|
with cgout_file:
|
|
cgout_line_num = 0
|
|
|
|
def parse_die(msg: str) -> NoReturn:
|
|
die(f"{cgout_file.name}:{cgout_line_num}: {msg}")
|
|
|
|
def readline() -> str:
|
|
nonlocal cgout_line_num
|
|
cgout_line_num += 1
|
|
return cgout_file.readline()
|
|
|
|
# Read "desc:" lines.
|
|
desc = ""
|
|
while line := readline():
|
|
if m := re.match(r"desc:\s+(.*)", line):
|
|
desc += m.group(1) + "\n"
|
|
else:
|
|
break
|
|
|
|
# Read "cmd:" line. (`line` is already set from the "desc:" loop.)
|
|
if m := re.match(r"cmd:\s+(.*)", line):
|
|
cmd = m.group(1)
|
|
else:
|
|
parse_die("missing a `command:` line")
|
|
|
|
# Read "events:" line.
|
|
line = readline()
|
|
if m := re.match(r"events:\s+(.*)", line):
|
|
events = Events(m.group(1))
|
|
else:
|
|
parse_die("missing an `events:` line")
|
|
|
|
def mk_empty_dict_line_cc() -> DictLineCc:
|
|
return defaultdict(events.mk_empty_cc)
|
|
|
|
curr_fl = ""
|
|
curr_flfn = Flfn(("", ""))
|
|
|
|
# Different places where we accumulate CC data.
|
|
dict_flfn_cc: DictFlfnCc = defaultdict(events.mk_empty_cc)
|
|
dict_fl_dict_line_cc: DictFlDictLineCc = defaultdict(mk_empty_dict_line_cc)
|
|
summary_cc = None
|
|
|
|
# Compile the one hot regex.
|
|
count_pat = re.compile(r"(\d+)\s+(.*)")
|
|
|
|
# Line matching is done in order of pattern frequency, for speed.
|
|
while True:
|
|
line = readline()
|
|
|
|
if m := count_pat.match(line):
|
|
line_num = int(m.group(1))
|
|
try:
|
|
cc = events.mk_cc(m.group(2))
|
|
except ValueError:
|
|
parse_die("malformed or too many event counts")
|
|
|
|
# Record this CC at the function level.
|
|
flfn_cc = dict_flfn_cc[curr_flfn]
|
|
flfn_cc += cc
|
|
|
|
# Record this CC at the file/line level.
|
|
line_cc = dict_fl_dict_line_cc[curr_fl][line_num]
|
|
line_cc += cc
|
|
|
|
elif line.startswith("fn="):
|
|
curr_flfn = Flfn((curr_fl, line[3:-1]))
|
|
|
|
elif line.startswith("fl="):
|
|
curr_fl = line[3:-1]
|
|
# A `fn=` line should follow, overwriting the function name.
|
|
curr_flfn = Flfn((curr_fl, "<unspecified>"))
|
|
|
|
elif m := re.match(r"summary:\s+(.*)", line):
|
|
try:
|
|
summary_cc = events.mk_cc(m.group(1))
|
|
except ValueError:
|
|
parse_die("too many event counts")
|
|
|
|
elif line == "":
|
|
break # EOF
|
|
|
|
elif line == "\n" or line.startswith("#"):
|
|
# Skip empty lines and comment lines.
|
|
pass
|
|
|
|
else:
|
|
parse_die(f"malformed line: {line[:-1]}")
|
|
|
|
# Check if summary line was present.
|
|
if not summary_cc:
|
|
parse_die("missing `summary:` line, aborting")
|
|
|
|
# Check summary is correct.
|
|
total_cc = events.mk_empty_cc()
|
|
for flfn_cc in dict_flfn_cc.values():
|
|
total_cc += flfn_cc
|
|
if summary_cc != total_cc:
|
|
msg = (
|
|
"`summary:` line doesn't match computed total\n"
|
|
f"- summary: {summary_cc}\n"
|
|
f"- total: {total_cc}"
|
|
)
|
|
parse_die(msg)
|
|
|
|
return (desc, cmd, events, dict_flfn_cc, dict_fl_dict_line_cc, summary_cc)
|
|
|
|
|
|
class CcPrinter:
|
|
# Note: every `CcPrinter` gets the same `Events` object.
|
|
events: Events
|
|
|
|
# Note: every `CcPrinter` gets the same summary CC.
|
|
summary_cc: Cc
|
|
|
|
# The width of each event count column. (This column is also used for event
|
|
# names.) For simplicity, its length matches `events.events`, even though
|
|
# not all events are necessarily shown.
|
|
count_widths: list[int]
|
|
|
|
# The width of each percentage column. Zero if --show-percs is disabled.
|
|
# Its length matches `count_widths`.
|
|
perc_widths: list[int]
|
|
|
|
def __init__(self, events: Events, ccs: list[Cc], summary_cc: Cc) -> None:
|
|
self.events = events
|
|
self.summary_cc = summary_cc
|
|
|
|
# Find min and max value for each event. One of them will be the
|
|
# widest value.
|
|
min_cc = events.mk_empty_cc()
|
|
max_cc = events.mk_empty_cc()
|
|
for cc in ccs:
|
|
for i, _ in enumerate(events.events):
|
|
count = cc.counts[i]
|
|
if count > max_cc.counts[i]:
|
|
max_cc.counts[i] = count
|
|
elif count < min_cc.counts[i]:
|
|
min_cc.counts[i] = count
|
|
|
|
# Find maximum width for each column.
|
|
self.count_widths = [0] * events.num_events
|
|
self.perc_widths = [0] * events.num_events
|
|
for i, event in enumerate(events.events):
|
|
# Get count and perc widths of the min and max CCs.
|
|
(min_count, min_perc) = self.count_and_perc(min_cc, i)
|
|
(max_count, max_perc) = self.count_and_perc(max_cc, i)
|
|
|
|
# The event name goes in the count column.
|
|
self.count_widths[i] = max(len(min_count), len(max_count), len(event))
|
|
self.perc_widths[i] = max(len(min_perc), len(max_perc))
|
|
|
|
def print_events(self, suffix: str) -> None:
|
|
for i in self.events.show_indices:
|
|
# The event name goes in the count column.
|
|
event = self.events.events[i]
|
|
nwidth = self.count_widths[i]
|
|
pwidth = self.perc_widths[i]
|
|
empty_perc = ""
|
|
print(f"{event:<{nwidth}}{empty_perc:>{pwidth}} ", end="")
|
|
|
|
print(suffix)
|
|
|
|
def print_count_and_perc(self, i: int, count: str, perc: str) -> None:
|
|
nwidth = self.count_widths[i]
|
|
pwidth = self.perc_widths[i]
|
|
print(f"{count:>{nwidth}}{perc:>{pwidth}} ", end="")
|
|
|
|
def count_and_perc(self, cc: Cc, i: int) -> tuple[str, str]:
|
|
count = f"{cc.counts[i]:,d}" # commify
|
|
if args.show_percs:
|
|
if cc.counts[i] == 0:
|
|
# Don't show percentages for "0" entries, it's just clutter.
|
|
perc = ""
|
|
else:
|
|
summary_count = self.summary_cc.counts[i]
|
|
if summary_count == 0:
|
|
perc = " (n/a)"
|
|
else:
|
|
p = cc.counts[i] * 100 / summary_count
|
|
perc = f" ({p:.1f}%)"
|
|
else:
|
|
perc = ""
|
|
|
|
return (count, perc)
|
|
|
|
def print_cc(self, cc: Cc, suffix: str) -> None:
|
|
for i in self.events.show_indices:
|
|
(count, perc) = self.count_and_perc(cc, i)
|
|
self.print_count_and_perc(i, count, perc)
|
|
|
|
print("", suffix)
|
|
|
|
def print_missing_cc(self, suffix: str) -> None:
|
|
# Don't show percentages for "." entries, it's just clutter.
|
|
for i in self.events.show_indices:
|
|
self.print_count_and_perc(i, ".", "")
|
|
|
|
print("", suffix)
|
|
|
|
|
|
# Used in various places in the output.
|
|
def print_fancy(text: str) -> None:
|
|
fancy = "-" * 80
|
|
print(fancy)
|
|
print("--", text)
|
|
print(fancy)
|
|
|
|
|
|
def print_cachegrind_profile(desc: str, cmd: str, events: Events) -> None:
|
|
print_fancy("Cachegrind profile")
|
|
print(desc, end="")
|
|
print("Command: ", cmd)
|
|
print("Data file: ", args.cgout_filename[0])
|
|
print("Events recorded: ", *events.events)
|
|
print("Events shown: ", *events.show_events)
|
|
print("Event sort order:", *events.sort_events)
|
|
print("Threshold: ", args.threshold)
|
|
|
|
if len(args.include) == 0:
|
|
print("Include dirs: ")
|
|
else:
|
|
print(f"Include dirs: {args.include[0]}")
|
|
for include_dirname in args.include[1:]:
|
|
print(f" {include_dirname}")
|
|
|
|
print("Annotation: ", "on" if args.annotate else "off")
|
|
print()
|
|
|
|
|
|
def print_summary(events: Events, summary_cc: Cc) -> None:
|
|
printer = CcPrinter(events, [summary_cc], summary_cc)
|
|
print_fancy("Summary")
|
|
printer.print_events("")
|
|
print()
|
|
printer.print_cc(summary_cc, "PROGRAM TOTALS")
|
|
print()
|
|
|
|
|
|
def print_function_summary(
|
|
events: Events, dict_flfn_cc: DictFlfnCc, summary_cc: Cc
|
|
) -> set[str]:
|
|
# Only the first threshold percentage is actually used.
|
|
threshold_index = events.sort_indices[0]
|
|
|
|
# Convert the threshold from a percentage to an event count.
|
|
threshold = args.threshold * abs(summary_cc.counts[threshold_index]) / 100
|
|
|
|
def meets_threshold(flfn_and_cc: tuple[Flfn, Cc]) -> bool:
|
|
cc = flfn_and_cc[1]
|
|
return abs(cc.counts[threshold_index]) >= threshold
|
|
|
|
# Create a list with the counts in sort order, so that left-to-right list
|
|
# comparison does the right thing. Plus the `Flfn` at the end for
|
|
# deterministic output when all the event counts are identical in two CCs.
|
|
def key(flfn_and_cc: tuple[Flfn, Cc]) -> tuple[list[int], Flfn]:
|
|
cc = flfn_and_cc[1]
|
|
return ([abs(cc.counts[i]) for i in events.sort_indices], flfn_and_cc[0])
|
|
|
|
# Filter out functions for which the primary sort event count is below the
|
|
# threshold, and sort the remainder.
|
|
filtered_flfns_and_ccs = filter(meets_threshold, dict_flfn_cc.items())
|
|
sorted_flfns_and_ccs = sorted(filtered_flfns_and_ccs, key=key, reverse=True)
|
|
sorted_ccs = list(map(lambda flfn_and_cc: flfn_and_cc[1], sorted_flfns_and_ccs))
|
|
|
|
printer = CcPrinter(events, sorted_ccs, summary_cc)
|
|
print_fancy("Function summary")
|
|
printer.print_events(" file:function")
|
|
print()
|
|
|
|
# Print per-function counts.
|
|
for flfn, flfn_cc in sorted_flfns_and_ccs:
|
|
printer.print_cc(flfn_cc, f"{flfn[0]}:{flfn[1]}")
|
|
|
|
print()
|
|
|
|
# Files containing a function that met the threshold.
|
|
return set(flfn_and_cc[0][0] for flfn_and_cc in sorted_flfns_and_ccs)
|
|
|
|
|
|
class AnnotatedCcs:
|
|
line_nums_known_cc: Cc
|
|
line_nums_unknown_cc: Cc
|
|
unreadable_cc: Cc
|
|
below_threshold_cc: Cc
|
|
files_unknown_cc: Cc
|
|
|
|
labels = [
|
|
" annotated: files known & above threshold & readable, line numbers known",
|
|
" annotated: files known & above threshold & readable, line numbers unknown",
|
|
"unannotated: files known & above threshold & unreadable ",
|
|
"unannotated: files known & below threshold",
|
|
"unannotated: files unknown",
|
|
]
|
|
|
|
def __init__(self, events: Events) -> None:
|
|
self.line_nums_known_cc = events.mk_empty_cc()
|
|
self.line_nums_unknown_cc = events.mk_empty_cc()
|
|
self.unreadable_cc = events.mk_empty_cc()
|
|
self.below_threshold_cc = events.mk_empty_cc()
|
|
self.files_unknown_cc = events.mk_empty_cc()
|
|
|
|
def ccs(self) -> list[Cc]:
|
|
return [
|
|
self.line_nums_known_cc,
|
|
self.line_nums_unknown_cc,
|
|
self.unreadable_cc,
|
|
self.below_threshold_cc,
|
|
self.files_unknown_cc,
|
|
]
|
|
|
|
|
|
def mk_warning(msg: str) -> str:
|
|
return f"""\
|
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
|
@@ WARNING @@ WARNING @@ WARNING @@ WARNING @@ WARNING @@ WARNING @@ WARNING @@
|
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
|
{msg}\
|
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
|
"""
|
|
|
|
|
|
def warn_src_file_is_newer(src_filename: str, cgout_filename: str) -> None:
|
|
msg = f"""\
|
|
@ Source file '{src_filename}' is newer than data file '{cgout_filename}'.
|
|
@ Annotations may not be correct.
|
|
"""
|
|
print(mk_warning(msg))
|
|
|
|
|
|
def warn_bogus_lines(src_filename: str) -> None:
|
|
msg = f"""\
|
|
@@ Information recorded about lines past the end of '{src_filename}'.
|
|
"""
|
|
print(mk_warning(msg), end="")
|
|
|
|
|
|
def print_annotated_src_file(
|
|
events: Events,
|
|
dict_line_cc: DictLineCc,
|
|
src_file: TextIO,
|
|
annotated_ccs: AnnotatedCcs,
|
|
summary_cc: Cc,
|
|
) -> None:
|
|
# If the source file is more recent than the cgout file, issue warning.
|
|
if os.stat(src_file.name).st_mtime_ns > os.stat(args.cgout_filename[0]).st_mtime_ns:
|
|
warn_src_file_is_newer(src_file.name, args.cgout_filename[0])
|
|
|
|
printer = CcPrinter(events, list(dict_line_cc.values()), summary_cc)
|
|
# The starting fancy has already been printed by the caller.
|
|
printer.print_events("")
|
|
print()
|
|
|
|
# The CC for line 0 is special, holding counts attributed to the source
|
|
# file but not to any particular line (due to incomplete debug info).
|
|
# Annotate the start of the file with this info, if present.
|
|
line0_cc = dict_line_cc.pop(0, None)
|
|
if line0_cc:
|
|
suffix = "<unknown (line 0)>"
|
|
printer.print_cc(line0_cc, suffix)
|
|
annotated_ccs.line_nums_unknown_cc += line0_cc
|
|
print()
|
|
|
|
# Find interesting line ranges: all lines with a CC, and all lines within
|
|
# `args.context` lines of a line with a CC.
|
|
line_nums = list(sorted(dict_line_cc.keys()))
|
|
pairs: list[tuple[int, int]] = []
|
|
n = len(line_nums)
|
|
i = 0
|
|
context = args.context
|
|
while i < n:
|
|
lo = max(line_nums[i] - context, 1) # `max` to prevent negatives
|
|
while i < n - 1 and line_nums[i] + 2 * context >= line_nums[i + 1]:
|
|
i += 1
|
|
hi = line_nums[i] + context
|
|
pairs.append((lo, hi))
|
|
i += 1
|
|
|
|
def print_lines(pairs: list[tuple[int, int]]) -> None:
|
|
line_num = 0
|
|
while pairs:
|
|
src_line = ""
|
|
(lo, hi) = pairs.pop(0)
|
|
while line_num < lo - 1:
|
|
src_line = src_file.readline()
|
|
line_num += 1
|
|
if not src_line:
|
|
return # EOF
|
|
|
|
# Print line number, unless start of file.
|
|
if lo != 1:
|
|
print("-- line", lo, "-" * 40)
|
|
|
|
while line_num < hi:
|
|
src_line = src_file.readline()
|
|
line_num += 1
|
|
if not src_line:
|
|
return # EOF
|
|
if line_nums and line_num == line_nums[0]:
|
|
printer.print_cc(dict_line_cc[line_num], src_line[:-1])
|
|
annotated_ccs.line_nums_known_cc += dict_line_cc[line_num]
|
|
del line_nums[0]
|
|
else:
|
|
printer.print_missing_cc(src_line[:-1])
|
|
|
|
# Print line number.
|
|
print("-- line", hi, "-" * 40)
|
|
|
|
# Annotate chosen lines, tracking total annotated counts.
|
|
if pairs:
|
|
print_lines(pairs)
|
|
|
|
# If there was info on lines past the end of the file, warn.
|
|
if line_nums:
|
|
print()
|
|
for line_num in line_nums:
|
|
printer.print_cc(dict_line_cc[line_num], f"<bogus line {line_num}>")
|
|
annotated_ccs.line_nums_known_cc += dict_line_cc[line_num]
|
|
|
|
print()
|
|
warn_bogus_lines(src_file.name)
|
|
|
|
print()
|
|
|
|
|
|
# This (partially) consumes `dict_fl_dict_line_cc`.
|
|
def print_annotated_src_files(
|
|
events: Events,
|
|
ann_src_filenames: set[str],
|
|
dict_fl_dict_line_cc: DictFlDictLineCc,
|
|
summary_cc: Cc,
|
|
) -> AnnotatedCcs:
|
|
annotated_ccs = AnnotatedCcs(events)
|
|
|
|
def add_dict_line_cc_to_cc(dict_line_cc: DictLineCc | None, accum_cc: Cc) -> None:
|
|
if dict_line_cc:
|
|
for line_cc in dict_line_cc.values():
|
|
accum_cc += line_cc
|
|
|
|
# Exclude the unknown ("???") file, which is unannotatable.
|
|
ann_src_filenames.discard("???")
|
|
dict_line_cc = dict_fl_dict_line_cc.pop("???", None)
|
|
add_dict_line_cc_to_cc(dict_line_cc, annotated_ccs.files_unknown_cc)
|
|
|
|
# Prepend "" to the include dirnames so things work in the case where the
|
|
# filename has the full path.
|
|
include_dirnames = args.include.copy()
|
|
include_dirnames.insert(0, "")
|
|
|
|
def print_ann_fancy(src_filename: str) -> None:
|
|
print_fancy(f"Annotated source file: {src_filename}")
|
|
|
|
for src_filename in sorted(ann_src_filenames):
|
|
readable = False
|
|
for include_dirname in include_dirnames:
|
|
if include_dirname == "":
|
|
full_src_filename = src_filename
|
|
else:
|
|
full_src_filename = os.path.join(include_dirname, src_filename)
|
|
|
|
try:
|
|
with open(full_src_filename, "r", encoding="utf-8") as src_file:
|
|
dict_line_cc = dict_fl_dict_line_cc.pop(src_filename, None)
|
|
assert dict_line_cc is not None
|
|
print_ann_fancy(src_file.name) # includes full path
|
|
print_annotated_src_file(
|
|
events,
|
|
dict_line_cc,
|
|
src_file,
|
|
annotated_ccs,
|
|
summary_cc,
|
|
)
|
|
|
|
readable = True
|
|
break
|
|
except OSError:
|
|
pass
|
|
|
|
if not readable:
|
|
dict_line_cc = dict_fl_dict_line_cc.pop(src_filename, None)
|
|
add_dict_line_cc_to_cc(dict_line_cc, annotated_ccs.unreadable_cc)
|
|
|
|
print_ann_fancy(src_filename)
|
|
print("This file was unreadable")
|
|
print()
|
|
|
|
# Sum the CCs remaining in `dict_fl_dict_line_cc`, which are all in files
|
|
# below the threshold.
|
|
for dict_line_cc in dict_fl_dict_line_cc.values():
|
|
add_dict_line_cc_to_cc(dict_line_cc, annotated_ccs.below_threshold_cc)
|
|
|
|
return annotated_ccs
|
|
|
|
|
|
def print_annotation_summary(
|
|
events: Events,
|
|
annotated_ccs: AnnotatedCcs,
|
|
summary_cc: Cc,
|
|
) -> None:
|
|
# Show how many events were covered by annotated lines above.
|
|
printer = CcPrinter(events, annotated_ccs.ccs(), summary_cc)
|
|
print_fancy("Annotation summary")
|
|
printer.print_events("")
|
|
print()
|
|
|
|
total_cc = events.mk_empty_cc()
|
|
for (cc, label) in zip(annotated_ccs.ccs(), AnnotatedCcs.labels):
|
|
printer.print_cc(cc, label)
|
|
total_cc += cc
|
|
|
|
print()
|
|
|
|
# Internal sanity check.
|
|
if summary_cc != total_cc:
|
|
msg = (
|
|
"`summary:` line doesn't match computed annotated counts\n"
|
|
f"- summary: {summary_cc}\n"
|
|
f"- annotated: {total_cc}"
|
|
)
|
|
die(msg)
|
|
|
|
|
|
def main() -> None:
|
|
(
|
|
desc,
|
|
cmd,
|
|
events,
|
|
dict_flfn_cc,
|
|
dict_fl_dict_line_cc,
|
|
summary_cc,
|
|
) = read_cgout_file()
|
|
|
|
# Each of the following calls prints a section of the output.
|
|
|
|
print_cachegrind_profile(desc, cmd, events)
|
|
|
|
print_summary(events, summary_cc)
|
|
|
|
ann_src_filenames = print_function_summary(events, dict_flfn_cc, summary_cc)
|
|
|
|
if args.annotate:
|
|
annotated_ccs = print_annotated_src_files(
|
|
events, ann_src_filenames, dict_fl_dict_line_cc, summary_cc
|
|
)
|
|
|
|
print_annotation_summary(events, annotated_ccs, summary_cc)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|