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