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