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 import csv, splaytree, sys, re 29 from operator import itemgetter 30 import getopt, os, string 31 32 class CodeEntry(object): 33 34 def __init__(self, start_addr, name): 35 self.start_addr = start_addr 36 self.tick_count = 0 37 self.name = name 38 self.stacks = {} 39 40 def Tick(self, pc, stack): 41 self.tick_count += 1 42 if len(stack) > 0: 43 stack.insert(0, self.ToString()) 44 stack_key = tuple(stack) 45 self.stacks[stack_key] = self.stacks.setdefault(stack_key, 0) + 1 46 47 def RegionTicks(self): 48 return None 49 50 def SetStartAddress(self, start_addr): 51 self.start_addr = start_addr 52 53 def ToString(self): 54 return self.name 55 56 def IsSharedLibraryEntry(self): 57 return False 58 59 def IsICEntry(self): 60 return False 61 62 def IsJSFunction(self): 63 return False 64 65 class SharedLibraryEntry(CodeEntry): 66 67 def __init__(self, start_addr, name): 68 CodeEntry.__init__(self, start_addr, name) 69 70 def IsSharedLibraryEntry(self): 71 return True 72 73 74 class JSCodeEntry(CodeEntry): 75 76 def __init__(self, start_addr, name, type, size, assembler): 77 CodeEntry.__init__(self, start_addr, name) 78 self.type = type 79 self.size = size 80 self.assembler = assembler 81 self.region_ticks = None 82 self.builtin_ic_re = re.compile('^(Keyed)?(Call|Load|Store)IC_') 83 84 def Tick(self, pc, stack): 85 super(JSCodeEntry, self).Tick(pc, stack) 86 if not pc is None: 87 offset = pc - self.start_addr 88 seen = [] 89 narrowest = None 90 narrowest_width = None 91 for region in self.Regions(): 92 if region.Contains(offset): 93 if (not region.name in seen): 94 seen.append(region.name) 95 if narrowest is None or region.Width() < narrowest.Width(): 96 narrowest = region 97 if len(seen) == 0: 98 return 99 if self.region_ticks is None: 100 self.region_ticks = {} 101 for name in seen: 102 if not name in self.region_ticks: 103 self.region_ticks[name] = [0, 0] 104 self.region_ticks[name][0] += 1 105 if name == narrowest.name: 106 self.region_ticks[name][1] += 1 107 108 def RegionTicks(self): 109 return self.region_ticks 110 111 def Regions(self): 112 if self.assembler: 113 return self.assembler.regions 114 else: 115 return [] 116 117 def ToString(self): 118 name = self.name 119 if name == '': 120 name = '<anonymous>' 121 elif name.startswith(' '): 122 name = '<anonymous>' + name 123 return self.type + ': ' + name 124 125 def IsICEntry(self): 126 return self.type in ('CallIC', 'LoadIC', 'StoreIC') or \ 127 (self.type == 'Builtin' and self.builtin_ic_re.match(self.name)) 128 129 def IsJSFunction(self): 130 return self.type in ('Function', 'LazyCompile', 'Script') 131 132 class CodeRegion(object): 133 134 def __init__(self, start_offset, name): 135 self.start_offset = start_offset 136 self.name = name 137 self.end_offset = None 138 139 def Contains(self, pc): 140 return (self.start_offset <= pc) and (pc <= self.end_offset) 141 142 def Width(self): 143 return self.end_offset - self.start_offset 144 145 146 class Assembler(object): 147 148 def __init__(self): 149 # Mapping from region ids to open regions 150 self.pending_regions = {} 151 self.regions = [] 152 153 154 class FunctionEnumerator(object): 155 156 def __init__(self): 157 self.known_funcs = {} 158 self.next_func_id = 0 159 160 def GetFunctionId(self, name): 161 if not self.known_funcs.has_key(name): 162 self.known_funcs[name] = self.next_func_id 163 self.next_func_id += 1 164 return self.known_funcs[name] 165 166 def GetKnownFunctions(self): 167 known_funcs_items = self.known_funcs.items(); 168 known_funcs_items.sort(key = itemgetter(1)) 169 result = [] 170 for func, id_not_used in known_funcs_items: 171 result.append(func) 172 return result 173 174 175 VMStates = { 'JS': 0, 'GC': 1, 'COMPILER': 2, 'OTHER': 3, 'EXTERNAL' : 4 } 176 177 178 class TickProcessor(object): 179 180 def __init__(self): 181 self.log_file = '' 182 self.deleted_code = [] 183 self.vm_extent = {} 184 # Map from assembler ids to the pending assembler objects 185 self.pending_assemblers = {} 186 # Map from code addresses the have been allocated but not yet officially 187 # created to their assemblers. 188 self.assemblers = {} 189 self.js_entries = splaytree.SplayTree() 190 self.cpp_entries = splaytree.SplayTree() 191 self.total_number_of_ticks = 0 192 self.number_of_library_ticks = 0 193 self.unaccounted_number_of_ticks = 0 194 self.excluded_number_of_ticks = 0 195 self.number_of_gc_ticks = 0 196 # Flag indicating whether to ignore unaccounted ticks in the report 197 self.ignore_unknown = False 198 self.func_enum = FunctionEnumerator() 199 self.packed_stacks = [] 200 201 def ProcessLogfile(self, filename, included_state = None, ignore_unknown = False, separate_ic = False, call_graph_json = False): 202 self.log_file = filename 203 self.included_state = included_state 204 self.ignore_unknown = ignore_unknown 205 self.separate_ic = separate_ic 206 self.call_graph_json = call_graph_json 207 208 try: 209 logfile = open(filename, 'rb') 210 except IOError: 211 sys.exit("Could not open logfile: " + filename) 212 try: 213 try: 214 logreader = csv.reader(logfile) 215 row_num = 1 216 for row in logreader: 217 row_num += 1 218 if row[0] == 'tick': 219 self.ProcessTick(int(row[1], 16), int(row[2], 16), int(row[3], 16), int(row[4]), self.PreprocessStack(row[5:])) 220 elif row[0] == 'code-creation': 221 self.ProcessCodeCreation(row[1], int(row[2], 16), int(row[3]), row[4]) 222 elif row[0] == 'code-move': 223 self.ProcessCodeMove(int(row[1], 16), int(row[2], 16)) 224 elif row[0] == 'code-delete': 225 self.ProcessCodeDelete(int(row[1], 16)) 226 elif row[0] == 'function-creation': 227 self.ProcessFunctionCreation(int(row[1], 16), int(row[2], 16)) 228 elif row[0] == 'function-move': 229 self.ProcessFunctionMove(int(row[1], 16), int(row[2], 16)) 230 elif row[0] == 'function-delete': 231 self.ProcessFunctionDelete(int(row[1], 16)) 232 elif row[0] == 'shared-library': 233 self.AddSharedLibraryEntry(row[1], int(row[2], 16), int(row[3], 16)) 234 self.ParseVMSymbols(row[1], int(row[2], 16), int(row[3], 16)) 235 elif row[0] == 'begin-code-region': 236 self.ProcessBeginCodeRegion(int(row[1], 16), int(row[2], 16), int(row[3], 16), row[4]) 237 elif row[0] == 'end-code-region': 238 self.ProcessEndCodeRegion(int(row[1], 16), int(row[2], 16), int(row[3], 16)) 239 elif row[0] == 'code-allocate': 240 self.ProcessCodeAllocate(int(row[1], 16), int(row[2], 16)) 241 except csv.Error: 242 print("parse error in line " + str(row_num)) 243 raise 244 finally: 245 logfile.close() 246 247 def AddSharedLibraryEntry(self, filename, start, end): 248 # Mark the pages used by this library. 249 i = start 250 while i < end: 251 page = i >> 12 252 self.vm_extent[page] = 1 253 i += 4096 254 # Add the library to the entries so that ticks for which we do not 255 # have symbol information is reported as belonging to the library. 256 self.cpp_entries.Insert(start, SharedLibraryEntry(start, filename)) 257 258 def ParseVMSymbols(self, filename, start, end): 259 return 260 261 def ProcessCodeAllocate(self, addr, assem): 262 if assem in self.pending_assemblers: 263 assembler = self.pending_assemblers.pop(assem) 264 self.assemblers[addr] = assembler 265 266 def ProcessCodeCreation(self, type, addr, size, name): 267 if addr in self.assemblers: 268 assembler = self.assemblers.pop(addr) 269 else: 270 assembler = None 271 self.js_entries.Insert(addr, JSCodeEntry(addr, name, type, size, assembler)) 272 273 def ProcessCodeMove(self, from_addr, to_addr): 274 try: 275 removed_node = self.js_entries.Remove(from_addr) 276 removed_node.value.SetStartAddress(to_addr); 277 self.js_entries.Insert(to_addr, removed_node.value) 278 except splaytree.KeyNotFoundError: 279 print('Code move event for unknown code: 0x%x' % from_addr) 280 281 def ProcessCodeDelete(self, from_addr): 282 try: 283 removed_node = self.js_entries.Remove(from_addr) 284 self.deleted_code.append(removed_node.value) 285 except splaytree.KeyNotFoundError: 286 print('Code delete event for unknown code: 0x%x' % from_addr) 287 288 def ProcessFunctionCreation(self, func_addr, code_addr): 289 js_entry_node = self.js_entries.Find(code_addr) 290 if js_entry_node: 291 js_entry = js_entry_node.value 292 self.js_entries.Insert(func_addr, JSCodeEntry(func_addr, js_entry.name, js_entry.type, 1, None)) 293 294 def ProcessFunctionMove(self, from_addr, to_addr): 295 try: 296 removed_node = self.js_entries.Remove(from_addr) 297 removed_node.value.SetStartAddress(to_addr); 298 self.js_entries.Insert(to_addr, removed_node.value) 299 except splaytree.KeyNotFoundError: 300 return 301 302 def ProcessFunctionDelete(self, from_addr): 303 try: 304 removed_node = self.js_entries.Remove(from_addr) 305 self.deleted_code.append(removed_node.value) 306 except splaytree.KeyNotFoundError: 307 return 308 309 def ProcessBeginCodeRegion(self, id, assm, start, name): 310 if not assm in self.pending_assemblers: 311 self.pending_assemblers[assm] = Assembler() 312 assembler = self.pending_assemblers[assm] 313 assembler.pending_regions[id] = CodeRegion(start, name) 314 315 def ProcessEndCodeRegion(self, id, assm, end): 316 assm = self.pending_assemblers[assm] 317 region = assm.pending_regions.pop(id) 318 region.end_offset = end 319 assm.regions.append(region) 320 321 def IncludeTick(self, pc, sp, state): 322 return (self.included_state is None) or (self.included_state == state) 323 324 def FindEntry(self, pc): 325 page = pc >> 12 326 if page in self.vm_extent: 327 entry = self.cpp_entries.FindGreatestsLessThan(pc) 328 if entry != None: 329 return entry.value 330 else: 331 return entry 332 max = self.js_entries.FindMax() 333 min = self.js_entries.FindMin() 334 if max != None and pc < (max.key + max.value.size) and pc > min.key: 335 return self.js_entries.FindGreatestsLessThan(pc).value 336 return None 337 338 def PreprocessStack(self, stack): 339 # remove all non-addresses (e.g. 'overflow') and convert to int 340 result = [] 341 for frame in stack: 342 if frame.startswith('0x'): 343 result.append(int(frame, 16)) 344 return result 345 346 def ProcessStack(self, stack): 347 result = [] 348 for frame in stack: 349 entry = self.FindEntry(frame) 350 if entry != None: 351 result.append(entry.ToString()) 352 return result 353 354 def ProcessTick(self, pc, sp, func, state, stack): 355 if state == VMStates['GC']: 356 self.number_of_gc_ticks += 1 357 if not self.IncludeTick(pc, sp, state): 358 self.excluded_number_of_ticks += 1; 359 return 360 self.total_number_of_ticks += 1 361 entry = self.FindEntry(pc) 362 if entry == None: 363 self.unaccounted_number_of_ticks += 1 364 return 365 if entry.IsSharedLibraryEntry(): 366 self.number_of_library_ticks += 1 367 if entry.IsICEntry() and not self.separate_ic: 368 if len(stack) > 0: 369 caller_pc = stack.pop(0) 370 self.total_number_of_ticks -= 1 371 self.ProcessTick(caller_pc, sp, func, state, stack) 372 else: 373 self.unaccounted_number_of_ticks += 1 374 else: 375 processed_stack = self.ProcessStack(stack) 376 if not entry.IsSharedLibraryEntry() and not entry.IsJSFunction(): 377 func_entry_node = self.js_entries.Find(func) 378 if func_entry_node and func_entry_node.value.IsJSFunction(): 379 processed_stack.insert(0, func_entry_node.value.ToString()) 380 entry.Tick(pc, processed_stack) 381 if self.call_graph_json: 382 self.AddToPackedStacks(pc, stack) 383 384 def AddToPackedStacks(self, pc, stack): 385 full_stack = stack 386 full_stack.insert(0, pc) 387 func_names = self.ProcessStack(full_stack) 388 func_ids = [] 389 for func in func_names: 390 func_ids.append(self.func_enum.GetFunctionId(func)) 391 self.packed_stacks.append(func_ids) 392 393 def PrintResults(self): 394 if not self.call_graph_json: 395 self.PrintStatistics() 396 else: 397 self.PrintCallGraphJSON() 398 399 def PrintStatistics(self): 400 print('Statistical profiling result from %s, (%d ticks, %d unaccounted, %d excluded).' % 401 (self.log_file, 402 self.total_number_of_ticks, 403 self.unaccounted_number_of_ticks, 404 self.excluded_number_of_ticks)) 405 if self.total_number_of_ticks > 0: 406 js_entries = self.js_entries.ExportValueList() 407 js_entries.extend(self.deleted_code) 408 cpp_entries = self.cpp_entries.ExportValueList() 409 # Print the unknown ticks percentage if they are not ignored. 410 if not self.ignore_unknown and self.unaccounted_number_of_ticks > 0: 411 self.PrintHeader('Unknown') 412 self.PrintCounter(self.unaccounted_number_of_ticks) 413 # Print the library ticks. 414 self.PrintHeader('Shared libraries') 415 self.PrintEntries(cpp_entries, lambda e:e.IsSharedLibraryEntry()) 416 # Print the JavaScript ticks. 417 self.PrintHeader('JavaScript') 418 self.PrintEntries(js_entries, lambda e:not e.IsSharedLibraryEntry()) 419 # Print the C++ ticks. 420 self.PrintHeader('C++') 421 self.PrintEntries(cpp_entries, lambda e:not e.IsSharedLibraryEntry()) 422 # Print the GC ticks. 423 self.PrintHeader('GC') 424 self.PrintCounter(self.number_of_gc_ticks) 425 # Print call profile. 426 print('\n [Call profile]:') 427 print(' total call path') 428 js_entries.extend(cpp_entries) 429 self.PrintCallProfile(js_entries) 430 431 def PrintHeader(self, header_title): 432 print('\n [%s]:' % header_title) 433 print(' ticks total nonlib name') 434 435 def PrintCounter(self, ticks_count): 436 percentage = ticks_count * 100.0 / self.total_number_of_ticks 437 print(' %(ticks)5d %(total)5.1f%%' % { 438 'ticks' : ticks_count, 439 'total' : percentage, 440 }) 441 442 def PrintEntries(self, entries, condition): 443 # If ignoring unaccounted ticks don't include these in percentage 444 # calculations 445 number_of_accounted_ticks = self.total_number_of_ticks 446 if self.ignore_unknown: 447 number_of_accounted_ticks -= self.unaccounted_number_of_ticks 448 449 number_of_non_library_ticks = number_of_accounted_ticks - self.number_of_library_ticks 450 entries.sort(key=lambda e: (e.tick_count, e.ToString()), reverse=True) 451 for entry in entries: 452 if entry.tick_count > 0 and condition(entry): 453 total_percentage = entry.tick_count * 100.0 / number_of_accounted_ticks 454 if entry.IsSharedLibraryEntry(): 455 non_library_percentage = 0 456 else: 457 non_library_percentage = entry.tick_count * 100.0 / number_of_non_library_ticks 458 print(' %(ticks)5d %(total)5.1f%% %(nonlib)6.1f%% %(name)s' % { 459 'ticks' : entry.tick_count, 460 'total' : total_percentage, 461 'nonlib' : non_library_percentage, 462 'name' : entry.ToString() 463 }) 464 region_ticks = entry.RegionTicks() 465 if not region_ticks is None: 466 items = region_ticks.items() 467 items.sort(key=lambda e: e[1][1], reverse=True) 468 for (name, ticks) in items: 469 print(' flat cum') 470 print(' %(flat)5.1f%% %(accum)5.1f%% %(name)s' % { 471 'flat' : ticks[1] * 100.0 / entry.tick_count, 472 'accum' : ticks[0] * 100.0 / entry.tick_count, 473 'name': name 474 }) 475 476 def PrintCallProfile(self, entries): 477 all_stacks = {} 478 total_stacks = 0 479 for entry in entries: 480 all_stacks.update(entry.stacks) 481 for count in entry.stacks.itervalues(): 482 total_stacks += count 483 all_stacks_items = all_stacks.items(); 484 all_stacks_items.sort(key = itemgetter(1), reverse=True) 485 missing_percentage = (self.total_number_of_ticks - total_stacks) * 100.0 / self.total_number_of_ticks 486 print(' %(ticks)5d %(total)5.1f%% <no call path information>' % { 487 'ticks' : self.total_number_of_ticks - total_stacks, 488 'total' : missing_percentage 489 }) 490 for stack, count in all_stacks_items: 491 total_percentage = count * 100.0 / self.total_number_of_ticks 492 print(' %(ticks)5d %(total)5.1f%% %(call_path)s' % { 493 'ticks' : count, 494 'total' : total_percentage, 495 'call_path' : stack[0] + ' <- ' + stack[1] 496 }) 497 498 def PrintCallGraphJSON(self): 499 print('\nvar __profile_funcs = ["' + 500 '",\n"'.join(self.func_enum.GetKnownFunctions()) + 501 '"];') 502 print('var __profile_ticks = [') 503 str_packed_stacks = [] 504 for stack in self.packed_stacks: 505 str_packed_stacks.append('[' + ','.join(map(str, stack)) + ']') 506 print(',\n'.join(str_packed_stacks)) 507 print('];') 508 509 class CmdLineProcessor(object): 510 511 def __init__(self): 512 self.options = ["js", 513 "gc", 514 "compiler", 515 "other", 516 "external", 517 "ignore-unknown", 518 "separate-ic", 519 "call-graph-json"] 520 # default values 521 self.state = None 522 self.ignore_unknown = False 523 self.log_file = None 524 self.separate_ic = False 525 self.call_graph_json = False 526 527 def ProcessArguments(self): 528 try: 529 opts, args = getopt.getopt(sys.argv[1:], "jgcoe", self.options) 530 except getopt.GetoptError: 531 self.PrintUsageAndExit() 532 for key, value in opts: 533 if key in ("-j", "--js"): 534 self.state = VMStates['JS'] 535 if key in ("-g", "--gc"): 536 self.state = VMStates['GC'] 537 if key in ("-c", "--compiler"): 538 self.state = VMStates['COMPILER'] 539 if key in ("-o", "--other"): 540 self.state = VMStates['OTHER'] 541 if key in ("-e", "--external"): 542 self.state = VMStates['EXTERNAL'] 543 if key in ("--ignore-unknown"): 544 self.ignore_unknown = True 545 if key in ("--separate-ic"): 546 self.separate_ic = True 547 if key in ("--call-graph-json"): 548 self.call_graph_json = True 549 self.ProcessRequiredArgs(args) 550 551 def ProcessRequiredArgs(self, args): 552 return 553 554 def GetRequiredArgsNames(self): 555 return 556 557 def PrintUsageAndExit(self): 558 print('Usage: %(script_name)s --{%(opts)s} %(req_opts)s' % { 559 'script_name': os.path.basename(sys.argv[0]), 560 'opts': string.join(self.options, ','), 561 'req_opts': self.GetRequiredArgsNames() 562 }) 563 sys.exit(2) 564 565 def RunLogfileProcessing(self, tick_processor): 566 tick_processor.ProcessLogfile(self.log_file, self.state, self.ignore_unknown, 567 self.separate_ic, self.call_graph_json) 568 569 570 if __name__ == '__main__': 571 sys.exit('You probably want to run windows-tick-processor.py or linux-tick-processor.py.') 572