Darwin: another csrctl change
[valgrind.git] / cachegrind / cg_merge.in
blob7cc0f2b14b734585d46566a00ef7c63e4ba5c30b
1 #! /usr/bin/env python3
2 # pyright: strict
4 # --------------------------------------------------------------------
5 # --- Cachegrind's merger.                             cg_merge.in ---
6 # --------------------------------------------------------------------
8 # This file is part of Cachegrind, a high-precision tracing profiler
9 # built with Valgrind.
11 # Copyright (C) 2002-2023 Nicholas Nethercote
12 #    njn@valgrind.org
14 # This program is free software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License as
16 # published by the Free Software Foundation; either version 2 of the
17 # License, or (at your option) any later version.
19 # This program is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
22 # General Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, see <http://www.gnu.org/licenses/>.
27 # The GNU General Public License is contained in the file COPYING.
29 # This script merges Cachegrind output files.
31 # Use `make pymerge` to "build" this script every time it is changed. This runs
32 # the formatters, type-checkers, and linters on `cg_merge.in` and then
33 # generates `cg_merge`.
35 # This is a cut-down version of `cg_annotate.in`.
37 from __future__ import annotations
39 import re
40 import sys
41 from argparse import ArgumentParser, Namespace
42 from collections import defaultdict
43 from typing import DefaultDict, NoReturn, TextIO
46 # A typed wrapper for parsed args.
47 class Args(Namespace):
48     # None of these fields are modified after arg parsing finishes.
49     output: str
50     cgout_filename: list[str]
52     @staticmethod
53     def parse() -> Args:
54         desc = (
55             "Merge multiple Cachegrind output files. Deprecated; use "
56             "`cg_annotate` with multiple Cachegrind output files instead."
57         )
58         p = ArgumentParser(description=desc)
60         p.add_argument("--version", action="version", version="%(prog)s-@VERSION@")
62         p.add_argument(
63             "-o",
64             dest="output",
65             type=str,
66             metavar="FILE",
67             help="output file (default: stdout)",
68         )
70         p.add_argument(
71             "cgout_filename",
72             nargs="+",
73             metavar="cachegrind-out-file",
74             help="file produced by Cachegrind",
75         )
77         return p.parse_args(namespace=Args())  # type: ignore [return-value]
80 # Args are stored in a global for easy access.
81 args = Args.parse()
84 # A single instance of this class is constructed, from `args` and the `events:`
85 # line in the cgout file.
86 class Events:
87     # The event names.
88     events: list[str]
90     def __init__(self, text: str) -> None:
91         self.events = text.split()
92         self.num_events = len(self.events)
94     # Raises a `ValueError` exception on syntax error.
95     def mk_cc(self, str_counts: list[str]) -> Cc:
96         # This is slightly faster than a list comprehension.
97         counts = list(map(int, str_counts))
99         if len(counts) == self.num_events:
100             pass
101         elif len(counts) < self.num_events:
102             # Add zeroes at the end for any missing numbers.
103             counts.extend([0] * (self.num_events - len(counts)))
104         else:
105             raise ValueError
107         return counts
109     def mk_empty_cc(self) -> Cc:
110         # This is much faster than a list comprehension.
111         return [0] * self.num_events
114 # A "cost centre", which is a dumb container for counts. Always the same length
115 # as `Events.events`, but it doesn't even know event names. `Events.mk_cc` and
116 # `Events.mk_empty_cc` are used for construction.
118 # This used to be a class with a single field `counts: list[int]`, but this
119 # type is very hot and just using a type alias is much faster.
120 Cc = list[int]
123 # Add the counts in `a_cc` to `b_cc`.
124 def add_cc_to_cc(a_cc: Cc, b_cc: Cc) -> None:
125     for i, a_count in enumerate(a_cc):
126         b_cc[i] += a_count
129 # Per-line CCs, organised by filename, function name, and line number.
130 DictLineCc = DefaultDict[int, Cc]
131 DictFnDictLineCc = DefaultDict[str, DictLineCc]
132 DictFlDictFnDictLineCc = DefaultDict[str, DictFnDictLineCc]
135 def die(msg: str) -> NoReturn:
136     print("cg_merge: error:", msg, file=sys.stderr)
137     sys.exit(1)
140 def read_cgout_file(
141     cgout_filename: str,
142     is_first_file: bool,
143     cumul_dict_fl_dict_fn_dict_line_cc: DictFlDictFnDictLineCc,
144     cumul_summary_cc: Cc,
145 ) -> tuple[list[str], str, Events]:
146     # The file format is described in Cachegrind's manual.
147     try:
148         cgout_file = open(cgout_filename, "r", encoding="utf-8")
149     except OSError as err:
150         die(f"{err}")
152     with cgout_file:
153         cgout_line_num = 0
155         def parse_die(msg: str) -> NoReturn:
156             die(f"{cgout_file.name}:{cgout_line_num}: {msg}")
158         def readline() -> str:
159             nonlocal cgout_line_num
160             cgout_line_num += 1
161             return cgout_file.readline()
163         # Read "desc:" lines.
164         desc: list[str] = []
165         while line := readline():
166             if m := re.match(r"desc:\s+(.*)", line):
167                 desc.append(m.group(1))
168             else:
169                 break
171         # Read "cmd:" line. (`line` is already set from the "desc:" loop.)
172         if m := re.match(r"cmd:\s+(.*)", line):
173             cmd = m.group(1)
174         else:
175             parse_die("missing a `command:` line")
177         # Read "events:" line.
178         line = readline()
179         if m := re.match(r"events:\s+(.*)", line):
180             events = Events(m.group(1))
181         else:
182             parse_die("missing an `events:` line")
184         def mk_empty_dict_line_cc() -> DictLineCc:
185             return defaultdict(events.mk_empty_cc)
187         def mk_empty_dict_fn_dict_line_cc() -> DictFnDictLineCc:
188             return defaultdict(mk_empty_dict_line_cc)
190         summary_cc_present = False
192         fl = ""
193         fn = ""
195         # The `cumul_*` values are passed in by reference and are modified by
196         # this function. But they can't be properly initialized until the
197         # `events:` line of the first file is read and the number of events is
198         # known. So we initialize them in an invalid state, and then
199         # reinitialize them properly here, before their first use.
200         if is_first_file:
201             cumul_dict_fl_dict_fn_dict_line_cc.default_factory = (
202                 mk_empty_dict_fn_dict_line_cc
203             )
204             cumul_summary_cc.extend(events.mk_empty_cc())
206         # Line matching is done in order of pattern frequency, for speed.
207         while line := readline():
208             if line[0].isdigit():
209                 split_line = line.split()
210                 try:
211                     line_num = int(split_line[0])
212                     cc = events.mk_cc(split_line[1:])
213                 except ValueError:
214                     parse_die("malformed or too many event counts")
216                 # Record this CC at the file/func/line level.
217                 add_cc_to_cc(cc, cumul_dict_fl_dict_fn_dict_line_cc[fl][fn][line_num])
219             elif line.startswith("fn="):
220                 fn = line[3:-1]
222             elif line.startswith("fl="):
223                 fl = line[3:-1]
224                 # A `fn=` line should follow, overwriting the "???".
225                 fn = "???"
227             elif m := re.match(r"summary:\s+(.*)", line):
228                 summary_cc_present = True
229                 try:
230                     add_cc_to_cc(events.mk_cc(m.group(1).split()), cumul_summary_cc)
231                 except ValueError:
232                     parse_die("malformed or too many event counts")
234             elif line == "\n" or line.startswith("#"):
235                 # Skip empty lines and comment lines.
236                 pass
238             else:
239                 parse_die(f"malformed line: {line[:-1]}")
241     # Check if summary line was present.
242     if not summary_cc_present:
243         parse_die("missing `summary:` line, aborting")
245     # In `cg_annotate.in` and `cg_diff.in` we check that the file's summary CC
246     # matches the totals of the file's individual CCs, but not here. That's
247     # because in this script we don't collect the file's CCs in isolation,
248     # instead we just add them to the accumulated CCs, for speed. This makes it
249     # difficult to do the per-file checking.
251     return (desc, cmd, events)
254 def main() -> None:
255     desc1: list[str] | None = None
256     cmd1 = None
257     events1 = None
259     # Different places where we accumulate CC data. Initialized to invalid
260     # states prior to the number of events being known.
261     cumul_dict_fl_dict_fn_dict_line_cc: DictFlDictFnDictLineCc = defaultdict(None)
262     cumul_summary_cc: Cc = []
264     for n, filename in enumerate(args.cgout_filename):
265         is_first_file = n == 0
266         (desc_n, cmd_n, events_n) = read_cgout_file(
267             filename,
268             is_first_file,
269             cumul_dict_fl_dict_fn_dict_line_cc,
270             cumul_summary_cc,
271         )
272         # We reuse the description and command from the first file, like the
273         # the old C version of `cg_merge`.
274         if is_first_file:
275             desc1 = desc_n
276             cmd1 = cmd_n
277             events1 = events_n
278         else:
279             assert events1
280             if events1.events != events_n.events:
281                 die("events in data files don't match")
283     def write_output(f: TextIO) -> None:
284         # These assertions hold because the loop above executes at least twice.
285         assert desc1
286         assert events1
287         assert cumul_dict_fl_dict_fn_dict_line_cc is not None
288         assert cumul_summary_cc
290         for desc_line in desc1:
291             print("desc:", desc_line, file=f)
292         print("cmd:", cmd1, file=f)
293         print("events:", *events1.events, sep=" ", file=f)
295         for fl, dict_fn_dict_line_cc in cumul_dict_fl_dict_fn_dict_line_cc.items():
296             print(f"fl={fl}", file=f)
297             for fn, dict_line_cc in dict_fn_dict_line_cc.items():
298                 print(f"fn={fn}", file=f)
299                 for line, cc in dict_line_cc.items():
300                     print(line, *cc, file=f)
302         print("summary:", *cumul_summary_cc, sep=" ", file=f)
304     if args.output:
305         try:
306             with open(args.output, "w", encoding="utf-8") as f:
307                 write_output(f)
308         except OSError as err:
309             die(f"{err}")
310     else:
311         write_output(sys.stdout)
314 if __name__ == "__main__":
315     main()