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
30
31"""A cross-platform execution counter viewer.
32
33The stats viewer reads counters from a binary file and displays them
34in a window, re-reading and re-displaying with regular intervals.
35"""
36
37import mmap
38import optparse
39import os
40import re
41import struct
42import sys
43import time
44import Tkinter
45
46
47# The interval, in milliseconds, between ui updates
48UPDATE_INTERVAL_MS = 100
49
50
51# Mapping from counter prefix to the formatting to be used for the counter
52COUNTER_LABELS = {"t": "%i ms.", "c": "%i"}
53
54
55# The magic numbers used to check if a file is not a counters file
56COUNTERS_FILE_MAGIC_NUMBER = 0xDEADFACE
57CHROME_COUNTERS_FILE_MAGIC_NUMBER = 0x13131313
58
59
60class StatsViewer(object):
61  """The main class that keeps the data used by the stats viewer."""
62
63  def __init__(self, data_name, name_filter):
64    """Creates a new instance.
65
66    Args:
67      data_name: the name of the file containing the counters.
68      name_filter: The regexp filter to apply to counter names.
69    """
70    self.data_name = data_name
71    self.name_filter = name_filter
72
73    # The handle created by mmap.mmap to the counters file.  We need
74    # this to clean it up on exit.
75    self.shared_mmap = None
76
77    # A mapping from counter names to the ui element that displays
78    # them
79    self.ui_counters = {}
80
81    # The counter collection used to access the counters file
82    self.data = None
83
84    # The Tkinter root window object
85    self.root = None
86
87  def Run(self):
88    """The main entry-point to running the stats viewer."""
89    try:
90      self.data = self.MountSharedData()
91      # OpenWindow blocks until the main window is closed
92      self.OpenWindow()
93    finally:
94      self.CleanUp()
95
96  def MountSharedData(self):
97    """Mount the binary counters file as a memory-mapped file.  If
98    something goes wrong print an informative message and exit the
99    program."""
100    if not os.path.exists(self.data_name):
101      maps_name = "/proc/%s/maps" % self.data_name
102      if not os.path.exists(maps_name):
103        print "\"%s\" is neither a counter file nor a PID." % self.data_name
104        sys.exit(1)
105      maps_file = open(maps_name, "r")
106      try:
107        self.data_name = None
108        for m in re.finditer(r"/dev/shm/\S*", maps_file.read()):
109          if os.path.exists(m.group(0)):
110            self.data_name = m.group(0)
111            break
112        if self.data_name is None:
113          print "Can't find counter file in maps for PID %s." % self.data_name
114          sys.exit(1)
115      finally:
116        maps_file.close()
117    data_file = open(self.data_name, "r")
118    size = os.fstat(data_file.fileno()).st_size
119    fileno = data_file.fileno()
120    self.shared_mmap = mmap.mmap(fileno, size, access=mmap.ACCESS_READ)
121    data_access = SharedDataAccess(self.shared_mmap)
122    if data_access.IntAt(0) == COUNTERS_FILE_MAGIC_NUMBER:
123      return CounterCollection(data_access)
124    elif data_access.IntAt(0) == CHROME_COUNTERS_FILE_MAGIC_NUMBER:
125      return ChromeCounterCollection(data_access)
126    print "File %s is not stats data." % self.data_name
127    sys.exit(1)
128
129  def CleanUp(self):
130    """Cleans up the memory mapped file if necessary."""
131    if self.shared_mmap:
132      self.shared_mmap.close()
133
134  def UpdateCounters(self):
135    """Read the contents of the memory-mapped file and update the ui if
136    necessary.  If the same counters are present in the file as before
137    we just update the existing labels.  If any counters have been added
138    or removed we scrap the existing ui and draw a new one.
139    """
140    changed = False
141    counters_in_use = self.data.CountersInUse()
142    if counters_in_use != len(self.ui_counters):
143      self.RefreshCounters()
144      changed = True
145    else:
146      for i in xrange(self.data.CountersInUse()):
147        counter = self.data.Counter(i)
148        name = counter.Name()
149        if name in self.ui_counters:
150          value = counter.Value()
151          ui_counter = self.ui_counters[name]
152          counter_changed = ui_counter.Set(value)
153          changed = (changed or counter_changed)
154        else:
155          self.RefreshCounters()
156          changed = True
157          break
158    if changed:
159      # The title of the window shows the last time the file was
160      # changed.
161      self.UpdateTime()
162    self.ScheduleUpdate()
163
164  def UpdateTime(self):
165    """Update the title of the window with the current time."""
166    self.root.title("Stats Viewer [updated %s]" % time.strftime("%H:%M:%S"))
167
168  def ScheduleUpdate(self):
169    """Schedules the next ui update."""
170    self.root.after(UPDATE_INTERVAL_MS, lambda: self.UpdateCounters())
171
172  def RefreshCounters(self):
173    """Tear down and rebuild the controls in the main window."""
174    counters = self.ComputeCounters()
175    self.RebuildMainWindow(counters)
176
177  def ComputeCounters(self):
178    """Group the counters by the suffix of their name.
179
180    Since the same code-level counter (for instance "X") can result in
181    several variables in the binary counters file that differ only by a
182    two-character prefix (for instance "c:X" and "t:X") counters are
183    grouped by suffix and then displayed with custom formatting
184    depending on their prefix.
185
186    Returns:
187      A mapping from suffixes to a list of counters with that suffix,
188      sorted by prefix.
189    """
190    names = {}
191    for i in xrange(self.data.CountersInUse()):
192      counter = self.data.Counter(i)
193      name = counter.Name()
194      names[name] = counter
195
196    # By sorting the keys we ensure that the prefixes always come in the
197    # same order ("c:" before "t:") which looks more consistent in the
198    # ui.
199    sorted_keys = names.keys()
200    sorted_keys.sort()
201
202    # Group together the names whose suffix after a ':' are the same.
203    groups = {}
204    for name in sorted_keys:
205      counter = names[name]
206      if ":" in name:
207        name = name[name.find(":")+1:]
208      if not name in groups:
209        groups[name] = []
210      groups[name].append(counter)
211
212    return groups
213
214  def RebuildMainWindow(self, groups):
215    """Tear down and rebuild the main window.
216
217    Args:
218      groups: the groups of counters to display
219    """
220    # Remove elements in the current ui
221    self.ui_counters.clear()
222    for child in self.root.children.values():
223      child.destroy()
224
225    # Build new ui
226    index = 0
227    sorted_groups = groups.keys()
228    sorted_groups.sort()
229    for counter_name in sorted_groups:
230      counter_objs = groups[counter_name]
231      if self.name_filter.match(counter_name):
232        name = Tkinter.Label(self.root, width=50, anchor=Tkinter.W,
233                             text=counter_name)
234        name.grid(row=index, column=0, padx=1, pady=1)
235      count = len(counter_objs)
236      for i in xrange(count):
237        counter = counter_objs[i]
238        name = counter.Name()
239        var = Tkinter.StringVar()
240        if self.name_filter.match(name):
241          value = Tkinter.Label(self.root, width=15, anchor=Tkinter.W,
242                                textvariable=var)
243          value.grid(row=index, column=(1 + i), padx=1, pady=1)
244
245        # If we know how to interpret the prefix of this counter then
246        # add an appropriate formatting to the variable
247        if (":" in name) and (name[0] in COUNTER_LABELS):
248          format = COUNTER_LABELS[name[0]]
249        else:
250          format = "%i"
251        ui_counter = UiCounter(var, format)
252        self.ui_counters[name] = ui_counter
253        ui_counter.Set(counter.Value())
254      index += 1
255    self.root.update()
256
257  def OpenWindow(self):
258    """Create and display the root window."""
259    self.root = Tkinter.Tk()
260
261    # Tkinter is no good at resizing so we disable it
262    self.root.resizable(width=False, height=False)
263    self.RefreshCounters()
264    self.ScheduleUpdate()
265    self.root.mainloop()
266
267
268class UiCounter(object):
269  """A counter in the ui."""
270
271  def __init__(self, var, format):
272    """Creates a new ui counter.
273
274    Args:
275      var: the Tkinter string variable for updating the ui
276      format: the format string used to format this counter
277    """
278    self.var = var
279    self.format = format
280    self.last_value = None
281
282  def Set(self, value):
283    """Updates the ui for this counter.
284
285    Args:
286      value: The value to display
287
288    Returns:
289      True if the value had changed, otherwise False.  The first call
290      always returns True.
291    """
292    if value == self.last_value:
293      return False
294    else:
295      self.last_value = value
296      self.var.set(self.format % value)
297      return True
298
299
300class SharedDataAccess(object):
301  """A utility class for reading data from the memory-mapped binary
302  counters file."""
303
304  def __init__(self, data):
305    """Create a new instance.
306
307    Args:
308      data: A handle to the memory-mapped file, as returned by mmap.mmap.
309    """
310    self.data = data
311
312  def ByteAt(self, index):
313    """Return the (unsigned) byte at the specified byte index."""
314    return ord(self.CharAt(index))
315
316  def IntAt(self, index):
317    """Return the little-endian 32-byte int at the specified byte index."""
318    word_str = self.data[index:index+4]
319    result, = struct.unpack("I", word_str)
320    return result
321
322  def CharAt(self, index):
323    """Return the ascii character at the specified byte index."""
324    return self.data[index]
325
326
327class Counter(object):
328  """A pointer to a single counter withing a binary counters file."""
329
330  def __init__(self, data, offset):
331    """Create a new instance.
332
333    Args:
334      data: the shared data access object containing the counter
335      offset: the byte offset of the start of this counter
336    """
337    self.data = data
338    self.offset = offset
339
340  def Value(self):
341    """Return the integer value of this counter."""
342    return self.data.IntAt(self.offset)
343
344  def Name(self):
345    """Return the ascii name of this counter."""
346    result = ""
347    index = self.offset + 4
348    current = self.data.ByteAt(index)
349    while current:
350      result += chr(current)
351      index += 1
352      current = self.data.ByteAt(index)
353    return result
354
355
356class CounterCollection(object):
357  """An overlay over a counters file that provides access to the
358  individual counters contained in the file."""
359
360  def __init__(self, data):
361    """Create a new instance.
362
363    Args:
364      data: the shared data access object
365    """
366    self.data = data
367    self.max_counters = data.IntAt(4)
368    self.max_name_size = data.IntAt(8)
369
370  def CountersInUse(self):
371    """Return the number of counters in active use."""
372    return self.data.IntAt(12)
373
374  def Counter(self, index):
375    """Return the index'th counter."""
376    return Counter(self.data, 16 + index * self.CounterSize())
377
378  def CounterSize(self):
379    """Return the size of a single counter."""
380    return 4 + self.max_name_size
381
382
383class ChromeCounter(object):
384  """A pointer to a single counter withing a binary counters file."""
385
386  def __init__(self, data, name_offset, value_offset):
387    """Create a new instance.
388
389    Args:
390      data: the shared data access object containing the counter
391      name_offset: the byte offset of the start of this counter's name
392      value_offset: the byte offset of the start of this counter's value
393    """
394    self.data = data
395    self.name_offset = name_offset
396    self.value_offset = value_offset
397
398  def Value(self):
399    """Return the integer value of this counter."""
400    return self.data.IntAt(self.value_offset)
401
402  def Name(self):
403    """Return the ascii name of this counter."""
404    result = ""
405    index = self.name_offset
406    current = self.data.ByteAt(index)
407    while current:
408      result += chr(current)
409      index += 1
410      current = self.data.ByteAt(index)
411    return result
412
413
414class ChromeCounterCollection(object):
415  """An overlay over a counters file that provides access to the
416  individual counters contained in the file."""
417
418  _HEADER_SIZE = 4 * 4
419  _COUNTER_NAME_SIZE = 64
420  _THREAD_NAME_SIZE = 32
421
422  def __init__(self, data):
423    """Create a new instance.
424
425    Args:
426      data: the shared data access object
427    """
428    self.data = data
429    self.max_counters = data.IntAt(8)
430    self.max_threads = data.IntAt(12)
431    self.counter_names_offset = \
432        self._HEADER_SIZE + self.max_threads * (self._THREAD_NAME_SIZE + 2 * 4)
433    self.counter_values_offset = \
434        self.counter_names_offset + self.max_counters * self._COUNTER_NAME_SIZE
435
436  def CountersInUse(self):
437    """Return the number of counters in active use."""
438    for i in xrange(self.max_counters):
439      name_offset = self.counter_names_offset + i * self._COUNTER_NAME_SIZE
440      if self.data.ByteAt(name_offset) == 0:
441        return i
442    return self.max_counters
443
444  def Counter(self, i):
445    """Return the i'th counter."""
446    name_offset = self.counter_names_offset + i * self._COUNTER_NAME_SIZE
447    value_offset = self.counter_values_offset + i * self.max_threads * 4
448    return ChromeCounter(self.data, name_offset, value_offset)
449
450
451def Main(data_file, name_filter):
452  """Run the stats counter.
453
454  Args:
455    data_file: The counters file to monitor.
456    name_filter: The regexp filter to apply to counter names.
457  """
458  StatsViewer(data_file, name_filter).Run()
459
460
461if __name__ == "__main__":
462  parser = optparse.OptionParser("usage: %prog [--filter=re] "
463                                 "<stats data>|<test_shell pid>")
464  parser.add_option("--filter",
465                    default=".*",
466                    help=("regexp filter for counter names "
467                          "[default: %default]"))
468  (options, args) = parser.parse_args()
469  if len(args) != 1:
470    parser.print_help()
471    sys.exit(1)
472  Main(args[0], re.compile(options.filter))
473