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 38 import mmap 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): 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): 99 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() 113 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) 118 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) 124 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 262 class 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 294 class 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 321 class 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 350 class 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 377 class 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 408 class 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 443 def 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 452 if __name__ == "__main__": 453 if len(sys.argv) != 2: 454 print "Usage: stats-viewer.py <stats data>|<test_shell pid>" 455 sys.exit(1) 456 Main(sys.argv[1]) 457