| OLD | NEW |
| (Empty) | |
| 1 # Copyright 2008 the V8 project authors. All rights reserved. |
| 2 # Redistribution and use in source and binary forms, with or without |
| 3 # modification, are permitted provided that the following conditions are |
| 4 # met: |
| 5 # |
| 6 # * Redistributions of source code must retain the above copyright |
| 7 # notice, this list of conditions and the following disclaimer. |
| 8 # * Redistributions in binary form must reproduce the above |
| 9 # copyright notice, this list of conditions and the following |
| 10 # disclaimer in the documentation and/or other materials provided |
| 11 # with the distribution. |
| 12 # * Neither the name of Google Inc. nor the names of its |
| 13 # contributors may be used to endorse or promote products derived |
| 14 # from this software without specific prior written permission. |
| 15 # |
| 16 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 17 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 18 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 19 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 20 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 23 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 24 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 25 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 26 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 27 |
| 28 |
| 29 """A cross-platform execution counter viewer. |
| 30 |
| 31 The stats viewer reads counters from a binary file and displays them |
| 32 in a window, re-reading and re-displaying with regular intervals. |
| 33 """ |
| 34 |
| 35 |
| 36 import mmap |
| 37 import os |
| 38 import struct |
| 39 import sys |
| 40 import time |
| 41 import Tkinter |
| 42 |
| 43 |
| 44 # The interval, in milliseconds, between ui updates |
| 45 UPDATE_INTERVAL_MS = 100 |
| 46 |
| 47 |
| 48 # Mapping from counter prefix to the formatting to be used for the counter |
| 49 COUNTER_LABELS = {"t": "%i ms.", "c": "%i"} |
| 50 |
| 51 |
| 52 # The magic number used to check if a file is not a counters file |
| 53 COUNTERS_FILE_MAGIC_NUMBER = 0xDEADFACE |
| 54 |
| 55 |
| 56 class StatsViewer(object): |
| 57 """The main class that keeps the data used by the stats viewer.""" |
| 58 |
| 59 def __init__(self, data_name): |
| 60 """Creates a new instance. |
| 61 |
| 62 Args: |
| 63 data_name: the name of the file containing the counters. |
| 64 """ |
| 65 self.data_name = data_name |
| 66 |
| 67 # The handle created by mmap.mmap to the counters file. We need |
| 68 # this to clean it up on exit. |
| 69 self.shared_mmap = None |
| 70 |
| 71 # A mapping from counter names to the ui element that displays |
| 72 # them |
| 73 self.ui_counters = {} |
| 74 |
| 75 # The counter collection used to access the counters file |
| 76 self.data = None |
| 77 |
| 78 # The Tkinter root window object |
| 79 self.root = None |
| 80 |
| 81 def Run(self): |
| 82 """The main entry-point to running the stats viewer.""" |
| 83 try: |
| 84 self.data = self.MountSharedData() |
| 85 # OpenWindow blocks until the main window is closed |
| 86 self.OpenWindow() |
| 87 finally: |
| 88 self.CleanUp() |
| 89 |
| 90 def MountSharedData(self): |
| 91 """Mount the binary counters file as a memory-mapped file. If |
| 92 something goes wrong print an informative message and exit the |
| 93 program.""" |
| 94 if not os.path.exists(self.data_name): |
| 95 print "File %s doesn't exist." % self.data_name |
| 96 sys.exit(1) |
| 97 data_file = open(self.data_name, "r") |
| 98 size = os.fstat(data_file.fileno()).st_size |
| 99 fileno = data_file.fileno() |
| 100 self.shared_mmap = mmap.mmap(fileno, size, access=mmap.ACCESS_READ) |
| 101 data_access = SharedDataAccess(self.shared_mmap) |
| 102 if data_access.IntAt(0) != COUNTERS_FILE_MAGIC_NUMBER: |
| 103 print "File %s is not stats data." % self.data_name |
| 104 sys.exit(1) |
| 105 return CounterCollection(data_access) |
| 106 |
| 107 def CleanUp(self): |
| 108 """Cleans up the memory mapped file if necessary.""" |
| 109 if self.shared_mmap: |
| 110 self.shared_mmap.close() |
| 111 |
| 112 def UpdateCounters(self): |
| 113 """Read the contents of the memory-mapped file and update the ui if |
| 114 necessary. If the same counters are present in the file as before |
| 115 we just update the existing labels. If any counters have been added |
| 116 or removed we scrap the existing ui and draw a new one. |
| 117 """ |
| 118 changed = False |
| 119 counters_in_use = self.data.CountersInUse() |
| 120 if counters_in_use != len(self.ui_counters): |
| 121 self.RefreshCounters() |
| 122 changed = True |
| 123 else: |
| 124 for i in xrange(self.data.CountersInUse()): |
| 125 counter = self.data.Counter(i) |
| 126 name = counter.Name() |
| 127 if name in self.ui_counters: |
| 128 value = counter.Value() |
| 129 ui_counter = self.ui_counters[name] |
| 130 counter_changed = ui_counter.Set(value) |
| 131 changed = (changed or counter_changed) |
| 132 else: |
| 133 self.RefreshCounters() |
| 134 changed = True |
| 135 break |
| 136 if changed: |
| 137 # The title of the window shows the last time the file was |
| 138 # changed. |
| 139 self.UpdateTime() |
| 140 self.ScheduleUpdate() |
| 141 |
| 142 def UpdateTime(self): |
| 143 """Update the title of the window with the current time.""" |
| 144 self.root.title("Stats Viewer [updated %s]" % time.strftime("%H:%M:%S")) |
| 145 |
| 146 def ScheduleUpdate(self): |
| 147 """Schedules the next ui update.""" |
| 148 self.root.after(UPDATE_INTERVAL_MS, lambda: self.UpdateCounters()) |
| 149 |
| 150 def RefreshCounters(self): |
| 151 """Tear down and rebuild the controls in the main window.""" |
| 152 counters = self.ComputeCounters() |
| 153 self.RebuildMainWindow(counters) |
| 154 |
| 155 def ComputeCounters(self): |
| 156 """Group the counters by the suffix of their name. |
| 157 |
| 158 Since the same code-level counter (for instance "X") can result in |
| 159 several variables in the binary counters file that differ only by a |
| 160 two-character prefix (for instance "c:X" and "t:X") counters are |
| 161 grouped by suffix and then displayed with custom formatting |
| 162 depending on their prefix. |
| 163 |
| 164 Returns: |
| 165 A mapping from suffixes to a list of counters with that suffix, |
| 166 sorted by prefix. |
| 167 """ |
| 168 names = {} |
| 169 for i in xrange(self.data.CountersInUse()): |
| 170 counter = self.data.Counter(i) |
| 171 name = counter.Name() |
| 172 names[name] = counter |
| 173 |
| 174 # By sorting the keys we ensure that the prefixes always come in the |
| 175 # same order ("c:" before "t:") which looks more consistent in the |
| 176 # ui. |
| 177 sorted_keys = names.keys() |
| 178 sorted_keys.sort() |
| 179 |
| 180 # Group together the names whose suffix after a ':' are the same. |
| 181 groups = {} |
| 182 for name in sorted_keys: |
| 183 counter = names[name] |
| 184 if ":" in name: |
| 185 name = name[name.find(":")+1:] |
| 186 if not name in groups: |
| 187 groups[name] = [] |
| 188 groups[name].append(counter) |
| 189 |
| 190 return groups |
| 191 |
| 192 def RebuildMainWindow(self, groups): |
| 193 """Tear down and rebuild the main window. |
| 194 |
| 195 Args: |
| 196 groups: the groups of counters to display |
| 197 """ |
| 198 # Remove elements in the current ui |
| 199 self.ui_counters.clear() |
| 200 for child in self.root.children.values(): |
| 201 child.destroy() |
| 202 |
| 203 # Build new ui |
| 204 index = 0 |
| 205 sorted_groups = groups.keys() |
| 206 sorted_groups.sort() |
| 207 for counter_name in sorted_groups: |
| 208 counter_objs = groups[counter_name] |
| 209 name = Tkinter.Label(self.root, width=50, anchor=Tkinter.W, |
| 210 text=counter_name) |
| 211 name.grid(row=index, column=0, padx=1, pady=1) |
| 212 count = len(counter_objs) |
| 213 for i in xrange(count): |
| 214 counter = counter_objs[i] |
| 215 name = counter.Name() |
| 216 var = Tkinter.StringVar() |
| 217 value = Tkinter.Label(self.root, width=15, anchor=Tkinter.W, |
| 218 textvariable=var) |
| 219 value.grid(row=index, column=(1 + i), padx=1, pady=1) |
| 220 |
| 221 # If we know how to interpret the prefix of this counter then |
| 222 # add an appropriate formatting to the variable |
| 223 if (":" in name) and (name[0] in COUNTER_LABELS): |
| 224 format = COUNTER_LABELS[name[0]] |
| 225 else: |
| 226 format = "%i" |
| 227 ui_counter = UiCounter(var, format) |
| 228 self.ui_counters[name] = ui_counter |
| 229 ui_counter.Set(counter.Value()) |
| 230 index += 1 |
| 231 self.root.update() |
| 232 |
| 233 def OpenWindow(self): |
| 234 """Create and display the root window.""" |
| 235 self.root = Tkinter.Tk() |
| 236 |
| 237 # Tkinter is no good at resizing so we disable it |
| 238 self.root.resizable(width=False, height=False) |
| 239 self.RefreshCounters() |
| 240 self.ScheduleUpdate() |
| 241 self.root.mainloop() |
| 242 |
| 243 |
| 244 class UiCounter(object): |
| 245 """A counter in the ui.""" |
| 246 |
| 247 def __init__(self, var, format): |
| 248 """Creates a new ui counter. |
| 249 |
| 250 Args: |
| 251 var: the Tkinter string variable for updating the ui |
| 252 format: the format string used to format this counter |
| 253 """ |
| 254 self.var = var |
| 255 self.format = format |
| 256 self.last_value = None |
| 257 |
| 258 def Set(self, value): |
| 259 """Updates the ui for this counter. |
| 260 |
| 261 Args: |
| 262 value: The value to display |
| 263 |
| 264 Returns: |
| 265 True if the value had changed, otherwise False. The first call |
| 266 always returns True. |
| 267 """ |
| 268 if value == self.last_value: |
| 269 return False |
| 270 else: |
| 271 self.last_value = value |
| 272 self.var.set(self.format % value) |
| 273 return True |
| 274 |
| 275 |
| 276 class SharedDataAccess(object): |
| 277 """A utility class for reading data from the memory-mapped binary |
| 278 counters file.""" |
| 279 |
| 280 def __init__(self, data): |
| 281 """Create a new instance. |
| 282 |
| 283 Args: |
| 284 data: A handle to the memory-mapped file, as returned by mmap.mmap. |
| 285 """ |
| 286 self.data = data |
| 287 |
| 288 def ByteAt(self, index): |
| 289 """Return the (unsigned) byte at the specified byte index.""" |
| 290 return ord(self.CharAt(index)) |
| 291 |
| 292 def IntAt(self, index): |
| 293 """Return the little-endian 32-byte int at the specified byte index.""" |
| 294 word_str = self.data[index:index+4] |
| 295 result, = struct.unpack("I", word_str) |
| 296 return result |
| 297 |
| 298 def CharAt(self, index): |
| 299 """Return the ascii character at the specified byte index.""" |
| 300 return self.data[index] |
| 301 |
| 302 |
| 303 class Counter(object): |
| 304 """A pointer to a single counter withing a binary counters file.""" |
| 305 |
| 306 def __init__(self, data, offset): |
| 307 """Create a new instance. |
| 308 |
| 309 Args: |
| 310 data: the shared data access object containing the counter |
| 311 offset: the byte offset of the start of this counter |
| 312 """ |
| 313 self.data = data |
| 314 self.offset = offset |
| 315 |
| 316 def Value(self): |
| 317 """Return the integer value of this counter.""" |
| 318 return self.data.IntAt(self.offset) |
| 319 |
| 320 def Name(self): |
| 321 """Return the ascii name of this counter.""" |
| 322 result = "" |
| 323 index = self.offset + 4 |
| 324 current = self.data.ByteAt(index) |
| 325 while current: |
| 326 result += chr(current) |
| 327 index += 1 |
| 328 current = self.data.ByteAt(index) |
| 329 return result |
| 330 |
| 331 |
| 332 class CounterCollection(object): |
| 333 """An overlay over a counters file that provides access to the |
| 334 individual counters contained in the file.""" |
| 335 |
| 336 def __init__(self, data): |
| 337 """Create a new instance. |
| 338 |
| 339 Args: |
| 340 data: the shared data access object |
| 341 """ |
| 342 self.data = data |
| 343 self.max_counters = data.IntAt(4) |
| 344 self.max_name_size = data.IntAt(8) |
| 345 |
| 346 def CountersInUse(self): |
| 347 """Return the number of counters in active use.""" |
| 348 return self.data.IntAt(12) |
| 349 |
| 350 def Counter(self, index): |
| 351 """Return the index'th counter.""" |
| 352 return Counter(self.data, 16 + index * self.CounterSize()) |
| 353 |
| 354 def CounterSize(self): |
| 355 """Return the size of a single counter.""" |
| 356 return 4 + self.max_name_size |
| 357 |
| 358 |
| 359 def Main(data_file): |
| 360 """Run the stats counter. |
| 361 |
| 362 Args: |
| 363 data_file: The counters file to monitor. |
| 364 """ |
| 365 StatsViewer(data_file).Run() |
| 366 |
| 367 |
| 368 if __name__ == "__main__": |
| 369 if len(sys.argv) != 2: |
| 370 print "Usage: stats-viewer.py <stats data>" |
| 371 sys.exit(1) |
| 372 Main(sys.argv[1]) |
| OLD | NEW |