| #!/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. |
| """ |
| |
| # for py2/py3 compatibility |
| from __future__ import print_function |
| |
| 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 range(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 range(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 range(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 range(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)) |