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