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