aboutsummaryrefslogtreecommitdiff
path: root/benchstrace/strace.py
blob: efb35d47345301ac671af3dcf3bcc970a6e9a49d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import os
import re
import subprocess
import sys
import tempfile
from collections import namedtuple

KEYS = ["name", "calls", "seconds", "min", "max", "usecs_call"]
STraceRecord = namedtuple("STraceRecord", KEYS)


def find_program(name):
    syspath = os.environ.get("PATH", "")
    for x in syspath.split(":"):
        target = os.path.abspath(os.path.join(x, name))
        if os.path.exists(target):
            return target
    return ""


def parse_record(line):
    """
    Parse a single line from: strace -w -c
    :return: an StraceRecord
    """
    rec = line.split()
    data = dict(zip(*[KEYS, rec]))
    result = dict()
    for k, v in data.items():
        result[k] = v
        if k == "name":
            continue
        elif k == "calls" or k == "usecs_call":
            if "." in v:
                v = v[:v.find(".")]
            result[k] = int(v)
        else:
            result[k] = float(v)
    return STraceRecord(**result)


def parse_output(lines):
    """
    Parse all lines from: strace -w -c
    :return: a list of STraceRecords
    """
    result = []
    for i, line in enumerate(lines):
        if i < 2 or i > len(lines) - 3:
            continue
        result.append(parse_record(line))
    return result


class STrace:
    """Run strace command and parse statistical output
    """
    VERSION_RE = re.compile(r"strace.*version\s(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<patch>\d+)?")
    NEED_VERSION = (6, 0, 0)

    def __init__(self, command="", setup="", teardown="", output_file=""):
        self.program = find_program("strace")
        self.command = command
        major, minor, _ = self.version
        if major < 6:
            raise RuntimeError(f"strace {major}.{minor} is too old. Install {self.NEED_VERSION}, or greater.")
        self.records = self.run(setup, teardown) or []

    def run(self, setup="", teardown=""):
        """
        Execute strace
        :param setup: command to execute before `self.command`
        :param teardown: command to execute after `self.command`
        :return: a list of STraceRecords
        """
        handle, tmpfile = tempfile.mkstemp()
        os.close(handle)
        command = ["strace", "-o", tmpfile, "-w", "-c", "-S", "name", "-U", "name,calls,time-total,time-min,time-max,time-avg"] + self.command.split(" ")

        if setup:
            proc_setup = subprocess.run(setup.split())
            if proc_setup.returncode:
                print(f"Warning: setup command failed ({proc_setup.returncode})", file=sys.stderr)

        proc = subprocess.run(command, stderr=open("/dev/null", "w"))
        if proc.returncode:
            print("Warning: non-zero exit ({proc.returncode})", file=sys.stderr)

        if teardown:
            proc_teardown = subprocess.run(teardown.split())
            if proc_teardown.returncode:
                print(f"Warning: teardown command failed ({proc_teardown.returncode})", file=sys.stderr)

        data = open(tmpfile, "r").read().splitlines()
        os.remove(tmpfile)
        return parse_output(data)

    @property
    def version(self):
        """Retrieve version number from strace
        :return: tuple containing major, minor, and patch version
        """
        command = ["strace", "--version"]
        proc = subprocess.run(command, capture_output=True)
        data = proc.stdout.decode().splitlines()
        match = re.match(self.VERSION_RE, data[0])
        if not match:
            return 0, 0, 0

        result = match.groupdict()
        if not result.get("patch"):
            result["patch"] = 0
        return int(result["major"]), int(result["minor"]), int(result["patch"])