/Misc/tsc_stats.py

http://unladen-swallow.googlecode.com/ · Python · 282 lines · 171 code · 39 blank · 72 comment · 27 complexity · da3f43cd611153a89a37a7d0dcc37695 MD5 · raw file

  1. #!/usr/bin/env python
  2. """Compute timing statistics based on the output of Python with TSC enabled.
  3. To use this script, pass --with-tsc to ./configure and call sys.settscdump(True)
  4. in the script that you want to use to record timings. When the script and
  5. interpreter exit, the timings will be printed to stderr as a CSV file separated
  6. by tabs. You should redirect that to this script, either through a file or
  7. pipe:
  8. ./python myscript.py 2>&1 >&3 3>&- | Misc/tsc_stats.py
  9. ./python myscript.py 2> stats ; Misc/tsc_stats.py stats
  10. This script outputs statistics about function call overhead, exception handling
  11. overhead, bytecode to LLVM IR compilation overhead, native code generation
  12. overhead, and various other things.
  13. In order to not use too much memory, the event timer in Python periodically
  14. flushes the event buffer. This messes up a lot of the timings, so it also
  15. prints out events that let us figure out how long the flush took. We take that
  16. time and go back and adjust the times to erase that overhead. Otherwise our
  17. max, mean, and stddev statistics would be meaningless.
  18. In order to get more meaningful results for function call overhead, any time
  19. spent doing compilation in the eval loop is not counted against the function
  20. call overhead. Otherwise the mean and max function call overhead times would be
  21. way off.
  22. """
  23. from __future__ import division
  24. import itertools
  25. import math
  26. import sys
  27. def median(xs):
  28. """Return the median of some numeric values.
  29. Assumes that the input list is sorted.
  30. """
  31. mid = len(xs) // 2
  32. if len(xs) % 2 == 0:
  33. return (xs[mid] + xs[mid - 1]) / 2
  34. else:
  35. return xs[mid]
  36. def decile_decorator(func):
  37. def decile_func(xs):
  38. """Return the func applied to the inter-decile 80% of the input.
  39. Assumes that the input list is sorted.
  40. """
  41. decile = len(xs) // 10
  42. if decile == 0:
  43. # Special case this because -0 == 0, so the list slice below doesn't
  44. # work.
  45. return func(xs)
  46. return func(xs[decile:-decile])
  47. return decile_func
  48. def mean(xs):
  49. """Return the mean of some numeric values."""
  50. return float(sum(xs)) / len(xs)
  51. decile_mean = decile_decorator(mean)
  52. def stddev(xs):
  53. """Return the standard deviation of some numeric values."""
  54. if len(xs) == 1:
  55. return 0 # Avoid doing a ZeroDivisionError.
  56. mn = mean(xs)
  57. deviations = (x - mn for x in xs)
  58. square_sum = sum(d * d for d in deviations)
  59. variance = square_sum / float(len(xs) - 1)
  60. return math.sqrt(variance)
  61. decile_stddev = decile_decorator(stddev)
  62. class DeltaStatistic(object):
  63. """This class matches and stores delta timings for a class of events."""
  64. def __init__(self, start_prefix, end_prefix, missed_events):
  65. """Constructor.
  66. Args:
  67. start_prefix: the prefix matching the start event
  68. end_prefix: the prefix matching the end event
  69. missed_events: the list that we should missed events to
  70. """
  71. self.start_prefix = start_prefix
  72. self.end_prefix = end_prefix
  73. self.missed_events = missed_events
  74. self.delta_dict = {}
  75. self.aggregate_deltas = []
  76. self.started = False
  77. self.start_event = None
  78. self.start_time = 0
  79. def try_match(self, thread, event, time):
  80. """If this event matches the statistic, record it and return True.
  81. Args:
  82. thread: the thread id for this event
  83. event: the name of the event
  84. time: the timestamp counter when the event occurred
  85. """
  86. # TODO(rnk): Keep things thread local.
  87. if event.startswith(self.start_prefix):
  88. # If we already started, we missed an end event. Record the old
  89. # start event that didn't get an end, and use this start event
  90. # instead.
  91. if self.started:
  92. self.missed_events.append((self.start_event, self.start_time))
  93. self.started = True
  94. self.start_event = event
  95. self.start_time = time
  96. return True
  97. elif event.startswith(self.end_prefix):
  98. # If we have not started, we missed a start event. Record this
  99. # end event, and ignore it.
  100. if not self.started:
  101. self.missed_events.append((event, time))
  102. return True
  103. delta = time - self.start_time
  104. key = (self.start_event, event)
  105. self.delta_dict.setdefault(key, []).append(delta)
  106. self.aggregate_deltas.append(delta)
  107. # Reset us looking for a start event.
  108. self.started = False
  109. self.start_event = None
  110. self.start_time = 0
  111. return True
  112. return False
  113. class TimeAnalyzer(object):
  114. def __init__(self, input):
  115. self.input = input
  116. self.missed_events = []
  117. m_e = self.missed_events # Shorthand
  118. self.call_stats = DeltaStatistic("CALL_START_", "CALL_ENTER_", m_e)
  119. self.exception_stats = DeltaStatistic("EXCEPT_RAISE_", "EXCEPT_CATCH_",
  120. m_e)
  121. self.global_stats = DeltaStatistic("LOAD_GLOBAL_ENTER_",
  122. "LOAD_GLOBAL_EXIT_", m_e)
  123. self.native_stats = DeltaStatistic("JIT_START", "JIT_END", m_e)
  124. self.llvm_stats = DeltaStatistic("LLVM_COMPILE_START",
  125. "LLVM_COMPILE_END", m_e)
  126. self.eval_compile_stats = DeltaStatistic("EVAL_COMPILE_START",
  127. "EVAL_COMPILE_END", m_e)
  128. self.flush_stats = DeltaStatistic("FLUSH_START", "FLUSH_END", m_e)
  129. self.statistics = [
  130. self.call_stats,
  131. self.exception_stats,
  132. self.global_stats,
  133. self.native_stats,
  134. self.llvm_stats,
  135. self.eval_compile_stats,
  136. self.flush_stats,
  137. ]
  138. def flush_fudge(self, flush_delta):
  139. """Fudge the start time of open stats to eliminate flush overhead."""
  140. for stat in self.statistics:
  141. if stat.started:
  142. stat.start_time += flush_delta
  143. def analyze(self):
  144. """Process the input into categorized timings."""
  145. for line in self.input:
  146. (thread, event, time) = line.strip().split("\t")
  147. time = int(time)
  148. for stat in self.statistics:
  149. if stat.try_match(thread, event, time):
  150. if not stat.started and stat.aggregate_deltas:
  151. delta = stat.aggregate_deltas[-1]
  152. if (stat is self.eval_compile_stats and
  153. self.call_stats.started):
  154. # Fudge the call_stats start time to erase
  155. # compilation overhead in the eval loop.
  156. self.call_stats.start_time += delta
  157. if stat is self.flush_stats:
  158. # Fudge every stat that has an open timing to
  159. # eliminate the flush overhead.
  160. self.flush_fudge(delta)
  161. break
  162. else:
  163. # If no statistic matched the event, log it as missed.
  164. self.missed_events.append((event, time))
  165. def print_deltas(self, deltas):
  166. """Print out statistics about this sequence of timings."""
  167. print "occurrences:", len(deltas)
  168. if deltas:
  169. deltas = sorted(deltas)
  170. print "median:", median(deltas)
  171. print "inter-decile mean:", decile_mean(deltas)
  172. print "min delta:", deltas[0]
  173. print "max delta:", deltas[-1]
  174. print "inter-decile stddev:", decile_stddev(deltas)
  175. def print_stat_deltas(self, stat):
  176. """Print out the deltas for this statistic broken down by pairing."""
  177. for ((start, end), deltas) in stat.delta_dict.iteritems():
  178. print "for transitions from %s to %s:" % (start, end)
  179. self.print_deltas(deltas)
  180. print
  181. def print_stat_aggregate(self, stat):
  182. """Print out the aggregate deltas for this statistic."""
  183. print ("In aggregate, for transitions from %s* to %s*:" %
  184. (stat.start_prefix, stat.end_prefix))
  185. self.print_deltas(stat.aggregate_deltas)
  186. def print_analysis(self):
  187. print ("All times are in time stamp counter units, which are related "
  188. "to your CPU frequency.")
  189. print
  190. print "Call overhead:"
  191. print "----------------------------------------"
  192. self.print_stat_deltas(self.call_stats)
  193. self.print_stat_aggregate(self.call_stats)
  194. print
  195. print "Exception handling overhead:"
  196. print "----------------------------------------"
  197. self.print_stat_deltas(self.exception_stats)
  198. self.print_stat_aggregate(self.exception_stats)
  199. print
  200. print "LOAD_GLOBAL overhead:"
  201. print "----------------------------------------"
  202. self.print_stat_deltas(self.global_stats)
  203. self.print_stat_aggregate(self.global_stats)
  204. print
  205. print "LLVM IR compilation:"
  206. print "----------------------------------------"
  207. self.print_stat_aggregate(self.llvm_stats)
  208. print
  209. print "Native code generation:"
  210. print "----------------------------------------"
  211. self.print_stat_aggregate(self.native_stats)
  212. print
  213. print "Total time doing compilation in the eval loop:"
  214. print "----------------------------------------"
  215. self.print_stat_aggregate(self.eval_compile_stats)
  216. print
  217. grouped = {}
  218. for (event, time) in self.missed_events:
  219. grouped[event] = grouped.get(event, 0) + 1
  220. print "missed events:",
  221. print ", ".join("%s %d" % (event, count)
  222. for (event, count) in grouped.iteritems())
  223. def main(argv):
  224. if argv:
  225. assert len(argv) == 2, "tsc_stats.py expects one file as input."
  226. input = open(argv[1])
  227. else:
  228. input = sys.stdin
  229. analyzer = TimeAnalyzer(input)
  230. analyzer.analyze()
  231. analyzer.print_analysis()
  232. if __name__ == "__main__":
  233. main(sys.argv)