blob: 6dd8505062398da03d6fc29a77a3fec8999059be [file] [log] [blame]
David Riley9c382782017-11-23 16:41:34 -08001#!/usr/bin/python
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
David Rileyf0897b92017-12-14 13:25:35 -08009import argparse
David Riley9c382782017-11-23 16:41:34 -080010import itertools
11import json
David Riley9c382782017-11-23 16:41:34 -080012import pprint
13import re
14import sys
15
16import common
17from chromite.lib import commandline
18from chromite.lib import cros_logging as logging
19
20
David Riley9c382782017-11-23 16:41:34 -080021# Default keys to skip displaying.
22DEFAULT_SKIP = [
23 'build_name',
24 'devserver',
25 'name',
26 'parent',
27 'quick_provision',
David Riley7a2a7b92017-12-06 15:24:20 -080028 'trigger_response',
David Riley9c382782017-11-23 16:41:34 -080029]
30
31# List of commandline arguments for easy filtering.
32FILTER_ARGS = [
33 'board',
34 'build_name',
35 'devserver',
36 'name',
37 'status',
38]
39
40
41def get_parser():
David Riley48cac472017-12-18 12:23:28 -080042 """Creates the argparse parser."""
43 parser = commandline.ArgumentParser(description=__doc__)
44 parser.add_argument('infile', nargs='*', type=argparse.FileType('r'),
45 help='Path to JSON file to read.',
46 default=[sys.stdin])
47 parser.add_argument('--boards', type=str, action='store',
48 help='Boards to show.')
49 parser.add_argument('--group', type=str, action='store',
50 help='Comma-spearated list of keys to group by.')
51 parser.add_argument('--dump', action='store_true',
52 help='Dump all filtered entries.')
53 parser.add_argument('--skip', type=str, action='store',
54 help='Comma-separated list of keys to skip displaying.',
55 default=','.join(DEFAULT_SKIP))
56 parser.add_argument('--filter', type=str, action='store',
57 help='Filter expression to apply to each node.')
58 for arg in FILTER_ARGS:
59 parser.add_argument('--%s' % arg, type=str, action='store',
60 help='Comma-separated list of %s to filter by.' %
61 arg)
David Rileye3e67db2018-01-16 08:53:03 -080062 parser.add_argument('--no-summary', action='store_false', dest='summary',
63 help='Disable summary.')
David Riley9c382782017-11-23 16:41:34 -080064
David Riley48cac472017-12-18 12:23:28 -080065 return parser
David Riley9c382782017-11-23 16:41:34 -080066
David Riley9c382782017-11-23 16:41:34 -080067def summarize_entries(entries, skip=set()):
David Riley48cac472017-12-18 12:23:28 -080068 """Summarize a list of entries."""
69 TAG_KEYS = [
70 'board', 'build_name', 'devserver', 'name',
71 'parent', 'quick_provision', 'status'
72 ]
73 VALUE_KEYS = [
74 'avg_active', 'elapsed',
75 ]
76 summary = {
77 'COUNT': len(entries),
78 }
79 summary.update({key: summarize_tags(entries, key) for key in TAG_KEYS
80 if key not in skip})
81 summary.update({key: summarize_values(entries, key) for key in VALUE_KEYS
82 if key not in skip})
83 return summary
David Riley9c382782017-11-23 16:41:34 -080084
85def summarize_tags(entries, key):
David Riley48cac472017-12-18 12:23:28 -080086 """Summarize all the different string values for a given key."""
87 tags = {str(entry[key]) for entry in entries}
88 return list(tags)
David Riley9c382782017-11-23 16:41:34 -080089
90def summarize_values(entries, key):
David Riley48cac472017-12-18 12:23:28 -080091 """Summarize the numeric values for a given key."""
92 if entries is None or len(entries) == 0:
93 return None
David Riley9c382782017-11-23 16:41:34 -080094
David Riley48cac472017-12-18 12:23:28 -080095 values = [entry[key] for entry in entries if key in entry]
96 summary = {}
97 num_values = len(values)
98 if num_values:
99 summary['min'] = min(values)
100 summary['max'] = max(values)
101 summary['avg'] = sum(values) / num_values
102 num_skipped = len(entries) - num_values
103 if num_skipped:
104 summary['num'] = num_values
105 summary['skipped'] = num_skipped
106 return summary
David Riley9c382782017-11-23 16:41:34 -0800107
108def group_entries(keys, entries):
David Riley48cac472017-12-18 12:23:28 -0800109 """Group entries based on different values of given keys.
David Riley9c382782017-11-23 16:41:34 -0800110
David Riley48cac472017-12-18 12:23:28 -0800111 @param keys: A list of keys to group by.
112 @param entries: A list of entries to split into groups.
David Riley9c382782017-11-23 16:41:34 -0800113
David Riley48cac472017-12-18 12:23:28 -0800114 @return A list of list of entries, where each list has a different key
115 value.
116 """
117 if not keys:
118 return [entries]
David Riley9c382782017-11-23 16:41:34 -0800119
David Riley48cac472017-12-18 12:23:28 -0800120 # Divide the group based on the first key.
121 indexed = {}
122 for entry in entries:
123 value = str(entry[keys[0]])
124 indexed.setdefault(value, []).append(entry)
125 groups = [indexed[value] for value in sorted(indexed.keys())]
David Riley9c382782017-11-23 16:41:34 -0800126
David Riley48cac472017-12-18 12:23:28 -0800127 # Recursively subdivide all the groups based on the rest of the keys.
128 subgroups = []
129 for group in groups:
130 subgroups.extend(group_entries(keys[1:], group))
131 return subgroups
David Riley9c382782017-11-23 16:41:34 -0800132
133def main(argv):
David Riley48cac472017-12-18 12:23:28 -0800134 """Load generator for a devserver."""
135 parser = get_parser()
136 options = parser.parse_args(argv)
David Riley9c382782017-11-23 16:41:34 -0800137
David Riley48cac472017-12-18 12:23:28 -0800138 # Read entries from the specified file.
139 all_entries = []
140 for f in options.infile:
141 all_entries.extend([json.loads(line) for line in f])
David Riley9c382782017-11-23 16:41:34 -0800142
David Riley48cac472017-12-18 12:23:28 -0800143 # Filter entries:
144 # - Ignore non-provisions.
145 # - Filter via the specified FILTER_ARGS arguments.
146 # - Filter via explicit filter request.
147 entries = filter(lambda x: x['name'] != 'Runner', all_entries)
148 for arg in FILTER_ARGS:
149 if options.__dict__.get(arg):
150 entries = filter(lambda x: x[arg] in
151 options.__dict__[arg].split(','),
152 entries)
153 if options.filter:
David Rileye3e67db2018-01-16 08:53:03 -0800154 entries = filter(lambda x: eval(options.filter, {'re': re}, x), entries)
David Riley9c382782017-11-23 16:41:34 -0800155
David Riley48cac472017-12-18 12:23:28 -0800156 # Group the entries based on specified keys.
157 groups = group_entries(options.group.split(',') if options.group else None,
158 entries)
David Riley9c382782017-11-23 16:41:34 -0800159
David Riley48cac472017-12-18 12:23:28 -0800160 # 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))
David Rileye3e67db2018-01-16 08:53:03 -0800167 if 'parent' in entry and entry['parent'] not in parents:
David Riley48cac472017-12-18 12:23:28 -0800168 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))
David Riley9c382782017-11-23 16:41:34 -0800173
David Riley48cac472017-12-18 12:23:28 -0800174 # Summarize the entries, group by group.
David Rileye3e67db2018-01-16 08:53:03 -0800175 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))
David Riley9c382782017-11-23 16:41:34 -0800179
180if __name__ == '__main__':
David Riley48cac472017-12-18 12:23:28 -0800181 sys.exit(main(sys.argv[1:]))