Home | History | Annotate | Download | only in tools
      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 
     33 The stats viewer reads counters from a binary file and displays them
     34 in a window, re-reading and re-displaying with regular intervals.
     35 """
     36 
     37 import mmap
     38 import optparse
     39 import os
     40 import re
     41 import struct
     42 import sys
     43 import time
     44 import Tkinter
     45 
     46 
     47 # The interval, in milliseconds, between ui updates
     48 UPDATE_INTERVAL_MS = 100
     49 
     50 
     51 # Mapping from counter prefix to the formatting to be used for the counter
     52 COUNTER_LABELS = {"t": "%i ms.", "c": "%i"}
     53 
     54 
     55 # The magic numbers used to check if a file is not a counters file
     56 COUNTERS_FILE_MAGIC_NUMBER = 0xDEADFACE
     57 CHROME_COUNTERS_FILE_MAGIC_NUMBER = 0x13131313
     58 
     59 
     60 class 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 
    268 class 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 
    300 class 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 
    327 class 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 
    356 class 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 
    383 class 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 
    414 class 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 
    451 def 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 
    461 if __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