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