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