1# This file is part of pybootchartgui. 2 3# pybootchartgui is free software: you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation, either version 3 of the License, or 6# (at your option) any later version. 7 8# pybootchartgui is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12 13# You should have received a copy of the GNU General Public License 14# along with pybootchartgui. If not, see <http://www.gnu.org/licenses/>. 15 16import os 17import string 18import re 19import sys 20import tarfile 21import time 22from collections import defaultdict 23from functools import reduce 24 25from .samples import * 26from .process_tree import ProcessTree 27 28if sys.version_info >= (3, 0): 29 long = int 30 31# Parsing produces as its end result a 'Trace' 32 33class Trace: 34 def __init__(self, writer, paths, options): 35 self.processes = {} 36 self.start = {} 37 self.end = {} 38 self.min = None 39 self.max = None 40 self.headers = None 41 self.disk_stats = [] 42 self.ps_stats = None 43 self.taskstats = None 44 self.cpu_stats = [] 45 self.cmdline = None 46 self.kernel = None 47 self.kernel_tree = None 48 self.filename = None 49 self.parent_map = None 50 self.mem_stats = [] 51 self.net_stats = [] 52 self.monitor_disk = None 53 self.cpu_pressure = [] 54 self.io_pressure = [] 55 self.mem_pressure = [] 56 self.times = [] # Always empty, but expected by draw.py when drawing system charts. 57 58 if len(paths): 59 parse_paths (writer, self, paths) 60 if not self.valid(): 61 raise ParseError("empty state: '%s' does not contain a valid bootchart" % ", ".join(paths)) 62 63 if options.full_time: 64 self.min = min(self.start.keys()) 65 self.max = max(self.end.keys()) 66 67 68 # Rendering system charts depends on start and end 69 # time. Provide them where the original drawing code expects 70 # them, i.e. in proc_tree. 71 class BitbakeProcessTree: 72 def __init__(self, start_time, end_time): 73 self.start_time = start_time 74 self.end_time = end_time 75 self.duration = self.end_time - self.start_time 76 self.proc_tree = BitbakeProcessTree(min(self.start.keys()), 77 max(self.end.keys())) 78 79 80 return 81 82 # Turn that parsed information into something more useful 83 # link processes into a tree of pointers, calculate statistics 84 self.compile(writer) 85 86 # Crop the chart to the end of the first idle period after the given 87 # process 88 if options.crop_after: 89 idle = self.crop (writer, options.crop_after) 90 else: 91 idle = None 92 93 # Annotate other times as the first start point of given process lists 94 self.times = [ idle ] 95 if options.annotate: 96 for procnames in options.annotate: 97 names = [x[:15] for x in procnames.split(",")] 98 for proc in self.ps_stats.process_map.values(): 99 if proc.cmd in names: 100 self.times.append(proc.start_time) 101 break 102 else: 103 self.times.append(None) 104 105 self.proc_tree = ProcessTree(writer, self.kernel, self.ps_stats, 106 self.ps_stats.sample_period, 107 self.headers.get("profile.process"), 108 options.prune, idle, self.taskstats, 109 self.parent_map is not None) 110 111 if self.kernel is not None: 112 self.kernel_tree = ProcessTree(writer, self.kernel, None, 0, 113 self.headers.get("profile.process"), 114 False, None, None, True) 115 116 def valid(self): 117 return len(self.processes) != 0 118 return self.headers != None and self.disk_stats != None and \ 119 self.ps_stats != None and self.cpu_stats != None 120 121 def add_process(self, process, start, end): 122 self.processes[process] = [start, end] 123 if start not in self.start: 124 self.start[start] = [] 125 if process not in self.start[start]: 126 self.start[start].append(process) 127 if end not in self.end: 128 self.end[end] = [] 129 if process not in self.end[end]: 130 self.end[end].append(process) 131 132 def compile(self, writer): 133 134 def find_parent_id_for(pid): 135 if pid == 0: 136 return 0 137 ppid = self.parent_map.get(pid) 138 if ppid: 139 # many of these double forks are so short lived 140 # that we have no samples, or process info for them 141 # so climb the parent hierarcy to find one 142 if int (ppid * 1000) not in self.ps_stats.process_map: 143# print "Pid '%d' short lived with no process" % ppid 144 ppid = find_parent_id_for (ppid) 145# else: 146# print "Pid '%d' has an entry" % ppid 147 else: 148# print "Pid '%d' missing from pid map" % pid 149 return 0 150 return ppid 151 152 # merge in the cmdline data 153 if self.cmdline is not None: 154 for proc in self.ps_stats.process_map.values(): 155 rpid = int (proc.pid // 1000) 156 if rpid in self.cmdline: 157 cmd = self.cmdline[rpid] 158 proc.exe = cmd['exe'] 159 proc.args = cmd['args'] 160# else: 161# print "proc %d '%s' not in cmdline" % (rpid, proc.exe) 162 163 # re-parent any stray orphans if we can 164 if self.parent_map is not None: 165 for process in self.ps_stats.process_map.values(): 166 ppid = find_parent_id_for (int(process.pid // 1000)) 167 if ppid: 168 process.ppid = ppid * 1000 169 170 # stitch the tree together with pointers 171 for process in self.ps_stats.process_map.values(): 172 process.set_parent (self.ps_stats.process_map) 173 174 # count on fingers variously 175 for process in self.ps_stats.process_map.values(): 176 process.calc_stats (self.ps_stats.sample_period) 177 178 def crop(self, writer, crop_after): 179 180 def is_idle_at(util, start, j): 181 k = j + 1 182 while k < len(util) and util[k][0] < start + 300: 183 k += 1 184 k = min(k, len(util)-1) 185 186 if util[j][1] >= 0.25: 187 return False 188 189 avgload = sum(u[1] for u in util[j:k+1]) / (k-j+1) 190 if avgload < 0.25: 191 return True 192 else: 193 return False 194 def is_idle(util, start): 195 for j in range(0, len(util)): 196 if util[j][0] < start: 197 continue 198 return is_idle_at(util, start, j) 199 else: 200 return False 201 202 names = [x[:15] for x in crop_after.split(",")] 203 for proc in self.ps_stats.process_map.values(): 204 if proc.cmd in names or proc.exe in names: 205 writer.info("selected proc '%s' from list (start %d)" 206 % (proc.cmd, proc.start_time)) 207 break 208 if proc is None: 209 writer.warn("no selected crop proc '%s' in list" % crop_after) 210 211 212 cpu_util = [(sample.time, sample.user + sample.sys + sample.io) for sample in self.cpu_stats] 213 disk_util = [(sample.time, sample.util) for sample in self.disk_stats] 214 215 idle = None 216 for i in range(0, len(cpu_util)): 217 if cpu_util[i][0] < proc.start_time: 218 continue 219 if is_idle_at(cpu_util, cpu_util[i][0], i) \ 220 and is_idle(disk_util, cpu_util[i][0]): 221 idle = cpu_util[i][0] 222 break 223 224 if idle is None: 225 writer.warn ("not idle after proc '%s'" % crop_after) 226 return None 227 228 crop_at = idle + 300 229 writer.info ("cropping at time %d" % crop_at) 230 while len (self.cpu_stats) \ 231 and self.cpu_stats[-1].time > crop_at: 232 self.cpu_stats.pop() 233 while len (self.disk_stats) \ 234 and self.disk_stats[-1].time > crop_at: 235 self.disk_stats.pop() 236 237 self.ps_stats.end_time = crop_at 238 239 cropped_map = {} 240 for key, value in self.ps_stats.process_map.items(): 241 if (value.start_time <= crop_at): 242 cropped_map[key] = value 243 244 for proc in cropped_map.values(): 245 proc.duration = min (proc.duration, crop_at - proc.start_time) 246 while len (proc.samples) \ 247 and proc.samples[-1].time > crop_at: 248 proc.samples.pop() 249 250 self.ps_stats.process_map = cropped_map 251 252 return idle 253 254 255 256class ParseError(Exception): 257 """Represents errors during parse of the bootchart.""" 258 def __init__(self, value): 259 self.value = value 260 261 def __str__(self): 262 return self.value 263 264def _parse_headers(file): 265 """Parses the headers of the bootchart.""" 266 def parse(acc, line): 267 (headers, last) = acc 268 if '=' in line: 269 last, value = map (lambda x: x.strip(), line.split('=', 1)) 270 else: 271 value = line.strip() 272 headers[last] += value 273 return headers, last 274 return reduce(parse, file.read().split('\n'), (defaultdict(str),''))[0] 275 276def _parse_timed_blocks(file): 277 """Parses (ie., splits) a file into so-called timed-blocks. A 278 timed-block consists of a timestamp on a line by itself followed 279 by zero or more lines of data for that point in time.""" 280 def parse(block): 281 lines = block.split('\n') 282 if not lines: 283 raise ParseError('expected a timed-block consisting a timestamp followed by data lines') 284 try: 285 return (int(lines[0]), lines[1:]) 286 except ValueError: 287 raise ParseError("expected a timed-block, but timestamp '%s' is not an integer" % lines[0]) 288 blocks = file.read().split('\n\n') 289 return [parse(block) for block in blocks if block.strip() and not block.endswith(' not running\n')] 290 291def _parse_proc_ps_log(writer, file): 292 """ 293 * See proc(5) for details. 294 * 295 * {pid, comm, state, ppid, pgrp, session, tty_nr, tpgid, flags, minflt, cminflt, majflt, cmajflt, utime, stime, 296 * cutime, cstime, priority, nice, 0, itrealvalue, starttime, vsize, rss, rlim, startcode, endcode, startstack, 297 * kstkesp, kstkeip} 298 """ 299 processMap = {} 300 ltime = 0 301 timed_blocks = _parse_timed_blocks(file) 302 for time, lines in timed_blocks: 303 for line in lines: 304 if not line: continue 305 tokens = line.split(' ') 306 if len(tokens) < 21: 307 continue 308 309 offset = [index for index, token in enumerate(tokens[1:]) if token[-1] == ')'][0] 310 pid, cmd, state, ppid = int(tokens[0]), ' '.join(tokens[1:2+offset]), tokens[2+offset], int(tokens[3+offset]) 311 userCpu, sysCpu, stime = int(tokens[13+offset]), int(tokens[14+offset]), int(tokens[21+offset]) 312 313 # magic fixed point-ness ... 314 pid *= 1000 315 ppid *= 1000 316 if pid in processMap: 317 process = processMap[pid] 318 process.cmd = cmd.strip('()') # why rename after latest name?? 319 else: 320 process = Process(writer, pid, cmd.strip('()'), ppid, min(time, stime)) 321 processMap[pid] = process 322 323 if process.last_user_cpu_time is not None and process.last_sys_cpu_time is not None and ltime is not None: 324 userCpuLoad, sysCpuLoad = process.calc_load(userCpu, sysCpu, max(1, time - ltime)) 325 cpuSample = CPUSample('null', userCpuLoad, sysCpuLoad, 0.0) 326 process.samples.append(ProcessSample(time, state, cpuSample)) 327 328 process.last_user_cpu_time = userCpu 329 process.last_sys_cpu_time = sysCpu 330 ltime = time 331 332 if len (timed_blocks) < 2: 333 return None 334 335 startTime = timed_blocks[0][0] 336 avgSampleLength = (ltime - startTime)/(len (timed_blocks) - 1) 337 338 return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime) 339 340def _parse_taskstats_log(writer, file): 341 """ 342 * See bootchart-collector.c for details. 343 * 344 * { pid, ppid, comm, cpu_run_real_total, blkio_delay_total, swapin_delay_total } 345 * 346 """ 347 processMap = {} 348 pidRewrites = {} 349 ltime = None 350 timed_blocks = _parse_timed_blocks(file) 351 for time, lines in timed_blocks: 352 # we have no 'stime' from taskstats, so prep 'init' 353 if ltime is None: 354 process = Process(writer, 1, '[init]', 0, 0) 355 processMap[1000] = process 356 ltime = time 357# continue 358 for line in lines: 359 if not line: continue 360 tokens = line.split(' ') 361 if len(tokens) != 6: 362 continue 363 364 opid, ppid, cmd = int(tokens[0]), int(tokens[1]), tokens[2] 365 cpu_ns, blkio_delay_ns, swapin_delay_ns = long(tokens[-3]), long(tokens[-2]), long(tokens[-1]), 366 367 # make space for trees of pids 368 opid *= 1000 369 ppid *= 1000 370 371 # when the process name changes, we re-write the pid. 372 if opid in pidRewrites: 373 pid = pidRewrites[opid] 374 else: 375 pid = opid 376 377 cmd = cmd.strip('(').strip(')') 378 if pid in processMap: 379 process = processMap[pid] 380 if process.cmd != cmd: 381 pid += 1 382 pidRewrites[opid] = pid 383# print "process mutation ! '%s' vs '%s' pid %s -> pid %s\n" % (process.cmd, cmd, opid, pid) 384 process = process.split (writer, pid, cmd, ppid, time) 385 processMap[pid] = process 386 else: 387 process.cmd = cmd; 388 else: 389 process = Process(writer, pid, cmd, ppid, time) 390 processMap[pid] = process 391 392 delta_cpu_ns = (float) (cpu_ns - process.last_cpu_ns) 393 delta_blkio_delay_ns = (float) (blkio_delay_ns - process.last_blkio_delay_ns) 394 delta_swapin_delay_ns = (float) (swapin_delay_ns - process.last_swapin_delay_ns) 395 396 # make up some state data ... 397 if delta_cpu_ns > 0: 398 state = "R" 399 elif delta_blkio_delay_ns + delta_swapin_delay_ns > 0: 400 state = "D" 401 else: 402 state = "S" 403 404 # retain the ns timing information into a CPUSample - that tries 405 # with the old-style to be a %age of CPU used in this time-slice. 406 if delta_cpu_ns + delta_blkio_delay_ns + delta_swapin_delay_ns > 0: 407# print "proc %s cpu_ns %g delta_cpu %g" % (cmd, cpu_ns, delta_cpu_ns) 408 cpuSample = CPUSample('null', delta_cpu_ns, 0.0, 409 delta_blkio_delay_ns, 410 delta_swapin_delay_ns) 411 process.samples.append(ProcessSample(time, state, cpuSample)) 412 413 process.last_cpu_ns = cpu_ns 414 process.last_blkio_delay_ns = blkio_delay_ns 415 process.last_swapin_delay_ns = swapin_delay_ns 416 ltime = time 417 418 if len (timed_blocks) < 2: 419 return None 420 421 startTime = timed_blocks[0][0] 422 avgSampleLength = (ltime - startTime)/(len(timed_blocks)-1) 423 424 return ProcessStats (writer, processMap, len (timed_blocks), avgSampleLength, startTime, ltime) 425 426def _parse_proc_stat_log(file): 427 samples = [] 428 ltimes = None 429 for time, lines in _parse_timed_blocks(file): 430 # skip emtpy lines 431 if not lines: 432 continue 433 # CPU times {user, nice, system, idle, io_wait, irq, softirq} 434 tokens = lines[0].split() 435 times = [ int(token) for token in tokens[1:] ] 436 if ltimes: 437 user = float((times[0] + times[1]) - (ltimes[0] + ltimes[1])) 438 system = float((times[2] + times[5] + times[6]) - (ltimes[2] + ltimes[5] + ltimes[6])) 439 idle = float(times[3] - ltimes[3]) 440 iowait = float(times[4] - ltimes[4]) 441 442 aSum = max(user + system + idle + iowait, 1) 443 samples.append( CPUSample(time, user/aSum, system/aSum, iowait/aSum) ) 444 445 ltimes = times 446 # skip the rest of statistics lines 447 return samples 448 449def _parse_reduced_log(file, sample_class): 450 samples = [] 451 for time, lines in _parse_timed_blocks(file): 452 samples.append(sample_class(time, *[float(x) for x in lines[0].split()])) 453 return samples 454 455def _parse_proc_disk_stat_log(file): 456 """ 457 Parse file for disk stats, but only look at the whole device, eg. sda, 458 not sda1, sda2 etc. The format of relevant lines should be: 459 {major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq} 460 """ 461 disk_regex_re = re.compile (r'^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') 462 463 # this gets called an awful lot. 464 def is_relevant_line(linetokens): 465 if len(linetokens) != 14: 466 return False 467 disk = linetokens[2] 468 return disk_regex_re.match(disk) 469 470 disk_stat_samples = [] 471 472 for time, lines in _parse_timed_blocks(file): 473 sample = DiskStatSample(time) 474 relevant_tokens = [linetokens for linetokens in map (lambda x: x.split(),lines) if is_relevant_line(linetokens)] 475 476 for tokens in relevant_tokens: 477 disk, rsect, wsect, use = tokens[2], int(tokens[5]), int(tokens[9]), int(tokens[12]) 478 sample.add_diskdata([rsect, wsect, use]) 479 480 disk_stat_samples.append(sample) 481 482 disk_stats = [] 483 for sample1, sample2 in zip(disk_stat_samples[:-1], disk_stat_samples[1:]): 484 interval = sample1.time - sample2.time 485 if interval == 0: 486 interval = 1 487 sums = [ a - b for a, b in zip(sample1.diskdata, sample2.diskdata) ] 488 readTput = sums[0] / 2.0 * 100.0 / interval 489 writeTput = sums[1] / 2.0 * 100.0 / interval 490 util = float( sums[2] ) / 10 / interval 491 util = max(0.0, min(1.0, util)) 492 disk_stats.append(DiskSample(sample2.time, readTput, writeTput, util)) 493 494 return disk_stats 495 496def _parse_reduced_proc_meminfo_log(file): 497 """ 498 Parse file for global memory statistics with 499 'MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree' values 500 (in that order) directly stored on one line. 501 """ 502 used_values = ('MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree',) 503 504 mem_stats = [] 505 for time, lines in _parse_timed_blocks(file): 506 sample = MemSample(time) 507 for name, value in zip(used_values, lines[0].split()): 508 sample.add_value(name, int(value)) 509 510 if sample.valid(): 511 mem_stats.append(DrawMemSample(sample)) 512 513 return mem_stats 514 515def _parse_proc_meminfo_log(file): 516 """ 517 Parse file for global memory statistics. 518 The format of relevant lines should be: ^key: value( unit)? 519 """ 520 used_values = ('MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree',) 521 522 mem_stats = [] 523 meminfo_re = re.compile(r'([^ \t:]+):\s*(\d+).*') 524 525 for time, lines in _parse_timed_blocks(file): 526 sample = MemSample(time) 527 528 for line in lines: 529 match = meminfo_re.match(line) 530 if not match: 531 raise ParseError("Invalid meminfo line \"%s\"" % line) 532 sample.add_value(match.group(1), int(match.group(2))) 533 534 if sample.valid(): 535 mem_stats.append(DrawMemSample(sample)) 536 537 return mem_stats 538 539def _parse_monitor_disk_log(file): 540 """ 541 Parse file with information about amount of diskspace used. 542 The format of relevant lines should be: ^volume path: number-of-bytes? 543 """ 544 disk_stats = [] 545 diskinfo_re = re.compile(r'^(.+):\s*(\d+)$') 546 547 for time, lines in _parse_timed_blocks(file): 548 sample = DiskSpaceSample(time) 549 550 for line in lines: 551 match = diskinfo_re.match(line) 552 if not match: 553 raise ParseError("Invalid monitor_disk line \"%s\"" % line) 554 sample.add_value(match.group(1), int(match.group(2))) 555 556 if sample.valid(): 557 disk_stats.append(sample) 558 559 return disk_stats 560 561 562def _parse_reduced_net_log(file): 563 net_stats = {} 564 for time, lines in _parse_timed_blocks(file): 565 566 for line in lines: 567 parts = line.split() 568 iface = parts[0][:-1] 569 if iface not in net_stats: 570 net_stats[iface] = [NetSample(time, iface, int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4]))] 571 else: 572 net_stats[iface].append(NetSample(time, iface, int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4]))) 573 return net_stats 574 575 576def _parse_pressure_logs(file, filename): 577 """ 578 Parse file for "some" pressure with 'avg10', 'avg60' 'avg300' and delta total values 579 (in that order) directly stored on one line for both CPU and IO, based on filename. 580 """ 581 pressure_stats = [] 582 if filename == "cpu.log": 583 SamplingClass = CPUPressureSample 584 elif filename == "memory.log": 585 SamplingClass = MemPressureSample 586 else: 587 SamplingClass = IOPressureSample 588 for time, lines in _parse_timed_blocks(file): 589 for line in lines: 590 if not line: continue 591 tokens = line.split() 592 avg10 = float(tokens[0]) 593 avg60 = float(tokens[1]) 594 avg300 = float(tokens[2]) 595 delta = float(tokens[3]) 596 pressure_stats.append(SamplingClass(time, avg10, avg60, avg300, delta)) 597 598 return pressure_stats 599 600# if we boot the kernel with: initcall_debug printk.time=1 we can 601# get all manner of interesting data from the dmesg output 602# We turn this into a pseudo-process tree: each event is 603# characterised by a 604# we don't try to detect a "kernel finished" state - since the kernel 605# continues to do interesting things after init is called. 606# 607# sample input: 608# [ 0.000000] ACPI: FACP 3f4fc000 000F4 (v04 INTEL Napa 00000001 MSFT 01000013) 609# ... 610# [ 0.039993] calling migration_init+0x0/0x6b @ 1 611# [ 0.039993] initcall migration_init+0x0/0x6b returned 1 after 0 usecs 612def _parse_dmesg(writer, file): 613 timestamp_re = re.compile (r"^\[\s*(\d+\.\d+)\s*]\s+(.*)$") 614 split_re = re.compile (r"^(\S+)\s+([\S\+_-]+) (.*)$") 615 processMap = {} 616 idx = 0 617 inc = 1.0 / 1000000 618 kernel = Process(writer, idx, "k-boot", 0, 0.1) 619 processMap['k-boot'] = kernel 620 base_ts = False 621 max_ts = 0 622 for line in file.read().split('\n'): 623 t = timestamp_re.match (line) 624 if t is None: 625# print "duff timestamp " + line 626 continue 627 628 time_ms = float (t.group(1)) * 1000 629 # looks like we may have a huge diff after the clock 630 # has been set up. This could lead to huge graph: 631 # so huge we will be killed by the OOM. 632 # So instead of using the plain timestamp we will 633 # use a delta to first one and skip the first one 634 # for convenience 635 if max_ts == 0 and not base_ts and time_ms > 1000: 636 base_ts = time_ms 637 continue 638 max_ts = max(time_ms, max_ts) 639 if base_ts: 640# print "fscked clock: used %f instead of %f" % (time_ms - base_ts, time_ms) 641 time_ms -= base_ts 642 m = split_re.match (t.group(2)) 643 644 if m is None: 645 continue 646# print "match: '%s'" % (m.group(1)) 647 type = m.group(1) 648 func = m.group(2) 649 rest = m.group(3) 650 651 if t.group(2).startswith ('Write protecting the') or \ 652 t.group(2).startswith ('Freeing unused kernel memory'): 653 kernel.duration = time_ms / 10 654 continue 655 656# print "foo: '%s' '%s' '%s'" % (type, func, rest) 657 if type == "calling": 658 ppid = kernel.pid 659 p = re.match (r"\@ (\d+)", rest) 660 if p is not None: 661 ppid = float (p.group(1)) // 1000 662# print "match: '%s' ('%g') at '%s'" % (func, ppid, time_ms) 663 name = func.split ('+', 1) [0] 664 idx += inc 665 processMap[func] = Process(writer, ppid + idx, name, ppid, time_ms / 10) 666 elif type == "initcall": 667# print "finished: '%s' at '%s'" % (func, time_ms) 668 if func in processMap: 669 process = processMap[func] 670 process.duration = (time_ms / 10) - process.start_time 671 else: 672 print("corrupted init call for %s" % (func)) 673 674 elif type == "async_waiting" or type == "async_continuing": 675 continue # ignore 676 677 return processMap.values() 678 679# 680# Parse binary pacct accounting file output if we have one 681# cf. /usr/include/linux/acct.h 682# 683def _parse_pacct(writer, file): 684 # read LE int32 685 def _read_le_int32(file): 686 byts = file.read(4) 687 return (ord(byts[0])) | (ord(byts[1]) << 8) | \ 688 (ord(byts[2]) << 16) | (ord(byts[3]) << 24) 689 690 parent_map = {} 691 parent_map[0] = 0 692 while file.read(1) != "": # ignore flags 693 ver = file.read(1) 694 if ord(ver) < 3: 695 print("Invalid version 0x%x" % (ord(ver))) 696 return None 697 698 file.seek (14, 1) # user, group etc. 699 pid = _read_le_int32 (file) 700 ppid = _read_le_int32 (file) 701# print "Parent of %d is %d" % (pid, ppid) 702 parent_map[pid] = ppid 703 file.seek (4 + 4 + 16, 1) # timings 704 file.seek (16, 1) # acct_comm 705 return parent_map 706 707def _parse_paternity_log(writer, file): 708 parent_map = {} 709 parent_map[0] = 0 710 for line in file.read().split('\n'): 711 if not line: 712 continue 713 elems = line.split(' ') # <Child> <Parent> 714 if len (elems) >= 2: 715# print "paternity of %d is %d" % (int(elems[0]), int(elems[1])) 716 parent_map[int(elems[0])] = int(elems[1]) 717 else: 718 print("Odd paternity line '%s'" % (line)) 719 return parent_map 720 721def _parse_cmdline_log(writer, file): 722 cmdLines = {} 723 for block in file.read().split('\n\n'): 724 lines = block.split('\n') 725 if len (lines) >= 3: 726# print "Lines '%s'" % (lines[0]) 727 pid = int (lines[0]) 728 values = {} 729 values['exe'] = lines[1].lstrip(':') 730 args = lines[2].lstrip(':').split('\0') 731 args.pop() 732 values['args'] = args 733 cmdLines[pid] = values 734 return cmdLines 735 736def _parse_bitbake_buildstats(writer, state, filename, file): 737 paths = filename.split("/") 738 task = paths[-1] 739 pn = paths[-2] 740 start = None 741 end = None 742 for line in file: 743 if line.startswith("Started:"): 744 start = int(float(line.split()[-1])) 745 elif line.startswith("Ended:"): 746 end = int(float(line.split()[-1])) 747 if start and end: 748 state.add_process(pn + ":" + task, start, end) 749 750def get_num_cpus(headers): 751 """Get the number of CPUs from the system.cpu header property. As the 752 CPU utilization graphs are relative, the number of CPUs currently makes 753 no difference.""" 754 if headers is None: 755 return 1 756 if headers.get("system.cpu.num"): 757 return max (int (headers.get("system.cpu.num")), 1) 758 cpu_model = headers.get("system.cpu") 759 if cpu_model is None: 760 return 1 761 mat = re.match(r".*\\((\\d+)\\)", cpu_model) 762 if mat is None: 763 return 1 764 return max (int(mat.group(1)), 1) 765 766def _do_parse(writer, state, filename, file): 767 writer.info("parsing '%s'" % filename) 768 t1 = time.process_time() 769 name = os.path.basename(filename) 770 if name == "proc_diskstats.log": 771 state.disk_stats = _parse_proc_disk_stat_log(file) 772 elif name == "reduced_proc_diskstats.log": 773 state.disk_stats = _parse_reduced_log(file, DiskSample) 774 elif name == "proc_stat.log": 775 state.cpu_stats = _parse_proc_stat_log(file) 776 elif name == "reduced_proc_stat.log": 777 state.cpu_stats = _parse_reduced_log(file, CPUSample) 778 elif name == "proc_meminfo.log": 779 state.mem_stats = _parse_proc_meminfo_log(file) 780 elif name == "reduced_proc_meminfo.log": 781 state.mem_stats = _parse_reduced_proc_meminfo_log(file) 782 elif name == "cmdline2.log": 783 state.cmdline = _parse_cmdline_log(writer, file) 784 elif name == "monitor_disk.log": 785 state.monitor_disk = _parse_monitor_disk_log(file) 786 elif name == "reduced_proc_net.log": 787 state.net_stats = _parse_reduced_net_log(file) 788 #pressure logs are in a subdirectory 789 elif name == "cpu.log": 790 state.cpu_pressure = _parse_pressure_logs(file, name) 791 elif name == "io.log": 792 state.io_pressure = _parse_pressure_logs(file, name) 793 elif name == "memory.log": 794 state.mem_pressure = _parse_pressure_logs(file, name) 795 elif not filename.endswith('.log'): 796 _parse_bitbake_buildstats(writer, state, filename, file) 797 t2 = time.process_time() 798 writer.info(" %s seconds" % str(t2-t1)) 799 return state 800 801def parse_file(writer, state, filename): 802 if state.filename is None: 803 state.filename = filename 804 basename = os.path.basename(filename) 805 with open(filename, "r") as file: 806 return _do_parse(writer, state, filename, file) 807 808def parse_paths(writer, state, paths): 809 for path in paths: 810 if state.filename is None: 811 state.filename = path 812 root, extension = os.path.splitext(path) 813 if not(os.path.exists(path)): 814 writer.warn("warning: path '%s' does not exist, ignoring." % path) 815 continue 816 #state.filename = path 817 if os.path.isdir(path): 818 files = sorted([os.path.join(path, f) for f in os.listdir(path)]) 819 state = parse_paths(writer, state, files) 820 elif extension in [".tar", ".tgz", ".gz"]: 821 if extension == ".gz": 822 root, extension = os.path.splitext(root) 823 if extension != ".tar": 824 writer.warn("warning: can only handle zipped tar files, not zipped '%s'-files; ignoring" % extension) 825 continue 826 tf = None 827 try: 828 writer.status("parsing '%s'" % path) 829 tf = tarfile.open(path, 'r:*') 830 for name in tf.getnames(): 831 state = _do_parse(writer, state, name, tf.extractfile(name)) 832 except tarfile.ReadError as error: 833 raise ParseError("error: could not read tarfile '%s': %s." % (path, error)) 834 finally: 835 if tf != None: 836 tf.close() 837 else: 838 state = parse_file(writer, state, path) 839 return state 840 841def split_res(res, options): 842 """ Split the res into n pieces """ 843 res_list = [] 844 if options.num > 1: 845 s_list = sorted(res.start.keys()) 846 frag_size = len(s_list) / float(options.num) 847 # Need the top value 848 if frag_size > int(frag_size): 849 frag_size = int(frag_size + 1) 850 else: 851 frag_size = int(frag_size) 852 853 start = 0 854 end = frag_size 855 while start < end: 856 state = Trace(None, [], None) 857 if options.full_time: 858 state.min = min(res.start.keys()) 859 state.max = max(res.end.keys()) 860 for i in range(start, end): 861 # Add this line for reference 862 #state.add_process(pn + ":" + task, start, end) 863 for p in res.start[s_list[i]]: 864 state.add_process(p, s_list[i], res.processes[p][1]) 865 start = end 866 end = end + frag_size 867 if end > len(s_list): 868 end = len(s_list) 869 res_list.append(state) 870 else: 871 res_list.append(res) 872 return res_list 873