xref: /openbmc/openbmc/poky/scripts/pybootchartgui/pybootchartgui/parsing.py (revision c9537f57ab488bf5d90132917b0184e2527970a5)
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