cg_diff.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. #!/usr/bin/env python3
  2. # Copyright 2020, gRPC Authors All rights reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import argparse
  16. import enum
  17. import os
  18. import subprocess
  19. import sys
  20. class State(enum.Enum):
  21. READING_HEADERS = enum.auto()
  22. READING_INSTRUCTION = enum.auto()
  23. READING_COUNTS = enum.auto()
  24. READING_SUMMARY = enum.auto()
  25. class InstructionCounts(object):
  26. def __init__(self, events):
  27. self._events = events
  28. self._counts = {}
  29. @property
  30. def events(self):
  31. return self._events
  32. @property
  33. def instructions(self):
  34. return self._counts.keys()
  35. def add(self, instruction, counts):
  36. """Add a list of counts or the given instruction."""
  37. if instruction in self._counts:
  38. existing = self._counts[instruction]
  39. self._counts[instruction] = [a + b for (a, b) in zip(existing, counts)]
  40. else:
  41. self._counts[instruction] = counts
  42. def count(self, instruction, event):
  43. """The number of occurrences of the event for the given instruction."""
  44. counts = self._counts.get(instruction)
  45. index = self._events.index(event)
  46. if counts:
  47. return counts[index]
  48. else:
  49. return 0
  50. def aggregate(self):
  51. """Aggregates event counts over all instructions."""
  52. return [sum(x) for x in zip(*self._counts.values())]
  53. def aggregate_by_event(self, event):
  54. """Aggregates event counts over all instructions for a given event."""
  55. return self.aggregate_by_index(self._events.index(event))
  56. def aggregate_by_index(self, index):
  57. """Aggregates event counts over all instructions for the event at the given index."""
  58. return sum(x[index] for x in self._counts.values())
  59. class Parser(object):
  60. HEADERS = ["desc:", "cmd:"]
  61. def __init__(self):
  62. # Parsing state.
  63. self._state = State.READING_HEADERS
  64. # File for current instruction
  65. self._file = None
  66. # Function for current instruction
  67. self._function = None
  68. # Instruction counts
  69. self._counts = None
  70. @property
  71. def counts(self):
  72. return self._counts
  73. @property
  74. def _key(self):
  75. fl = "???" if self._file is None else self._file
  76. fn = "???" if self._function is None else self._function
  77. return fl + ":" + fn
  78. ### Helpers
  79. def _is_header(self, line):
  80. return any(line.startswith(p) for p in Parser.HEADERS)
  81. def _read_events_header(self, line):
  82. if line.startswith("events:"):
  83. self._counts = InstructionCounts(line[7:].strip().split(" "))
  84. return True
  85. else:
  86. return False
  87. def _read_function(self, line):
  88. if not line.startswith("fn="):
  89. return None
  90. return line[3:].strip()
  91. def _read_file(self, line):
  92. if not line.startswith("fl="):
  93. return None
  94. return line[3:].strip()
  95. def _read_file_or_function(self, line, reset_instruction=False):
  96. function = self._read_function(line)
  97. if function is not None:
  98. self._function = function
  99. self._file = None if reset_instruction else self._file
  100. return State.READING_INSTRUCTION
  101. file = self._read_file(line)
  102. if file is not None:
  103. self._file = file
  104. self._function = None if reset_instruction else self._function
  105. return State.READING_INSTRUCTION
  106. return None
  107. ### Section parsing
  108. def _read_headers(self, line):
  109. if self._read_events_header(line) or self._is_header(line):
  110. # Still reading headers.
  111. return State.READING_HEADERS
  112. # Not a header, maybe a file or function.
  113. next_state = self._read_file_or_function(line)
  114. if next_state is None:
  115. raise RuntimeWarning("Unhandled line:", line)
  116. return next_state
  117. def _read_instruction(self, line, reset_instruction=False):
  118. next_state = self._read_file_or_function(line, reset_instruction)
  119. if next_state is not None:
  120. return next_state
  121. if self._read_summary(line):
  122. return State.READING_SUMMARY
  123. return self._read_counts(line)
  124. def _read_counts(self, line):
  125. # Drop the line number
  126. counts = [int(x) for x in line.split(" ")][1:]
  127. self._counts.add(self._key, counts)
  128. return State.READING_COUNTS
  129. def _read_summary(self, line):
  130. if line.startswith("summary:"):
  131. summary = [int(x) for x in line[8:].strip().split(" ")]
  132. computed_summary = self._counts.aggregate()
  133. assert summary == computed_summary
  134. return True
  135. else:
  136. return False
  137. ### Parse
  138. def parse(self, file, demangle):
  139. """Parse the given file."""
  140. with open(file) as fh:
  141. if demangle:
  142. demangled = subprocess.check_output(["swift", "demangle"], stdin=fh)
  143. self._parse_lines(x.decode("utf-8") for x in demangled.splitlines())
  144. else:
  145. self._parse_lines(fh)
  146. return self._counts
  147. def _parse_lines(self, lines):
  148. for line in lines:
  149. self._next_line(line)
  150. def _next_line(self, line):
  151. """Parses a line of input."""
  152. if self._state is State.READING_HEADERS:
  153. self._state = self._read_headers(line)
  154. elif self._state is State.READING_INSTRUCTION:
  155. self._state = self._read_instruction(line)
  156. elif self._state is State.READING_COUNTS:
  157. self._state = self._read_instruction(line, reset_instruction=True)
  158. elif self._state is State.READING_SUMMARY:
  159. # We're done.
  160. return
  161. else:
  162. raise RuntimeError("Unexpected state", self._state)
  163. def parse(filename, demangle):
  164. parser = Parser()
  165. return parser.parse(filename, demangle)
  166. def print_summary(args):
  167. # No need to demangle for summary.
  168. counts1 = parse(args.file1, False)
  169. aggregate1 = counts1.aggregate_by_event(args.event)
  170. counts2 = parse(args.file2, False)
  171. aggregate2 = counts2.aggregate_by_event(args.event)
  172. delta = aggregate2 - aggregate1
  173. pc = 100.0 * delta / aggregate1
  174. print("{:16,} {}".format(aggregate1, os.path.basename(args.file1)))
  175. print("{:16,} {}".format(aggregate2, os.path.basename(args.file2)))
  176. print("{:+16,} ({:+.3f}%)".format(delta, pc))
  177. def print_diff_table(args):
  178. counts1 = parse(args.file1, args.demangle)
  179. aggregate1 = counts1.aggregate_by_event(args.event)
  180. counts2 = parse(args.file2, args.demangle)
  181. aggregate2 = counts2.aggregate_by_event(args.event)
  182. file1_total = aggregate1
  183. diffs = []
  184. def _count(key, counts):
  185. block = counts.get(key)
  186. return 0 if block is None else block.counts[0]
  187. def _row(c1, c2, key):
  188. delta = c2 - c1
  189. delta_pc = 100.0 * (delta / file1_total)
  190. return (c1, c2, delta, delta_pc, key)
  191. def _row_for_key(key):
  192. c1 = counts1.count(key, args.event)
  193. c2 = counts2.count(key, args.event)
  194. return _row(c1, c2, key)
  195. if args.only_common:
  196. keys = counts1.instructions & counts2.instructions
  197. else:
  198. keys = counts1.instructions | counts2.instructions
  199. rows = [_row_for_key(k) for k in keys]
  200. rows.append(_row(aggregate1, aggregate2, "PROGRAM TOTALS"))
  201. print(
  202. " | ".join(
  203. [
  204. "file1".rjust(14),
  205. "file2".rjust(14),
  206. "delta".rjust(14),
  207. "%".rjust(7),
  208. "name",
  209. ]
  210. )
  211. )
  212. index = _sort_index(args.sort)
  213. reverse = not args.ascending
  214. sorted_rows = sorted(rows, key=lambda x: x[index], reverse=reverse)
  215. for (c1, c2, delta, delta_pc, key) in sorted_rows:
  216. if abs(delta_pc) >= args.low_watermark:
  217. print(
  218. " | ".join(
  219. [
  220. "{:14,}".format(c1),
  221. "{:14,}".format(c2),
  222. "{:+14,}".format(delta),
  223. "{:+7.3f}".format(delta_pc),
  224. key,
  225. ]
  226. )
  227. )
  228. def _sort_index(key):
  229. return ("file1", "file2", "delta").index(key)
  230. if __name__ == "__main__":
  231. parser = argparse.ArgumentParser("cg_diff.py")
  232. parser.add_argument(
  233. "--sort",
  234. choices=("file1", "file2", "delta"),
  235. default="file1",
  236. help="The column to sort on.",
  237. )
  238. parser.add_argument(
  239. "--ascending", action="store_true", help="Sorts in ascending order."
  240. )
  241. parser.add_argument(
  242. "--only-common",
  243. action="store_true",
  244. help="Only print instructions present in both files.",
  245. )
  246. parser.add_argument(
  247. "--no-demangle",
  248. action="store_false",
  249. dest="demangle",
  250. help="Disables demangling of input files.",
  251. )
  252. parser.add_argument("--event", default="Ir", help="The event to compare.")
  253. parser.add_argument(
  254. "--low-watermark",
  255. type=float,
  256. default=0.01,
  257. help="A low watermark, percentage changes in counts "
  258. "relative to the total instruction count of "
  259. "file1 below this value will not be printed.",
  260. )
  261. parser.add_argument(
  262. "--summary", action="store_true", help="Prints a summary of the diff."
  263. )
  264. parser.add_argument("file1")
  265. parser.add_argument("file2")
  266. args = parser.parse_args()
  267. if args.summary:
  268. print_summary(args)
  269. else:
  270. print_diff_table(args)