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