stats-viewer.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2008 the V8 project authors. All rights reserved.
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are
  6. # met:
  7. #
  8. # * Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. # * Redistributions in binary form must reproduce the above
  11. # copyright notice, this list of conditions and the following
  12. # disclaimer in the documentation and/or other materials provided
  13. # with the distribution.
  14. # * Neither the name of Google Inc. nor the names of its
  15. # contributors may be used to endorse or promote products derived
  16. # from this software without specific prior written permission.
  17. #
  18. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. """A cross-platform execution counter viewer.
  30. The stats viewer reads counters from a binary file and displays them
  31. in a window, re-reading and re-displaying with regular intervals.
  32. """
  33. # for py2/py3 compatibility
  34. from __future__ import print_function
  35. import mmap
  36. import optparse
  37. import os
  38. import re
  39. import struct
  40. import sys
  41. import time
  42. import Tkinter
  43. # The interval, in milliseconds, between ui updates
  44. UPDATE_INTERVAL_MS = 100
  45. # Mapping from counter prefix to the formatting to be used for the counter
  46. COUNTER_LABELS = {"t": "%i ms.", "c": "%i"}
  47. # The magic numbers used to check if a file is not a counters file
  48. COUNTERS_FILE_MAGIC_NUMBER = 0xDEADFACE
  49. CHROME_COUNTERS_FILE_MAGIC_NUMBER = 0x13131313
  50. class StatsViewer(object):
  51. """The main class that keeps the data used by the stats viewer."""
  52. def __init__(self, data_name, name_filter):
  53. """Creates a new instance.
  54. Args:
  55. data_name: the name of the file containing the counters.
  56. name_filter: The regexp filter to apply to counter names.
  57. """
  58. self.data_name = data_name
  59. self.name_filter = name_filter
  60. # The handle created by mmap.mmap to the counters file. We need
  61. # this to clean it up on exit.
  62. self.shared_mmap = None
  63. # A mapping from counter names to the ui element that displays
  64. # them
  65. self.ui_counters = {}
  66. # The counter collection used to access the counters file
  67. self.data = None
  68. # The Tkinter root window object
  69. self.root = None
  70. def Run(self):
  71. """The main entry-point to running the stats viewer."""
  72. try:
  73. self.data = self.MountSharedData()
  74. # OpenWindow blocks until the main window is closed
  75. self.OpenWindow()
  76. finally:
  77. self.CleanUp()
  78. def MountSharedData(self):
  79. """Mount the binary counters file as a memory-mapped file. If
  80. something goes wrong print an informative message and exit the
  81. program."""
  82. if not os.path.exists(self.data_name):
  83. maps_name = "/proc/%s/maps" % self.data_name
  84. if not os.path.exists(maps_name):
  85. print("\"%s\" is neither a counter file nor a PID." % self.data_name)
  86. sys.exit(1)
  87. maps_file = open(maps_name, "r")
  88. try:
  89. self.data_name = None
  90. for m in re.finditer(r"/dev/shm/\S*", maps_file.read()):
  91. if os.path.exists(m.group(0)):
  92. self.data_name = m.group(0)
  93. break
  94. if self.data_name is None:
  95. print("Can't find counter file in maps for PID %s." % self.data_name)
  96. sys.exit(1)
  97. finally:
  98. maps_file.close()
  99. data_file = open(self.data_name, "r")
  100. size = os.fstat(data_file.fileno()).st_size
  101. fileno = data_file.fileno()
  102. self.shared_mmap = mmap.mmap(fileno, size, access=mmap.ACCESS_READ)
  103. data_access = SharedDataAccess(self.shared_mmap)
  104. if data_access.IntAt(0) == COUNTERS_FILE_MAGIC_NUMBER:
  105. return CounterCollection(data_access)
  106. elif data_access.IntAt(0) == CHROME_COUNTERS_FILE_MAGIC_NUMBER:
  107. return ChromeCounterCollection(data_access)
  108. print("File %s is not stats data." % self.data_name)
  109. sys.exit(1)
  110. def CleanUp(self):
  111. """Cleans up the memory mapped file if necessary."""
  112. if self.shared_mmap:
  113. self.shared_mmap.close()
  114. def UpdateCounters(self):
  115. """Read the contents of the memory-mapped file and update the ui if
  116. necessary. If the same counters are present in the file as before
  117. we just update the existing labels. If any counters have been added
  118. or removed we scrap the existing ui and draw a new one.
  119. """
  120. changed = False
  121. counters_in_use = self.data.CountersInUse()
  122. if counters_in_use != len(self.ui_counters):
  123. self.RefreshCounters()
  124. changed = True
  125. else:
  126. for i in range(self.data.CountersInUse()):
  127. counter = self.data.Counter(i)
  128. name = counter.Name()
  129. if name in self.ui_counters:
  130. value = counter.Value()
  131. ui_counter = self.ui_counters[name]
  132. counter_changed = ui_counter.Set(value)
  133. changed = (changed or counter_changed)
  134. else:
  135. self.RefreshCounters()
  136. changed = True
  137. break
  138. if changed:
  139. # The title of the window shows the last time the file was
  140. # changed.
  141. self.UpdateTime()
  142. self.ScheduleUpdate()
  143. def UpdateTime(self):
  144. """Update the title of the window with the current time."""
  145. self.root.title("Stats Viewer [updated %s]" % time.strftime("%H:%M:%S"))
  146. def ScheduleUpdate(self):
  147. """Schedules the next ui update."""
  148. self.root.after(UPDATE_INTERVAL_MS, lambda: self.UpdateCounters())
  149. def RefreshCounters(self):
  150. """Tear down and rebuild the controls in the main window."""
  151. counters = self.ComputeCounters()
  152. self.RebuildMainWindow(counters)
  153. def ComputeCounters(self):
  154. """Group the counters by the suffix of their name.
  155. Since the same code-level counter (for instance "X") can result in
  156. several variables in the binary counters file that differ only by a
  157. two-character prefix (for instance "c:X" and "t:X") counters are
  158. grouped by suffix and then displayed with custom formatting
  159. depending on their prefix.
  160. Returns:
  161. A mapping from suffixes to a list of counters with that suffix,
  162. sorted by prefix.
  163. """
  164. names = {}
  165. for i in range(self.data.CountersInUse()):
  166. counter = self.data.Counter(i)
  167. name = counter.Name()
  168. names[name] = counter
  169. # By sorting the keys we ensure that the prefixes always come in the
  170. # same order ("c:" before "t:") which looks more consistent in the
  171. # ui.
  172. sorted_keys = names.keys()
  173. sorted_keys.sort()
  174. # Group together the names whose suffix after a ':' are the same.
  175. groups = {}
  176. for name in sorted_keys:
  177. counter = names[name]
  178. if ":" in name:
  179. name = name[name.find(":")+1:]
  180. if not name in groups:
  181. groups[name] = []
  182. groups[name].append(counter)
  183. return groups
  184. def RebuildMainWindow(self, groups):
  185. """Tear down and rebuild the main window.
  186. Args:
  187. groups: the groups of counters to display
  188. """
  189. # Remove elements in the current ui
  190. self.ui_counters.clear()
  191. for child in self.root.children.values():
  192. child.destroy()
  193. # Build new ui
  194. index = 0
  195. sorted_groups = groups.keys()
  196. sorted_groups.sort()
  197. for counter_name in sorted_groups:
  198. counter_objs = groups[counter_name]
  199. if self.name_filter.match(counter_name):
  200. name = Tkinter.Label(self.root, width=50, anchor=Tkinter.W,
  201. text=counter_name)
  202. name.grid(row=index, column=0, padx=1, pady=1)
  203. count = len(counter_objs)
  204. for i in range(count):
  205. counter = counter_objs[i]
  206. name = counter.Name()
  207. var = Tkinter.StringVar()
  208. if self.name_filter.match(name):
  209. value = Tkinter.Label(self.root, width=15, anchor=Tkinter.W,
  210. textvariable=var)
  211. value.grid(row=index, column=(1 + i), padx=1, pady=1)
  212. # If we know how to interpret the prefix of this counter then
  213. # add an appropriate formatting to the variable
  214. if (":" in name) and (name[0] in COUNTER_LABELS):
  215. format = COUNTER_LABELS[name[0]]
  216. else:
  217. format = "%i"
  218. ui_counter = UiCounter(var, format)
  219. self.ui_counters[name] = ui_counter
  220. ui_counter.Set(counter.Value())
  221. index += 1
  222. self.root.update()
  223. def OpenWindow(self):
  224. """Create and display the root window."""
  225. self.root = Tkinter.Tk()
  226. # Tkinter is no good at resizing so we disable it
  227. self.root.resizable(width=False, height=False)
  228. self.RefreshCounters()
  229. self.ScheduleUpdate()
  230. self.root.mainloop()
  231. class UiCounter(object):
  232. """A counter in the ui."""
  233. def __init__(self, var, format):
  234. """Creates a new ui counter.
  235. Args:
  236. var: the Tkinter string variable for updating the ui
  237. format: the format string used to format this counter
  238. """
  239. self.var = var
  240. self.format = format
  241. self.last_value = None
  242. def Set(self, value):
  243. """Updates the ui for this counter.
  244. Args:
  245. value: The value to display
  246. Returns:
  247. True if the value had changed, otherwise False. The first call
  248. always returns True.
  249. """
  250. if value == self.last_value:
  251. return False
  252. else:
  253. self.last_value = value
  254. self.var.set(self.format % value)
  255. return True
  256. class SharedDataAccess(object):
  257. """A utility class for reading data from the memory-mapped binary
  258. counters file."""
  259. def __init__(self, data):
  260. """Create a new instance.
  261. Args:
  262. data: A handle to the memory-mapped file, as returned by mmap.mmap.
  263. """
  264. self.data = data
  265. def ByteAt(self, index):
  266. """Return the (unsigned) byte at the specified byte index."""
  267. return ord(self.CharAt(index))
  268. def IntAt(self, index):
  269. """Return the little-endian 32-byte int at the specified byte index."""
  270. word_str = self.data[index:index+4]
  271. result, = struct.unpack("I", word_str)
  272. return result
  273. def CharAt(self, index):
  274. """Return the ascii character at the specified byte index."""
  275. return self.data[index]
  276. class Counter(object):
  277. """A pointer to a single counter within a binary counters file."""
  278. def __init__(self, data, offset):
  279. """Create a new instance.
  280. Args:
  281. data: the shared data access object containing the counter
  282. offset: the byte offset of the start of this counter
  283. """
  284. self.data = data
  285. self.offset = offset
  286. def Value(self):
  287. """Return the integer value of this counter."""
  288. return self.data.IntAt(self.offset)
  289. def Name(self):
  290. """Return the ascii name of this counter."""
  291. result = ""
  292. index = self.offset + 4
  293. current = self.data.ByteAt(index)
  294. while current:
  295. result += chr(current)
  296. index += 1
  297. current = self.data.ByteAt(index)
  298. return result
  299. class CounterCollection(object):
  300. """An overlay over a counters file that provides access to the
  301. individual counters contained in the file."""
  302. def __init__(self, data):
  303. """Create a new instance.
  304. Args:
  305. data: the shared data access object
  306. """
  307. self.data = data
  308. self.max_counters = data.IntAt(4)
  309. self.max_name_size = data.IntAt(8)
  310. def CountersInUse(self):
  311. """Return the number of counters in active use."""
  312. return self.data.IntAt(12)
  313. def Counter(self, index):
  314. """Return the index'th counter."""
  315. return Counter(self.data, 16 + index * self.CounterSize())
  316. def CounterSize(self):
  317. """Return the size of a single counter."""
  318. return 4 + self.max_name_size
  319. class ChromeCounter(object):
  320. """A pointer to a single counter within a binary counters file."""
  321. def __init__(self, data, name_offset, value_offset):
  322. """Create a new instance.
  323. Args:
  324. data: the shared data access object containing the counter
  325. name_offset: the byte offset of the start of this counter's name
  326. value_offset: the byte offset of the start of this counter's value
  327. """
  328. self.data = data
  329. self.name_offset = name_offset
  330. self.value_offset = value_offset
  331. def Value(self):
  332. """Return the integer value of this counter."""
  333. return self.data.IntAt(self.value_offset)
  334. def Name(self):
  335. """Return the ascii name of this counter."""
  336. result = ""
  337. index = self.name_offset
  338. current = self.data.ByteAt(index)
  339. while current:
  340. result += chr(current)
  341. index += 1
  342. current = self.data.ByteAt(index)
  343. return result
  344. class ChromeCounterCollection(object):
  345. """An overlay over a counters file that provides access to the
  346. individual counters contained in the file."""
  347. _HEADER_SIZE = 4 * 4
  348. _COUNTER_NAME_SIZE = 64
  349. _THREAD_NAME_SIZE = 32
  350. def __init__(self, data):
  351. """Create a new instance.
  352. Args:
  353. data: the shared data access object
  354. """
  355. self.data = data
  356. self.max_counters = data.IntAt(8)
  357. self.max_threads = data.IntAt(12)
  358. self.counter_names_offset = \
  359. self._HEADER_SIZE + self.max_threads * (self._THREAD_NAME_SIZE + 2 * 4)
  360. self.counter_values_offset = \
  361. self.counter_names_offset + self.max_counters * self._COUNTER_NAME_SIZE
  362. def CountersInUse(self):
  363. """Return the number of counters in active use."""
  364. for i in range(self.max_counters):
  365. name_offset = self.counter_names_offset + i * self._COUNTER_NAME_SIZE
  366. if self.data.ByteAt(name_offset) == 0:
  367. return i
  368. return self.max_counters
  369. def Counter(self, i):
  370. """Return the i'th counter."""
  371. name_offset = self.counter_names_offset + i * self._COUNTER_NAME_SIZE
  372. value_offset = self.counter_values_offset + i * self.max_threads * 4
  373. return ChromeCounter(self.data, name_offset, value_offset)
  374. def Main(data_file, name_filter):
  375. """Run the stats counter.
  376. Args:
  377. data_file: The counters file to monitor.
  378. name_filter: The regexp filter to apply to counter names.
  379. """
  380. StatsViewer(data_file, name_filter).Run()
  381. if __name__ == "__main__":
  382. parser = optparse.OptionParser("usage: %prog [--filter=re] "
  383. "<stats data>|<test_shell pid>")
  384. parser.add_option("--filter",
  385. default=".*",
  386. help=("regexp filter for counter names "
  387. "[default: %default]"))
  388. (options, args) = parser.parse_args()
  389. if len(args) != 1:
  390. parser.print_help()
  391. sys.exit(1)
  392. Main(args[0], re.compile(options.filter))