| Index: tools/stats-viewer.py
|
| diff --git a/tools/stats-viewer.py b/tools/stats-viewer.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..bd6a8fb913e24e8983ad85664799e722a53bc39f
|
| --- /dev/null
|
| +++ b/tools/stats-viewer.py
|
| @@ -0,0 +1,372 @@
|
| +# 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 os
|
| +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 number used to check if a file is not a counters file
|
| +COUNTERS_FILE_MAGIC_NUMBER = 0xDEADFACE
|
| +
|
| +
|
| +class StatsViewer(object):
|
| + """The main class that keeps the data used by the stats viewer."""
|
| +
|
| + def __init__(self, data_name):
|
| + """Creates a new instance.
|
| +
|
| + Args:
|
| + data_name: the name of the file containing the counters.
|
| + """
|
| + self.data_name = data_name
|
| +
|
| + # 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):
|
| + print "File %s doesn't exist." % self.data_name
|
| + sys.exit(1)
|
| + 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:
|
| + print "File %s is not stats data." % self.data_name
|
| + sys.exit(1)
|
| + return CounterCollection(data_access)
|
| +
|
| + 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]
|
| + 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()
|
| + 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 withing 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
|
| +
|
| +
|
| +def Main(data_file):
|
| + """Run the stats counter.
|
| +
|
| + Args:
|
| + data_file: The counters file to monitor.
|
| + """
|
| + StatsViewer(data_file).Run()
|
| +
|
| +
|
| +if __name__ == "__main__":
|
| + if len(sys.argv) != 2:
|
| + print "Usage: stats-viewer.py <stats data>"
|
| + sys.exit(1)
|
| + Main(sys.argv[1])
|
|
|