| #!/usr/bin/env python | |
| # | |
| # Copyright 2008 the V8 project authors. All rights reserved. | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions are | |
| # met: | |
| # | |
| # * Redistributions of source code must retain the above copyright | |
| # notice, this list of conditions and the following disclaimer. | |
| # * Redistributions in binary form must reproduce the above | |
| # copyright notice, this list of conditions and the following | |
| # disclaimer in the documentation and/or other materials provided | |
| # with the distribution. | |
| # * Neither the name of Google Inc. nor the names of its | |
| # contributors may be used to endorse or promote products derived | |
| # from this software without specific prior written permission. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| """A cross-platform execution counter viewer. | |
| The stats viewer reads counters from a binary file and displays them | |
| in a window, re-reading and re-displaying with regular intervals. | |
| """ | |
| import mmap | |
| import optparse | |
| import os | |
| import re | |
| import struct | |
| import sys | |
| import time | |
| import Tkinter | |
| # The interval, in milliseconds, between ui updates | |
| UPDATE_INTERVAL_MS = 100 | |
| # Mapping from counter prefix to the formatting to be used for the counter | |
| COUNTER_LABELS = {"t": "%i ms.", "c": "%i"} | |
| # The magic numbers used to check if a file is not a counters file | |
| COUNTERS_FILE_MAGIC_NUMBER = 0xDEADFACE | |
| CHROME_COUNTERS_FILE_MAGIC_NUMBER = 0x13131313 | |
| class StatsViewer(object): | |
| """The main class that keeps the data used by the stats viewer.""" | |
| def __init__(self, data_name, name_filter): | |
| """Creates a new instance. | |
| Args: | |
| data_name: the name of the file containing the counters. | |
| name_filter: The regexp filter to apply to counter names. | |
| """ | |
| self.data_name = data_name | |
| self.name_filter = name_filter | |
| # The handle created by mmap.mmap to the counters file. We need | |
| # this to clean it up on exit. | |
| self.shared_mmap = None | |
| # A mapping from counter names to the ui element that displays | |
| # them | |
| self.ui_counters = {} | |
| # The counter collection used to access the counters file | |
| self.data = None | |
| # The Tkinter root window object | |
| self.root = None | |
| def Run(self): | |
| """The main entry-point to running the stats viewer.""" | |
| try: | |
| self.data = self.MountSharedData() | |
| # OpenWindow blocks until the main window is closed | |
| self.OpenWindow() | |
| finally: | |
| self.CleanUp() | |
| def MountSharedData(self): | |
| """Mount the binary counters file as a memory-mapped file. If | |
| something goes wrong print an informative message and exit the | |
| program.""" | |
| if not os.path.exists(self.data_name): | |
| maps_name = "/proc/%s/maps" % self.data_name | |
| if not os.path.exists(maps_name): | |
| print "\"%s\" is neither a counter file nor a PID." % self.data_name | |
| sys.exit(1) | |
| maps_file = open(maps_name, "r") | |
| try: | |
| self.data_name = None | |
| for m in re.finditer(r"/dev/shm/\S*", maps_file.read()): | |
| if os.path.exists(m.group(0)): | |
| self.data_name = m.group(0) | |
| break | |
| if self.data_name is None: | |
| print "Can't find counter file in maps for PID %s." % self.data_name | |
| sys.exit(1) | |
| finally: | |
| maps_file.close() | |
| data_file = open(self.data_name, "r") | |
| size = os.fstat(data_file.fileno()).st_size | |
| fileno = data_file.fileno() | |
| self.shared_mmap = mmap.mmap(fileno, size, access=mmap.ACCESS_READ) | |
| data_access = SharedDataAccess(self.shared_mmap) | |
| if data_access.IntAt(0) == COUNTERS_FILE_MAGIC_NUMBER: | |
| return CounterCollection(data_access) | |
| elif data_access.IntAt(0) == CHROME_COUNTERS_FILE_MAGIC_NUMBER: | |
| return ChromeCounterCollection(data_access) | |
| print "File %s is not stats data." % self.data_name | |
| sys.exit(1) | |
| def CleanUp(self): | |
| """Cleans up the memory mapped file if necessary.""" | |
| if self.shared_mmap: | |
| self.shared_mmap.close() | |
| def UpdateCounters(self): | |
| """Read the contents of the memory-mapped file and update the ui if | |
| necessary. If the same counters are present in the file as before | |
| we just update the existing labels. If any counters have been added | |
| or removed we scrap the existing ui and draw a new one. | |
| """ | |
| changed = False | |
| counters_in_use = self.data.CountersInUse() | |
| if counters_in_use != len(self.ui_counters): | |
| self.RefreshCounters() | |
| changed = True | |
| else: | |
| for i in xrange(self.data.CountersInUse()): | |
| counter = self.data.Counter(i) | |
| name = counter.Name() | |
| if name in self.ui_counters: | |
| value = counter.Value() | |
| ui_counter = self.ui_counters[name] | |
| counter_changed = ui_counter.Set(value) | |
| changed = (changed or counter_changed) | |
| else: | |
| self.RefreshCounters() | |
| changed = True | |
| break | |
| if changed: | |
| # The title of the window shows the last time the file was | |
| # changed. | |
| self.UpdateTime() | |
| self.ScheduleUpdate() | |
| def UpdateTime(self): | |
| """Update the title of the window with the current time.""" | |
| self.root.title("Stats Viewer [updated %s]" % time.strftime("%H:%M:%S")) | |
| def ScheduleUpdate(self): | |
| """Schedules the next ui update.""" | |
| self.root.after(UPDATE_INTERVAL_MS, lambda: self.UpdateCounters()) | |
| def RefreshCounters(self): | |
| """Tear down and rebuild the controls in the main window.""" | |
| counters = self.ComputeCounters() | |
| self.RebuildMainWindow(counters) | |
| def ComputeCounters(self): | |
| """Group the counters by the suffix of their name. | |
| Since the same code-level counter (for instance "X") can result in | |
| several variables in the binary counters file that differ only by a | |
| two-character prefix (for instance "c:X" and "t:X") counters are | |
| grouped by suffix and then displayed with custom formatting | |
| depending on their prefix. | |
| Returns: | |
| A mapping from suffixes to a list of counters with that suffix, | |
| sorted by prefix. | |
| """ | |
| names = {} | |
| for i in xrange(self.data.CountersInUse()): | |
| counter = self.data.Counter(i) | |
| name = counter.Name() | |
| names[name] = counter | |
| # By sorting the keys we ensure that the prefixes always come in the | |
| # same order ("c:" before "t:") which looks more consistent in the | |
| # ui. | |
| sorted_keys = names.keys() | |
| sorted_keys.sort() | |
| # Group together the names whose suffix after a ':' are the same. | |
| groups = {} | |
| for name in sorted_keys: | |
| counter = names[name] | |
| if ":" in name: | |
| name = name[name.find(":")+1:] | |
| if not name in groups: | |
| groups[name] = [] | |
| groups[name].append(counter) | |
| return groups | |
| def RebuildMainWindow(self, groups): | |
| """Tear down and rebuild the main window. | |
| Args: | |
| groups: the groups of counters to display | |
| """ | |
| # Remove elements in the current ui | |
| self.ui_counters.clear() | |
| for child in self.root.children.values(): | |
| child.destroy() | |
| # Build new ui | |
| index = 0 | |
| sorted_groups = groups.keys() | |
| sorted_groups.sort() | |
| for counter_name in sorted_groups: | |
| counter_objs = groups[counter_name] | |
| if self.name_filter.match(counter_name): | |
| name = Tkinter.Label(self.root, width=50, anchor=Tkinter.W, | |
| text=counter_name) | |
| name.grid(row=index, column=0, padx=1, pady=1) | |
| count = len(counter_objs) | |
| for i in xrange(count): | |
| counter = counter_objs[i] | |
| name = counter.Name() | |
| var = Tkinter.StringVar() | |
| if self.name_filter.match(name): | |
| value = Tkinter.Label(self.root, width=15, anchor=Tkinter.W, | |
| textvariable=var) | |
| value.grid(row=index, column=(1 + i), padx=1, pady=1) | |
| # If we know how to interpret the prefix of this counter then | |
| # add an appropriate formatting to the variable | |
| if (":" in name) and (name[0] in COUNTER_LABELS): | |
| format = COUNTER_LABELS[name[0]] | |
| else: | |
| format = "%i" | |
| ui_counter = UiCounter(var, format) | |
| self.ui_counters[name] = ui_counter | |
| ui_counter.Set(counter.Value()) | |
| index += 1 | |
| self.root.update() | |
| def OpenWindow(self): | |
| """Create and display the root window.""" | |
| self.root = Tkinter.Tk() | |
| # Tkinter is no good at resizing so we disable it | |
| self.root.resizable(width=False, height=False) | |
| self.RefreshCounters() | |
| self.ScheduleUpdate() | |
| self.root.mainloop() | |
| class UiCounter(object): | |
| """A counter in the ui.""" | |
| def __init__(self, var, format): | |
| """Creates a new ui counter. | |
| Args: | |
| var: the Tkinter string variable for updating the ui | |
| format: the format string used to format this counter | |
| """ | |
| self.var = var | |
| self.format = format | |
| self.last_value = None | |
| def Set(self, value): | |
| """Updates the ui for this counter. | |
| Args: | |
| value: The value to display | |
| Returns: | |
| True if the value had changed, otherwise False. The first call | |
| always returns True. | |
| """ | |
| if value == self.last_value: | |
| return False | |
| else: | |
| self.last_value = value | |
| self.var.set(self.format % value) | |
| return True | |
| class SharedDataAccess(object): | |
| """A utility class for reading data from the memory-mapped binary | |
| counters file.""" | |
| def __init__(self, data): | |
| """Create a new instance. | |
| Args: | |
| data: A handle to the memory-mapped file, as returned by mmap.mmap. | |
| """ | |
| self.data = data | |
| def ByteAt(self, index): | |
| """Return the (unsigned) byte at the specified byte index.""" | |
| return ord(self.CharAt(index)) | |
| def IntAt(self, index): | |
| """Return the little-endian 32-byte int at the specified byte index.""" | |
| word_str = self.data[index:index+4] | |
| result, = struct.unpack("I", word_str) | |
| return result | |
| def CharAt(self, index): | |
| """Return the ascii character at the specified byte index.""" | |
| return self.data[index] | |
| class Counter(object): | |
| """A pointer to a single counter within a binary counters file.""" | |
| def __init__(self, data, offset): | |
| """Create a new instance. | |
| Args: | |
| data: the shared data access object containing the counter | |
| offset: the byte offset of the start of this counter | |
| """ | |
| self.data = data | |
| self.offset = offset | |
| def Value(self): | |
| """Return the integer value of this counter.""" | |
| return self.data.IntAt(self.offset) | |
| def Name(self): | |
| """Return the ascii name of this counter.""" | |
| result = "" | |
| index = self.offset + 4 | |
| current = self.data.ByteAt(index) | |
| while current: | |
| result += chr(current) | |
| index += 1 | |
| current = self.data.ByteAt(index) | |
| return result | |
| class CounterCollection(object): | |
| """An overlay over a counters file that provides access to the | |
| individual counters contained in the file.""" | |
| def __init__(self, data): | |
| """Create a new instance. | |
| Args: | |
| data: the shared data access object | |
| """ | |
| self.data = data | |
| self.max_counters = data.IntAt(4) | |
| self.max_name_size = data.IntAt(8) | |
| def CountersInUse(self): | |
| """Return the number of counters in active use.""" | |
| return self.data.IntAt(12) | |
| def Counter(self, index): | |
| """Return the index'th counter.""" | |
| return Counter(self.data, 16 + index * self.CounterSize()) | |
| def CounterSize(self): | |
| """Return the size of a single counter.""" | |
| return 4 + self.max_name_size | |
| class ChromeCounter(object): | |
| """A pointer to a single counter within a binary counters file.""" | |
| def __init__(self, data, name_offset, value_offset): | |
| """Create a new instance. | |
| Args: | |
| data: the shared data access object containing the counter | |
| name_offset: the byte offset of the start of this counter's name | |
| value_offset: the byte offset of the start of this counter's value | |
| """ | |
| self.data = data | |
| self.name_offset = name_offset | |
| self.value_offset = value_offset | |
| def Value(self): | |
| """Return the integer value of this counter.""" | |
| return self.data.IntAt(self.value_offset) | |
| def Name(self): | |
| """Return the ascii name of this counter.""" | |
| result = "" | |
| index = self.name_offset | |
| current = self.data.ByteAt(index) | |
| while current: | |
| result += chr(current) | |
| index += 1 | |
| current = self.data.ByteAt(index) | |
| return result | |
| class ChromeCounterCollection(object): | |
| """An overlay over a counters file that provides access to the | |
| individual counters contained in the file.""" | |
| _HEADER_SIZE = 4 * 4 | |
| _COUNTER_NAME_SIZE = 64 | |
| _THREAD_NAME_SIZE = 32 | |
| def __init__(self, data): | |
| """Create a new instance. | |
| Args: | |
| data: the shared data access object | |
| """ | |
| self.data = data | |
| self.max_counters = data.IntAt(8) | |
| self.max_threads = data.IntAt(12) | |
| self.counter_names_offset = \ | |
| self._HEADER_SIZE + self.max_threads * (self._THREAD_NAME_SIZE + 2 * 4) | |
| self.counter_values_offset = \ | |
| self.counter_names_offset + self.max_counters * self._COUNTER_NAME_SIZE | |
| def CountersInUse(self): | |
| """Return the number of counters in active use.""" | |
| for i in xrange(self.max_counters): | |
| name_offset = self.counter_names_offset + i * self._COUNTER_NAME_SIZE | |
| if self.data.ByteAt(name_offset) == 0: | |
| return i | |
| return self.max_counters | |
| def Counter(self, i): | |
| """Return the i'th counter.""" | |
| name_offset = self.counter_names_offset + i * self._COUNTER_NAME_SIZE | |
| value_offset = self.counter_values_offset + i * self.max_threads * 4 | |
| return ChromeCounter(self.data, name_offset, value_offset) | |
| def Main(data_file, name_filter): | |
| """Run the stats counter. | |
| Args: | |
| data_file: The counters file to monitor. | |
| name_filter: The regexp filter to apply to counter names. | |
| """ | |
| StatsViewer(data_file, name_filter).Run() | |
| if __name__ == "__main__": | |
| parser = optparse.OptionParser("usage: %prog [--filter=re] " | |
| "<stats data>|<test_shell pid>") | |
| parser.add_option("--filter", | |
| default=".*", | |
| help=("regexp filter for counter names " | |
| "[default: %default]")) | |
| (options, args) = parser.parse_args() | |
| if len(args) != 1: | |
| parser.print_help() | |
| sys.exit(1) | |
| Main(args[0], re.compile(options.filter)) |