1#!/usr/bin/env python2.7 2 3# Copyright 2017 gRPC authors. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import collections 18import ctypes 19import math 20import sys 21import yaml 22import json 23 24with open('src/core/lib/debug/stats_data.yaml') as f: 25 attrs = yaml.load(f.read()) 26 27REQUIRED_FIELDS = ['name', 'doc'] 28 29 30def make_type(name, fields): 31 return (collections.namedtuple( 32 name, ' '.join(list(set(REQUIRED_FIELDS + fields)))), []) 33 34 35def c_str(s, encoding='ascii'): 36 if isinstance(s, unicode): 37 s = s.encode(encoding) 38 result = '' 39 for c in s: 40 if not (32 <= ord(c) < 127) or c in ('\\', '"'): 41 result += '\\%03o' % ord(c) 42 else: 43 result += c 44 return '"' + result + '"' 45 46 47types = ( 48 make_type('Counter', []), 49 make_type('Histogram', ['max', 'buckets']), 50) 51 52inst_map = dict((t[0].__name__, t[1]) for t in types) 53 54stats = [] 55 56for attr in attrs: 57 found = False 58 for t, lst in types: 59 t_name = t.__name__.lower() 60 if t_name in attr: 61 name = attr[t_name] 62 del attr[t_name] 63 lst.append(t(name=name, **attr)) 64 found = True 65 break 66 assert found, "Bad decl: %s" % attr 67 68 69def dbl2u64(d): 70 return ctypes.c_ulonglong.from_buffer(ctypes.c_double(d)).value 71 72 73def shift_works_until(mapped_bounds, shift_bits): 74 for i, ab in enumerate(zip(mapped_bounds, mapped_bounds[1:])): 75 a, b = ab 76 if (a >> shift_bits) == (b >> shift_bits): 77 return i 78 return len(mapped_bounds) 79 80 81def find_ideal_shift(mapped_bounds, max_size): 82 best = None 83 for shift_bits in reversed(range(0, 64)): 84 n = shift_works_until(mapped_bounds, shift_bits) 85 if n == 0: continue 86 table_size = mapped_bounds[n - 1] >> shift_bits 87 if table_size > max_size: continue 88 if table_size > 65535: continue 89 if best is None: 90 best = (shift_bits, n, table_size) 91 elif best[1] < n: 92 best = (shift_bits, n, table_size) 93 print best 94 return best 95 96 97def gen_map_table(mapped_bounds, shift_data): 98 tbl = [] 99 cur = 0 100 print mapped_bounds 101 mapped_bounds = [x >> shift_data[0] for x in mapped_bounds] 102 print mapped_bounds 103 for i in range(0, mapped_bounds[shift_data[1] - 1]): 104 while i > mapped_bounds[cur]: 105 cur += 1 106 tbl.append(cur) 107 return tbl 108 109 110static_tables = [] 111 112 113def decl_static_table(values, type): 114 global static_tables 115 v = (type, values) 116 for i, vp in enumerate(static_tables): 117 if v == vp: return i 118 print "ADD TABLE: %s %r" % (type, values) 119 r = len(static_tables) 120 static_tables.append(v) 121 return r 122 123 124def type_for_uint_table(table): 125 mv = max(table) 126 if mv < 2**8: 127 return 'uint8_t' 128 elif mv < 2**16: 129 return 'uint16_t' 130 elif mv < 2**32: 131 return 'uint32_t' 132 else: 133 return 'uint64_t' 134 135 136def gen_bucket_code(histogram): 137 bounds = [0, 1] 138 done_trivial = False 139 done_unmapped = False 140 first_nontrivial = None 141 first_unmapped = None 142 while len(bounds) < histogram.buckets + 1: 143 if len(bounds) == histogram.buckets: 144 nextb = int(histogram.max) 145 else: 146 mul = math.pow( 147 float(histogram.max) / bounds[-1], 148 1.0 / (histogram.buckets + 1 - len(bounds))) 149 nextb = int(math.ceil(bounds[-1] * mul)) 150 if nextb <= bounds[-1] + 1: 151 nextb = bounds[-1] + 1 152 elif not done_trivial: 153 done_trivial = True 154 first_nontrivial = len(bounds) 155 bounds.append(nextb) 156 bounds_idx = decl_static_table(bounds, 'int') 157 if done_trivial: 158 first_nontrivial_code = dbl2u64(first_nontrivial) 159 code_bounds = [dbl2u64(x) - first_nontrivial_code for x in bounds] 160 shift_data = find_ideal_shift(code_bounds[first_nontrivial:], 161 256 * histogram.buckets) 162 #print first_nontrivial, shift_data, bounds 163 #if shift_data is not None: print [hex(x >> shift_data[0]) for x in code_bounds[first_nontrivial:]] 164 code = 'value = GPR_CLAMP(value, 0, %d);\n' % histogram.max 165 map_table = gen_map_table(code_bounds[first_nontrivial:], shift_data) 166 if first_nontrivial is None: 167 code += ('GRPC_STATS_INC_HISTOGRAM(GRPC_STATS_HISTOGRAM_%s, value);\n' % 168 histogram.name.upper()) 169 else: 170 code += 'if (value < %d) {\n' % first_nontrivial 171 code += ('GRPC_STATS_INC_HISTOGRAM(GRPC_STATS_HISTOGRAM_%s, value);\n' % 172 histogram.name.upper()) 173 code += 'return;\n' 174 code += '}' 175 first_nontrivial_code = dbl2u64(first_nontrivial) 176 if shift_data is not None: 177 map_table_idx = decl_static_table(map_table, 178 type_for_uint_table(map_table)) 179 code += 'union { double dbl; uint64_t uint; } _val, _bkt;\n' 180 code += '_val.dbl = value;\n' 181 code += 'if (_val.uint < %dull) {\n' % ( 182 (map_table[-1] << shift_data[0]) + first_nontrivial_code) 183 code += 'int bucket = ' 184 code += 'grpc_stats_table_%d[((_val.uint - %dull) >> %d)] + %d;\n' % ( 185 map_table_idx, first_nontrivial_code, shift_data[0], 186 first_nontrivial) 187 code += '_bkt.dbl = grpc_stats_table_%d[bucket];\n' % bounds_idx 188 code += 'bucket -= (_val.uint < _bkt.uint);\n' 189 code += 'GRPC_STATS_INC_HISTOGRAM(GRPC_STATS_HISTOGRAM_%s, bucket);\n' % histogram.name.upper( 190 ) 191 code += 'return;\n' 192 code += '}\n' 193 code += 'GRPC_STATS_INC_HISTOGRAM(GRPC_STATS_HISTOGRAM_%s, ' % histogram.name.upper( 194 ) 195 code += 'grpc_stats_histo_find_bucket_slow(value, grpc_stats_table_%d, %d));\n' % ( 196 bounds_idx, histogram.buckets) 197 return (code, bounds_idx) 198 199 200# utility: print a big comment block into a set of files 201def put_banner(files, banner): 202 for f in files: 203 print >> f, '/*' 204 for line in banner: 205 print >> f, ' * %s' % line 206 print >> f, ' */' 207 print >> f 208 209 210with open('src/core/lib/debug/stats_data.h', 'w') as H: 211 # copy-paste copyright notice from this file 212 with open(sys.argv[0]) as my_source: 213 copyright = [] 214 for line in my_source: 215 if line[0] != '#': break 216 for line in my_source: 217 if line[0] == '#': 218 copyright.append(line) 219 break 220 for line in my_source: 221 if line[0] != '#': 222 break 223 copyright.append(line) 224 put_banner([H], [line[2:].rstrip() for line in copyright]) 225 226 put_banner( 227 [H], 228 ["Automatically generated by tools/codegen/core/gen_stats_data.py"]) 229 230 print >> H, "#ifndef GRPC_CORE_LIB_DEBUG_STATS_DATA_H" 231 print >> H, "#define GRPC_CORE_LIB_DEBUG_STATS_DATA_H" 232 print >> H 233 print >> H, "#include <grpc/support/port_platform.h>" 234 print >> H 235 print >> H, "#include <inttypes.h>" 236 print >> H, "#include \"src/core/lib/iomgr/exec_ctx.h\"" 237 print >> H 238 239 for typename, instances in sorted(inst_map.items()): 240 print >> H, "typedef enum {" 241 for inst in instances: 242 print >> H, " GRPC_STATS_%s_%s," % (typename.upper(), 243 inst.name.upper()) 244 print >> H, " GRPC_STATS_%s_COUNT" % (typename.upper()) 245 print >> H, "} grpc_stats_%ss;" % (typename.lower()) 246 print >> H, "extern const char *grpc_stats_%s_name[GRPC_STATS_%s_COUNT];" % ( 247 typename.lower(), typename.upper()) 248 print >> H, "extern const char *grpc_stats_%s_doc[GRPC_STATS_%s_COUNT];" % ( 249 typename.lower(), typename.upper()) 250 251 histo_start = [] 252 histo_buckets = [] 253 histo_bucket_boundaries = [] 254 255 print >> H, "typedef enum {" 256 first_slot = 0 257 for histogram in inst_map['Histogram']: 258 histo_start.append(first_slot) 259 histo_buckets.append(histogram.buckets) 260 print >> H, " GRPC_STATS_HISTOGRAM_%s_FIRST_SLOT = %d," % ( 261 histogram.name.upper(), first_slot) 262 print >> H, " GRPC_STATS_HISTOGRAM_%s_BUCKETS = %d," % ( 263 histogram.name.upper(), histogram.buckets) 264 first_slot += histogram.buckets 265 print >> H, " GRPC_STATS_HISTOGRAM_BUCKETS = %d" % first_slot 266 print >> H, "} grpc_stats_histogram_constants;" 267 268 print >> H, "#if defined(GRPC_COLLECT_STATS) || !defined(NDEBUG)" 269 for ctr in inst_map['Counter']: 270 print >> H, ("#define GRPC_STATS_INC_%s() " + 271 "GRPC_STATS_INC_COUNTER(GRPC_STATS_COUNTER_%s)") % ( 272 ctr.name.upper(), ctr.name.upper()) 273 for histogram in inst_map['Histogram']: 274 print >> H, "#define GRPC_STATS_INC_%s(value) grpc_stats_inc_%s( (int)(value))" % ( 275 histogram.name.upper(), histogram.name.lower()) 276 print >> H, "void grpc_stats_inc_%s(int x);" % histogram.name.lower() 277 278 print >> H, "#else" 279 for ctr in inst_map['Counter']: 280 print >> H, ("#define GRPC_STATS_INC_%s() ") % (ctr.name.upper()) 281 for histogram in inst_map['Histogram']: 282 print >> H, "#define GRPC_STATS_INC_%s(value)" % ( 283 histogram.name.upper()) 284 print >> H, "#endif /* defined(GRPC_COLLECT_STATS) || !defined(NDEBUG) */" 285 286 for i, tbl in enumerate(static_tables): 287 print >> H, "extern const %s grpc_stats_table_%d[%d];" % (tbl[0], i, 288 len(tbl[1])) 289 290 print >> H, "extern const int grpc_stats_histo_buckets[%d];" % len( 291 inst_map['Histogram']) 292 print >> H, "extern const int grpc_stats_histo_start[%d];" % len( 293 inst_map['Histogram']) 294 print >> H, "extern const int *const grpc_stats_histo_bucket_boundaries[%d];" % len( 295 inst_map['Histogram']) 296 print >> H, "extern void (*const grpc_stats_inc_histogram[%d])(int x);" % len( 297 inst_map['Histogram']) 298 299 print >> H 300 print >> H, "#endif /* GRPC_CORE_LIB_DEBUG_STATS_DATA_H */" 301 302with open('src/core/lib/debug/stats_data.cc', 'w') as C: 303 # copy-paste copyright notice from this file 304 with open(sys.argv[0]) as my_source: 305 copyright = [] 306 for line in my_source: 307 if line[0] != '#': break 308 for line in my_source: 309 if line[0] == '#': 310 copyright.append(line) 311 break 312 for line in my_source: 313 if line[0] != '#': 314 break 315 copyright.append(line) 316 put_banner([C], [line[2:].rstrip() for line in copyright]) 317 318 put_banner( 319 [C], 320 ["Automatically generated by tools/codegen/core/gen_stats_data.py"]) 321 322 print >> C, "#include <grpc/support/port_platform.h>" 323 print >> C 324 print >> C, "#include \"src/core/lib/debug/stats.h\"" 325 print >> C, "#include \"src/core/lib/debug/stats_data.h\"" 326 print >> C, "#include \"src/core/lib/gpr/useful.h\"" 327 print >> C, "#include \"src/core/lib/iomgr/exec_ctx.h\"" 328 print >> C 329 330 histo_code = [] 331 for histogram in inst_map['Histogram']: 332 code, bounds_idx = gen_bucket_code(histogram) 333 histo_bucket_boundaries.append(bounds_idx) 334 histo_code.append(code) 335 336 for typename, instances in sorted(inst_map.items()): 337 print >> C, "const char *grpc_stats_%s_name[GRPC_STATS_%s_COUNT] = {" % ( 338 typename.lower(), typename.upper()) 339 for inst in instances: 340 print >> C, " %s," % c_str(inst.name) 341 print >> C, "};" 342 print >> C, "const char *grpc_stats_%s_doc[GRPC_STATS_%s_COUNT] = {" % ( 343 typename.lower(), typename.upper()) 344 for inst in instances: 345 print >> C, " %s," % c_str(inst.doc) 346 print >> C, "};" 347 348 for i, tbl in enumerate(static_tables): 349 print >> C, "const %s grpc_stats_table_%d[%d] = {%s};" % ( 350 tbl[0], i, len(tbl[1]), ','.join('%s' % x for x in tbl[1])) 351 352 for histogram, code in zip(inst_map['Histogram'], histo_code): 353 print >> C, ("void grpc_stats_inc_%s(int value) {%s}") % ( 354 histogram.name.lower(), code) 355 356 print >> C, "const int grpc_stats_histo_buckets[%d] = {%s};" % (len( 357 inst_map['Histogram']), ','.join('%s' % x for x in histo_buckets)) 358 print >> C, "const int grpc_stats_histo_start[%d] = {%s};" % (len( 359 inst_map['Histogram']), ','.join('%s' % x for x in histo_start)) 360 print >> C, "const int *const grpc_stats_histo_bucket_boundaries[%d] = {%s};" % ( 361 len(inst_map['Histogram']), ','.join( 362 'grpc_stats_table_%d' % x for x in histo_bucket_boundaries)) 363 print >> C, "void (*const grpc_stats_inc_histogram[%d])(int x) = {%s};" % ( 364 len(inst_map['Histogram']), ','.join( 365 'grpc_stats_inc_%s' % histogram.name.lower() 366 for histogram in inst_map['Histogram'])) 367 368# patch qps_test bigquery schema 369RECORD_EXPLICIT_PERCENTILES = [50, 95, 99] 370 371with open('tools/run_tests/performance/scenario_result_schema.json', 'r') as f: 372 qps_schema = json.loads(f.read()) 373 374 375def FindNamed(js, name): 376 for el in js: 377 if el['name'] == name: 378 return el 379 380 381def RemoveCoreFields(js): 382 new_fields = [] 383 for field in js['fields']: 384 if not field['name'].startswith('core_'): 385 new_fields.append(field) 386 js['fields'] = new_fields 387 388 389RemoveCoreFields(FindNamed(qps_schema, 'clientStats')) 390RemoveCoreFields(FindNamed(qps_schema, 'serverStats')) 391 392 393def AddCoreFields(js): 394 for counter in inst_map['Counter']: 395 js['fields'].append({ 396 'name': 'core_%s' % counter.name, 397 'type': 'INTEGER', 398 'mode': 'NULLABLE' 399 }) 400 for histogram in inst_map['Histogram']: 401 js['fields'].append({ 402 'name': 'core_%s' % histogram.name, 403 'type': 'STRING', 404 'mode': 'NULLABLE' 405 }) 406 js['fields'].append({ 407 'name': 'core_%s_bkts' % histogram.name, 408 'type': 'STRING', 409 'mode': 'NULLABLE' 410 }) 411 for pctl in RECORD_EXPLICIT_PERCENTILES: 412 js['fields'].append({ 413 'name': 'core_%s_%dp' % (histogram.name, pctl), 414 'type': 'FLOAT', 415 'mode': 'NULLABLE' 416 }) 417 418 419AddCoreFields(FindNamed(qps_schema, 'clientStats')) 420AddCoreFields(FindNamed(qps_schema, 'serverStats')) 421 422with open('tools/run_tests/performance/scenario_result_schema.json', 'w') as f: 423 f.write(json.dumps(qps_schema, indent=2, sort_keys=True)) 424 425# and generate a helper script to massage scenario results into the format we'd 426# like to query 427with open('tools/run_tests/performance/massage_qps_stats.py', 'w') as P: 428 with open(sys.argv[0]) as my_source: 429 for line in my_source: 430 if line[0] != '#': break 431 for line in my_source: 432 if line[0] == '#': 433 print >> P, line.rstrip() 434 break 435 for line in my_source: 436 if line[0] != '#': 437 break 438 print >> P, line.rstrip() 439 440 print >> P 441 print >> P, '# Autogenerated by tools/codegen/core/gen_stats_data.py' 442 print >> P 443 444 print >> P, 'import massage_qps_stats_helpers' 445 446 print >> P, 'def massage_qps_stats(scenario_result):' 447 print >> P, ' for stats in scenario_result["serverStats"] + scenario_result["clientStats"]:' 448 print >> P, ' if "coreStats" in stats:' 449 print >> P, ' # Get rid of the "coreStats" element and replace it by statistics' 450 print >> P, ' # that correspond to columns in the bigquery schema.' 451 print >> P, ' core_stats = stats["coreStats"]' 452 print >> P, ' del stats["coreStats"]' 453 for counter in inst_map['Counter']: 454 print >> P, ' stats["core_%s"] = massage_qps_stats_helpers.counter(core_stats, "%s")' % ( 455 counter.name, counter.name) 456 for i, histogram in enumerate(inst_map['Histogram']): 457 print >> P, ' h = massage_qps_stats_helpers.histogram(core_stats, "%s")' % histogram.name 458 print >> P, ' stats["core_%s"] = ",".join("%%f" %% x for x in h.buckets)' % histogram.name 459 print >> P, ' stats["core_%s_bkts"] = ",".join("%%f" %% x for x in h.boundaries)' % histogram.name 460 for pctl in RECORD_EXPLICIT_PERCENTILES: 461 print >> P, ' stats["core_%s_%dp"] = massage_qps_stats_helpers.percentile(h.buckets, %d, h.boundaries)' % ( 462 histogram.name, pctl, pctl) 463 464with open('src/core/lib/debug/stats_data_bq_schema.sql', 'w') as S: 465 columns = [] 466 for counter in inst_map['Counter']: 467 columns.append(('%s_per_iteration' % counter.name, 'FLOAT')) 468 print >> S, ',\n'.join('%s:%s' % x for x in columns) 469