|
|
|
#!/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 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):
|
|
|
|
"""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):
|
|
|
|
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:
|
|
|
|
m = re.search(r"/dev/shm/\S*", maps_file.read())
|
|
|
|
if m is not None and os.path.exists(m.group(0)):
|
|
|
|
self.data_name = m.group(0)
|
|
|
|
else:
|
|
|
|
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]
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
class ChromeCounter(object):
|
|
|
|
"""A pointer to a single counter withing 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
|
|
|
|
_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._NAME_SIZE + 2 * 4)
|
|
|
|
self.counter_values_offset = \
|
|
|
|
self.counter_names_offset + self.max_counters * self._NAME_SIZE
|
|
|
|
|
|
|
|
def CountersInUse(self):
|
|
|
|
"""Return the number of counters in active use."""
|
|
|
|
for i in xrange(self.max_counters):
|
|
|
|
if self.data.ByteAt(self.counter_names_offset + i * self._NAME_SIZE) == 0:
|
|
|
|
return i
|
|
|
|
return self.max_counters
|
|
|
|
|
|
|
|
def Counter(self, i):
|
|
|
|
"""Return the i'th counter."""
|
|
|
|
return ChromeCounter(self.data,
|
|
|
|
self.counter_names_offset + i * self._NAME_SIZE,
|
|
|
|
self.counter_values_offset + i * self.max_threads * 4)
|
|
|
|
|
|
|
|
|
|
|
|
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>|<test_shell pid>"
|
|
|
|
sys.exit(1)
|
|
|
|
Main(sys.argv[1])
|