xref: /aosp_15_r20/external/autotest/utils/summarize_loadtest.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1#!/usr/bin/python3
2
3# Copyright 2017 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Load generator for devserver."""
8
9from __future__ import absolute_import
10from __future__ import division
11from __future__ import print_function
12
13import argparse
14import itertools
15import json
16import re
17import sys
18
19import common
20
21
22# Default keys to skip displaying.
23DEFAULT_SKIP = [
24    'build_name',
25    'devserver',
26    'name',
27    'parent',
28    'quick_provision',
29    'trigger_response',
30]
31
32# List of commandline arguments for easy filtering.
33FILTER_ARGS = [
34    'board',
35    'build_name',
36    'devserver',
37    'name',
38    'status',
39]
40
41
42def get_parser():
43    """Creates the argparse parser."""
44    parser = argparse.ArgumentParser(description=__doc__)
45    parser.add_argument('infile', nargs='*', type=argparse.FileType('r'),
46                        help='Path to JSON file to read.',
47                        default=[sys.stdin])
48    parser.add_argument('--boards', type=str, action='store',
49                        help='Boards to show.')
50    parser.add_argument('--group', type=str, action='store',
51                        help='Comma-spearated list of keys to group by.')
52    parser.add_argument('--dump', action='store_true',
53                        help='Dump all filtered entries.')
54    parser.add_argument('--skip', type=str, action='store',
55                        help='Comma-separated list of keys to skip displaying.',
56                        default=','.join(DEFAULT_SKIP))
57    parser.add_argument('--filter', type=str, action='store',
58                        help='Filter expression to apply to each node.')
59    for arg in FILTER_ARGS:
60        parser.add_argument('--%s' % arg, type=str, action='store',
61                            help='Comma-separated list of %s to filter by.' %
62                            arg)
63    parser.add_argument('--no-summary', action='store_false', dest='summary',
64                        help='Disable summary.')
65
66    return parser
67
68def summarize_entries(entries, skip=set()):
69    """Summarize a list of entries."""
70    TAG_KEYS = [
71        'board', 'build_name', 'devserver', 'name',
72        'parent', 'quick_provision', 'status'
73    ]
74    VALUE_KEYS = [
75        'avg_active', 'elapsed',
76    ]
77    summary = {
78        'COUNT': len(entries),
79    }
80    summary.update({key: summarize_tags(entries, key) for key in TAG_KEYS
81                    if key not in skip})
82    summary.update({key: summarize_values(entries, key) for key in VALUE_KEYS
83                    if key not in skip})
84    return summary
85
86def summarize_tags(entries, key):
87    """Summarize all the different string values for a given key."""
88    tags = {str(entry[key]) for entry in entries}
89    return list(tags)
90
91def summarize_values(entries, key):
92    """Summarize the numeric values for a given key."""
93    if entries is None or len(entries) == 0:
94        return None
95
96    values = [entry[key] for entry in entries if key in entry]
97    summary = {}
98    num_values = len(values)
99    if num_values:
100        summary['min'] = min(values)
101        summary['max'] = max(values)
102        summary['avg'] = sum(values) / num_values
103    num_skipped = len(entries) - num_values
104    if num_skipped:
105        summary['num'] = num_values
106        summary['skipped'] = num_skipped
107    return summary
108
109def group_entries(keys, entries):
110    """Group entries based on different values of given keys.
111
112    @param keys: A list of keys to group by.
113    @param entries: A list of entries to split into groups.
114
115    @return A list of list of entries, where each list has a different key
116            value.
117    """
118    if not keys:
119        return [entries]
120
121    # Divide the group based on the first key.
122    indexed = {}
123    for entry in entries:
124        value = str(entry[keys[0]])
125        indexed.setdefault(value, []).append(entry)
126    groups = [indexed[value] for value in sorted(indexed.keys())]
127
128    # Recursively subdivide all the groups based on the rest of the keys.
129    subgroups = []
130    for group in groups:
131        subgroups.extend(group_entries(keys[1:], group))
132    return subgroups
133
134def main(argv):
135    """Load generator for a devserver."""
136    parser = get_parser()
137    options = parser.parse_args(argv)
138
139    # Read entries from the specified file.
140    all_entries = []
141    for f in options.infile:
142        all_entries.extend([json.loads(line) for line in f])
143
144    # Filter entries:
145    # - Ignore non-provisions.
146    # - Filter via the specified FILTER_ARGS arguments.
147    # - Filter via explicit filter request.
148    entries = [x for x in all_entries if x['name'] != 'Runner']
149    for arg in FILTER_ARGS:
150        if options.__dict__.get(arg):
151            entries = [x for x in entries if x[arg] in
152                       options.__dict__[arg].split(',')]
153    if options.filter:
154        entries = [x for x in entries if eval(options.filter, {'re': re}, x)]
155
156    # Group the entries based on specified keys.
157    groups = group_entries(options.group.split(',') if options.group else None,
158                           entries)
159
160    # Dump all filtered entries as groups, including their parents.
161    if options.dump:
162        dump_entries = itertools.chain(*groups)
163        # Dump all entries, tracking needed parents.
164        parents = []
165        for entry in dump_entries:
166            print(json.dumps(entry))
167            if 'parent' in entry and entry['parent'] not in parents:
168                parents.append(entry['parent'])
169        # Dump all parents.
170        for entry in all_entries:
171            if entry['id'] in parents:
172                print(json.dumps(entry))
173
174    # Summarize the entries, group by group.
175    if options.summary:
176        skip = options.skip.split(',') if options.skip else set()
177        summaries = [summarize_entries(group, skip) for group in groups]
178        print(json.dumps(summaries, indent=2))
179
180if __name__ == '__main__':
181    sys.exit(main(sys.argv[1:]))
182