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