|
| 1 | +import re |
| 2 | +import argparse |
| 3 | +import os |
| 4 | +import sys |
| 5 | +import subprocess |
| 6 | +from prometheus_api_client import PrometheusConnect |
| 7 | +from utils import FLAMEGRAPHS_DIR, get_git_root |
| 8 | + |
| 9 | +def get_stack_lines(prom, group_by_kvs, stack_keys, metric_name, sum_metrics=None): |
| 10 | + """ |
| 11 | + Filters metrics from prometheus for entries that look like: |
| 12 | + [ { labels: [["key1", "span1;span2"], ["key2", "span3"]], "metric": metric_name, "value": 2 } ] |
| 13 | +
|
| 14 | + It will find entries that have all of stack_keys as present in the labels and then concatenate the corresponding values into a single flat stack entry and then add the value at the end. |
| 15 | + It will write a file with one line each for flamegraph.pl or inferno-flamegraph to consume. |
| 16 | + If sum_metrics is not None, instead of searching for metric_name, it will sum the values of the metrics in sum_metrics. |
| 17 | + """ |
| 18 | + lines = [] |
| 19 | + stack_sums = {} |
| 20 | + non_zero = False |
| 21 | + |
| 22 | + if sum_metrics is not None: |
| 23 | + regex = "|".join(re.escape(m) for m in sum_metrics) |
| 24 | + promql = f'{{__name__=~"^{regex}$"}}' |
| 25 | + metrics = prom.custom_query(promql) |
| 26 | + else: |
| 27 | + promql = f'{{__name__=~"^{metric_name}$"}}' |
| 28 | + metrics = prom.custom_query(promql) |
| 29 | + |
| 30 | + # Process metrics |
| 31 | + for metric in metrics: |
| 32 | + labels = metric['metric'] |
| 33 | + filter = False |
| 34 | + for key, value in group_by_kvs: |
| 35 | + if key not in labels or labels[key] != value: |
| 36 | + filter = True |
| 37 | + break |
| 38 | + if filter: |
| 39 | + continue |
| 40 | + |
| 41 | + stack_values = [] |
| 42 | + for key in stack_keys: |
| 43 | + if key not in labels: |
| 44 | + filter = True |
| 45 | + break |
| 46 | + stack_values.append(labels[key]) |
| 47 | + if filter: |
| 48 | + continue |
| 49 | + |
| 50 | + stack = ';'.join(stack_values) |
| 51 | + value = int(metric['value'][1]) |
| 52 | + stack_sums[stack] = stack_sums.get(stack, 0) + value |
| 53 | + |
| 54 | + if value != 0: |
| 55 | + non_zero = True |
| 56 | + |
| 57 | + lines = [f"{stack} {value}" for stack, value in stack_sums.items() if value != 0] |
| 58 | + |
| 59 | + # Currently cycle tracker does not use gauge |
| 60 | + return lines if non_zero else [] |
| 61 | + |
| 62 | + |
| 63 | +def create_flamegraph(fname, prom, group_by_kvs, stack_keys, metric_name, sum_metrics=None, reverse=False): |
| 64 | + lines = get_stack_lines(prom, group_by_kvs, stack_keys, metric_name, sum_metrics) |
| 65 | + if not lines: |
| 66 | + return |
| 67 | + |
| 68 | + suffixes = [key for key in stack_keys if key != "cycle_tracker_span"] |
| 69 | + |
| 70 | + git_root = get_git_root() |
| 71 | + flamegraph_dir = os.path.join(git_root, FLAMEGRAPHS_DIR) |
| 72 | + os.makedirs(flamegraph_dir, exist_ok=True) |
| 73 | + |
| 74 | + path_prefix = f"{flamegraph_dir}{fname}.{'.'.join(suffixes)}.{metric_name}{'.reverse' if reverse else ''}" |
| 75 | + stacks_path = f"{path_prefix}.stacks" |
| 76 | + flamegraph_path = f"{path_prefix}.svg" |
| 77 | + |
| 78 | + with open(stacks_path, 'w') as f: |
| 79 | + for line in lines: |
| 80 | + f.write(f"{line}\n") |
| 81 | + |
| 82 | + with open(flamegraph_path, 'w') as f: |
| 83 | + command = ["inferno-flamegraph", "--title", f"{fname} {' '.join(suffixes)} {metric_name}", stacks_path] |
| 84 | + if reverse: |
| 85 | + command.append("--reverse") |
| 86 | + |
| 87 | + subprocess.run(command, stdout=f, check=False) |
| 88 | + print(f"Created flamegraph at {flamegraph_path}") |
| 89 | + |
| 90 | + |
| 91 | +def create_flamegraphs(prom, group_by, stack_keys, metric_name, sum_metrics=None, reverse=False): |
| 92 | + # Assume group_by is a list of length 1 |
| 93 | + group_by_values_list = prom.get_label_values(label_name=group_by[0]) |
| 94 | + for group_by_values in group_by_values_list: |
| 95 | + group_by_kvs = list(zip(group_by, [group_by_values])) |
| 96 | + fname = 'metrics' + '-' + '-'.join([group_by_values]) |
| 97 | + create_flamegraph(fname, prom, group_by_kvs, stack_keys, metric_name, sum_metrics, reverse=reverse) |
| 98 | + |
| 99 | + |
| 100 | +def create_custom_flamegraphs(prom, group_by=["group"]): |
| 101 | + for reverse in [False, True]: |
| 102 | + create_flamegraphs(prom, group_by, ["cycle_tracker_span", "dsl_ir", "opcode"], "frequency", |
| 103 | + reverse=reverse) |
| 104 | + create_flamegraphs(prom, group_by, ["cycle_tracker_span", "dsl_ir", "opcode", "air_name"], "cells_used", |
| 105 | + reverse=reverse) |
| 106 | + create_flamegraphs(prom, group_by, ["cell_tracker_span"], "cells_used", |
| 107 | + sum_metrics=["simple_advice_cells", "fixed_cells", "lookup_advice_cells"], |
| 108 | + reverse=reverse) |
| 109 | + |
| 110 | + |
| 111 | +def main(): |
| 112 | + import shutil |
| 113 | + |
| 114 | + if not shutil.which("inferno-flamegraph"): |
| 115 | + print("You must have inferno-flamegraph installed to use this script.") |
| 116 | + sys.exit(1) |
| 117 | + |
| 118 | + argparser = argparse.ArgumentParser() |
| 119 | + argparser.add_argument('prometheus_url', type=str, help="Path to the prometheus server") |
| 120 | + args = argparser.parse_args() |
| 121 | + |
| 122 | + prom = PrometheusConnect(url=args.prometheus_url, disable_ssl=True) |
| 123 | + |
| 124 | + create_custom_flamegraphs(prom) |
| 125 | + |
| 126 | + |
| 127 | +if __name__ == '__main__': |
| 128 | + main() |
0 commit comments