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