add additional tool command to dump trace statistics
Florian Walbroel

Florian Walbroel commited on 2020-09-04 14:56:18
Showing 4 changed files, with 158 additions and 10 deletions.

... ...
@@ -25,7 +25,7 @@ apt-get install -y build-essential cmake gcc \
25 25
                    libprotobuf-c-dev libprotobuf-dev \
26 26
                    ninja-build \
27 27
                    protobuf-c-compiler protobuf-compiler \
28
-                   pylint3 python3 python3-protobuf
28
+                   pylint3 python3 python3-protobuf python3-tabulate
29 29
 ```
30 30
 
31 31
 For the graphical user interface, install the additional dependencies:
... ...
@@ -70,6 +70,7 @@ pyfile(dump)
70 70
 pyfile(gui)
71 71
 pyfile(parse)
72 72
 pyfile(processes)
73
+pyfile(stats)
73 74
 pyfile(tool)
74 75
 
75 76
 add_custom_target(
... ...
@@ -0,0 +1,81 @@
1
+# UProcTrace: User-space Process Tracing
2
+# Copyright 2020: Florian Walbroel, Bonn, Germany <florian@hswalbroel.de>
3
+# Copyleft: GNU LESSER GENERAL PUBLIC LICENSE version 3 (see LICENSE)
4
+"""
5
+Statistics for uproctrace trace files.
6
+"""
7
+
8
+import tabulate
9
+import uproctrace.processes
10
+
11
+# Map of process attribute to attribute title and unit
12
+_PROCESS_ATTRS = {
13
+    "cpu_time": ("CPU Time", "s"),
14
+    "sys_time": ("Kernel Time", "s"),
15
+    "user_time": ("User Time", "s"),
16
+    "in_block": ("Fileystem Input Operations", ""),
17
+    "ou_block": ("Fileystem Output Operations", ""),
18
+    "maj_flt": ("Major page fault count", ""),
19
+    "min_flt": ("Minor page fault count", ""),
20
+    "max_rss_kb": ("Maximum Resident Set Size", "KiB"),
21
+}
22
+
23
+
24
+def calculate_stats(upt_traces: list) -> dict:
25
+    """
26
+    Calculates trace statistics, such like the CPU time of processes, for
27
+    the given list of traces and returns mapping of process attribute to
28
+    tuple of (min value, mean value, max value, cummulative value).
29
+    """
30
+
31
+    # The overall statistics
32
+    attr_values = dict((attr, []) for attr in _PROCESS_ATTRS)
33
+
34
+    for upt_trace in upt_traces:
35
+
36
+        # Load all processes of the trace file
37
+        with open(upt_trace, "rb") as f:
38
+            processes = uproctrace.processes.Processes(f)
39
+
40
+        for process in processes._all_processes.values():
41
+
42
+            # Ignore processes for which we do not have full information
43
+            if process._begin is None or process._end is None:
44
+                continue
45
+
46
+            # Update the values
47
+            for attr in _PROCESS_ATTRS:
48
+                attr_values[attr].append(getattr(process, attr))
49
+
50
+    # Calulate the statistics
51
+    stats = dict()
52
+    for attr, values in attr_values.items():
53
+        if len(values) == 0:
54
+            stats[attr] = (0, 0, 0, 0)
55
+            continue
56
+
57
+        vmin = min(values)
58
+        vmax = max(values)
59
+        vcum = sum(values)
60
+        vmean = vcum / len(values)
61
+        stats[attr] = (vmin, vmean, vmax, vcum)
62
+
63
+    return stats
64
+
65
+
66
+def dump_stats(upt_traces: list):
67
+    """
68
+    Calculates trace statistics, such like the CPU time of processes, for
69
+    the given list of traces and dumps the statistics to standard output as
70
+    a table.
71
+    """
72
+    # Calulate the statistics
73
+    stats = calculate_stats(upt_traces)
74
+
75
+    rows = []
76
+    for attr, values in stats.items():
77
+        title, unit = _PROCESS_ATTRS[attr]
78
+        rows += [[title] + [f"{v:.2f}{unit:s}" for v in values]]
79
+
80
+    headers = ["Attribute", "Min", "Mean", "Max", "Cumulative"]
81
+    print(tabulate.tabulate(rows, headers=headers))
... ...
@@ -16,17 +15,42 @@ def dump(args):
16 15
     Dump all events in trace file to standard output.
17 16
     """
18 17
     import uproctrace.dump
19
-    with open(args.trace, 'rb') as proto_file:
18
+    for upt_trace in args.trace:
19
+        if len(args.trace) != 1:
20
+            print(f"[{upt_trace:s}]:")
21
+        with open(upt_trace, 'rb') as proto_file:
20 22
             while uproctrace.dump.dump_event(proto_file, sys.stdout):
21 23
                 pass
24
+        if len(args.trace) != 1:
25
+            print("")
26
+
27
+
28
+def stats(args):
29
+    """
30
+    Calculate trace statistics of trace file(s) and dump them to standard
31
+    output.
32
+    """
33
+    import uproctrace.stats
34
+    if not args.per_trace:
35
+        uproctrace.stats.dump_stats(args.trace)
36
+        return
37
+    for upt_trace in args.trace:
38
+        print(f"[{upt_trace:s}]:")
39
+        uproctrace.stats.dump_stats([upt_trace])
40
+        print("")
22 41
 
23 42
 
24 43
 def gui(args):
25 44
     """
26 45
     Run the graphical user interface.
27 46
     """
47
+    if len(args.trace) != 1:
48
+        print(
49
+            "error: upt-tool gui: only one trace file allowed",
50
+            file=sys.stderr)
51
+        return 1
28 52
     import uproctrace.gui
29
-    uproctrace.gui.run(args.trace)
53
+    uproctrace.gui.run(args.trace[0])
30 54
 
31 55
 
32 56
 def pstree(args):
... ...
@@ -34,8 +58,12 @@ def pstree(args):
34 58
     Print process tree.
35 59
     """
36 60
     import uproctrace.processes
37
-    with open(args.trace, 'rb') as proto_file:
61
+    for upt_trace in args.trace:
62
+        if len(args.trace) != 1:
63
+            print(f"[{upt_trace:s}]:")
64
+        with open(upt_trace, 'rb') as proto_file:
38 65
             processes = uproctrace.processes.Processes(proto_file)
66
+
39 67
         # tree output (iterative)
40 68
         to_be_output = [processes.toplevel]
41 69
         while to_be_output:
... ...
@@ -61,18 +89,55 @@ def parse_args():
61 89
     """
62 90
     # set up main parser
63 91
     parser = argparse.ArgumentParser(description='UProcTrace tool.')
64
-    parser.add_argument('trace', metavar='<trace.upt>', help='trace file')
92
+    parser.add_argument(
93
+        'trace',
94
+        metavar='<trace.upt>',
95
+        nargs='+',
96
+        help="""
97
+        The UPT trace file(s).
98
+        """)
99
+
100
+    # Create sub parsers
65 101
     subparsers = parser.add_subparsers()
102
+
66 103
     # dump
67
-    dump_parser = subparsers.add_parser('dump', help='Dump events to stdout.')
104
+    dump_parser = subparsers.add_parser(
105
+        'dump', help="""
106
+        Dump events to stdout.
107
+        """)
68 108
     dump_parser.set_defaults(func=dump)
109
+
69 110
     # gui
70
-    gui_parser = subparsers.add_parser('gui',
71
-                                       help='Run graphical user interface.')
111
+    gui_parser = subparsers.add_parser(
112
+        'gui',
113
+        help="""
114
+        Run graphical user interface. Only supports a single trace file.
115
+        """)
72 116
     gui_parser.set_defaults(func=gui)
117
+
73 118
     # pstree
74
-    pstree_parser = subparsers.add_parser('pstree', help='Print process tree.')
119
+    pstree_parser = subparsers.add_parser(
120
+        'pstree', help="""
121
+        Print process tree.
122
+        """)
75 123
     pstree_parser.set_defaults(func=pstree)
124
+
125
+    # stats
126
+    stats_parser = subparsers.add_parser(
127
+        'stats', help="""
128
+      Dump trace statistics to stdout.
129
+      """)
130
+    stats_parser.add_argument(
131
+        "-p",
132
+        "--per-trace",
133
+        default=False,
134
+        action="store_true",
135
+        help="""
136
+        If to calculate trace statistics per individual trace, instead of
137
+        calculating statistics over all traces.
138
+        """)
139
+    stats_parser.set_defaults(func=stats)
140
+
76 141
     # parse
77 142
     args = parser.parse_args()
78 143
     if not hasattr(args, 'func'):
... ...
@@ -87,4 +152,4 @@ def main():
87 152
     Parse command line arguments and execute selected action.
88 153
     """
89 154
     args = parse_args()
90
-    args.func(args)
155
+    sys.exit(args.func(args))
91 156