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 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: 113 print "Can't find counter file in maps for PID %s." % self.data_name 114 sys.exit(1) 115 finally: 116 maps_file.close() 117 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) 122 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) 128 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] 231 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) 235 count = len(counter_objs) 236 for i in xrange(count): 237 counter = counter_objs[i] 238 name = counter.Name() 239 var = Tkinter.StringVar() 240 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) 244 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 268 class 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 300 class 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 327 class 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 356 class 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 383 class 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 414 class 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 419 _COUNTER_NAME_SIZE = 64 420 _THREAD_NAME_SIZE = 32 421 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 = \ 432 self._HEADER_SIZE + self.max_threads * (self._THREAD_NAME_SIZE + 2 * 4) 433 self.counter_values_offset = \ 434 self.counter_names_offset + self.max_counters * self._COUNTER_NAME_SIZE 435 436 def CountersInUse(self): 437 """Return the number of counters in active use.""" 438 for i in xrange(self.max_counters): 439 name_offset = self.counter_names_offset + i * self._COUNTER_NAME_SIZE 440 if self.data.ByteAt(name_offset) == 0: 441 return i 442 return self.max_counters 443 444 def Counter(self, i): 445 """Return the i'th counter.""" 446 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) 449 450 451 def Main(data_file, name_filter): 452 """Run the stats counter. 453 454 Args: 455 data_file: The counters file to monitor. 456 name_filter: The regexp filter to apply to counter names. 457 """ 458 StatsViewer(data_file, name_filter).Run() 459 460 461 if __name__ == "__main__": 462 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() 471 sys.exit(1) 472 Main(args[0], re.compile(options.filter)) 473