From 9cbdafb0d322942a7b131064ee59d1e395425d42 Mon Sep 17 00:00:00 2001 From: Jose J Palacios-Perez Date: Mon, 23 Sep 2024 11:32:27 +0100 Subject: [PATCH] Add test plan examples using worlkloads. Add documentation Signed-off-by: Jose J Palacios-Perez --- README.md | 16 +- docs/AutomaticUnitTestGeneration.md | 6 +- docs/TestPlanSchema.md | 70 +++++ docs/Workloads.md | 46 +++ docs/benchmarks.png | Bin 0 -> 21786 bytes docs/cluster.png | Bin 0 -> 24911 bytes docs/toplevel.png | Bin 0 -> 14407 bytes docs/workloads.png | Bin 0 -> 9779 bytes tools/fio-parse-jsons/fio-parse-jsons.py | 383 +++++++++++++---------- 9 files changed, 349 insertions(+), 172 deletions(-) create mode 100644 docs/TestPlanSchema.md create mode 100644 docs/Workloads.md create mode 100644 docs/benchmarks.png create mode 100644 docs/cluster.png create mode 100644 docs/toplevel.png create mode 100644 docs/workloads.png diff --git a/README.md b/README.md index 557f9c31..e74139e5 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Optional tools and benchmarks can be used if desired: 6. fio - benchmark suite with integrated posix, libaio, and librbd support 7. cosbench - object storage benchmark from Intel + 8. pytest - to run the unit tests. ## USER AND NODE SETUP @@ -144,10 +145,12 @@ lab here: -## CREATING A YAML FILE +## CREATING A TEST PLAN YAML FILE CBT yaml files have a basic structure where you define a cluster and a set of -benchmarks to run against it. For example, the following yaml file creates a +benchmarks to run against it. The high level structure of a test plan is +detailed in the [documentation](docs/TestPlanSchema.md). +For example, the following yaml file creates a single node cluster on a node with hostname "burnupiX". A pool profile is defined for a 1x replication pool using 256 PGs, and that pool is used to run RBD performance tests using fio with the librbd engine. @@ -250,6 +253,15 @@ called mkcephconf.py lets you automatically generate hundreds or thousands of ceph.conf files from defined ranges of different options that can then be used with cbt in this way. +## RECENT FEATURES + +* Support for [workloads](docs/Workloads.md), that is sequence of performance tests, particularly +useful to generate *Response latency curves*. + +* Automatic unit test [generation](docs/AutomaticUnitTestGeneration.md) for the Benchmark classes, intended to help +refactoring to detect regressions. + + ## CONCLUSION There are many additional and powerful ways you can use cbt that are not yet diff --git a/docs/AutomaticUnitTestGeneration.md b/docs/AutomaticUnitTestGeneration.md index 9f8cb0e6..15258752 100644 --- a/docs/AutomaticUnitTestGeneration.md +++ b/docs/AutomaticUnitTestGeneration.md @@ -22,7 +22,7 @@ The following is an example of the execution of the script: ``` An example of the expected normal ouput is shown below. -![cbt_utests_gen](cbt_utest_gen.png) +![cbt_utests_gen](./cbt_utests_gen.png) This would have created (or updated if existing already) the set of unit tests for the supported benchmarks. @@ -35,7 +35,7 @@ The unit tests can be executed from the command line as follows: ``` An example output showing a successful execution: -![cbt_utests_gen](cbt_utest_gen.png) +![cbt_utests_run](./cbt_utests_run.png) Note: the tests skipped above require an environment variable to be defined to identify the target nodes for exercising pdsh. @@ -69,7 +69,7 @@ whether some attributes has been changed, replaced or deleted. This is especiall during code refactoring. -## Workflow recommeded +## Workflow recommeded * Before starting a code refactoring effort, run the unit tests: they should all pass as shown above. diff --git a/docs/TestPlanSchema.md b/docs/TestPlanSchema.md new file mode 100644 index 00000000..16b40076 --- /dev/null +++ b/docs/TestPlanSchema.md @@ -0,0 +1,70 @@ +# Test plan schema + +A valid test plan .yaml consists of the following compulsory sections at the top level (the level is +indicated by the indentation in .yaml: the top level has 0 indentation): + +* `cluster` +* `benchmarks`. + +It may also have the following optional sections at the same level: + +* `monitoring_profile` +* `client_endpoints`. + +![top_level](./toplevel.png) + +## `cluster` + +The cluster section enumerates the components of the Ceph cluster relevant to CBT. There are two +general classes of components: + +* scalars: for example names whose value is a string, a numeric or a boolean; +* collections: components that in turn contain further information, for example profile of pool +replication. + +The following are scalar compulsory entities: +* a head node: this is a string indicating the node that starts the cluster. +* a list of clients, each a string, representing a ssh-reachable host that has a benchmark +executable installed, +* a list of osds nodes, each of which has at least a running OSD process. + +![cluster](./cluster.png) + + +## `benchmarks` + +The benchmarks section consists of a non-empty list of collections, each describing a benchmark +entity. + +* A benchmark entity starts with its *name* (second level indentation), valid names are for example: +`radosbench`, `hsbench`, `kvmrbdfio`, `librbdfio`, etc. + +* The contents of the benchmark entity (third level indentation) consist of a collection of items +(either scalars or collections themselves). Most of these entities represent options for the +command line invocation of the benchmark when executed by the clients. + +![benchmarks](./benchmarks.png) + + +## `monitoring_profiles` + + +The monitoring_profiles section consists of a non-empty list of of collections, each describing a +monitoring tool. + +A monitoring entity starts with its name (at second level indentation). Currently supported are `perf` +, `collectl`, `top`. + +The contents of the monitoring entity consists of : +* a `nodes` (third level indentation) list of processes to monitor (by default the osd nodes), and +* an optional string `args` (third level indentation) to indicate the arguments to the monitoring tool. + + +## `client_endpoints` + +The client_endpoints section consists of a non-empty list of collections, each associated to a +benchmark entity, and typically indicating the driver for the benchmark. The client_endpoints, if +specified on a test plan, must be cross referenced by the benchmark section, and as such normally the +client_endpoints section precedes the benchmarks section in the test plan. + +See the dir `example/` for a number of test plan examples. diff --git a/docs/Workloads.md b/docs/Workloads.md new file mode 100644 index 00000000..81d1f2b0 --- /dev/null +++ b/docs/Workloads.md @@ -0,0 +1,46 @@ +# Workloads + +A workload is the specification of a sequence of tests to be executed in the order given. +Typically this involves a *range* of values for a specific benchmark argument. The most used is +the *queue depth*. Depending of the benchmark, this can be expressed as a function of the number +of jobs (or threads, or processes), such that the increase number of these causes a proportional +increase in the I/O. Specifiying workloads in this way permits to generate *response latency curves* +from the results. + +The workload feature is currently supported for `librbdfio` only. + +![workloads](./workloads.png) + +* A `workloads` section is composed by a non-empty collection. Each item in the workload has a free name, +and contains in turn a collection of valid options with values for the benchmark. +* For each of the `iodepth` and `numjobs` options, a range of integer values is permitted. + +During execution, any of the given values for the benchmark options in the global section are overwritten +by the given values within the current test workload. The global values are restored once the workload test +completes. + +As an example, the following specifies two workloads: + +* the first is named `precondition` and consists of executing a random write over a queue depth of 4, +(that is, the product of numjobs and iodepth), and indicates that monitoring should be disabled during the +execution of the workload, +* the second is named test1, and specifies a random read over the combinatorial of the provided sequences for +the numjobs and iodepth, resp. That is, (1,1), (1,4), (1,8) .. (8,8). + + +```yaml + +workloads: + precondition: + jobname: 'precond1rw' + mode: 'randwrite' + numjobs: [ 1 ] + iodepth: [ 4 ] + monitor: False # whether to run the monitors along the test + test1: + jobname: 'rr' + mode: 'randread' + numjobs: [ 1, 4, 8 ] + iodepth: [ 1, 4, 8 ] + +``` diff --git a/docs/benchmarks.png b/docs/benchmarks.png new file mode 100644 index 0000000000000000000000000000000000000000..b39686af47f28ec1a5242c5bd0963a8bb3102d2c GIT binary patch literal 21786 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 Xb}O??c84Oe^?FR-L@!&%=IXxzGuXWq literal 0 HcmV?d00001 diff --git a/docs/toplevel.png b/docs/toplevel.png new file mode 100644 index 0000000000000000000000000000000000000000..cc7e2bb919f2f00839619d7900137d94d1fa1cbd GIT binary patch literal 14407 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 SP!_hwS>c(AOpTOT=>Gsn7gCA< literal 0 HcmV?d00001 diff --git a/docs/workloads.png b/docs/workloads.png new file mode 100644 index 0000000000000000000000000000000000000000..43b50c3d50cc96ba7fa6524773b2d45c6d83dd74 GIT binary patch literal 9779 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 literal 0 HcmV?d00001 diff --git a/tools/fio-parse-jsons/fio-parse-jsons.py b/tools/fio-parse-jsons/fio-parse-jsons.py index f21cf9f8..e48faabf 100755 --- a/tools/fio-parse-jsons/fio-parse-jsons.py +++ b/tools/fio-parse-jsons/fio-parse-jsons.py @@ -26,7 +26,7 @@ # crimson4cores_200gb_1img_4k_1procs_randwrite_avg.json # -import glob, os, sys +import os import pprint import json import argparse @@ -39,48 +39,49 @@ # Keys are metric names, vallues are the string path in the .json to seek # For MultiFIO JSON files, the jobname no neccessarily matches any of the predef_dict keys, # so we need instead to use a separate query: -job_type='jobs/jobname=*/job options/rw' +#job_type='jobs/jobname=*/job options/rw' # Better still, use the global options -#job_type='global options/rw' +job_type='global options/rw' # All the following should be within the path # 'jobs/jobname=*/read/iops' predef_dict = { - 'randwrite': { - 'iops' : 'write/iops', - 'total_ios' : 'write/total_ios', - 'clat_ms' : 'write/clat_ns', - 'clat_stdev' : 'write/clat_ns', - 'usr_cpu': 'usr_cpu', - 'sys_cpu': 'sys_cpu' - }, - 'randread': { - 'iops' : 'read/iops', - 'total_ios' : 'read/total_ios', - 'clat_ms' : 'read/clat_ns', - 'clat_stdev' : 'read/clat_ns', - 'usr_cpu': 'usr_cpu', - 'sys_cpu': 'sys_cpu' - }, - 'seqwrite': { - 'bw' : 'write/bw', - 'total_ios' : 'write/total_ios', - 'clat_ms' : 'write/clat_ns', - 'clat_stdev' : 'write/clat_ns', - 'usr_cpu': 'usr_cpu', - 'sys_cpu': 'sys_cpu' - }, - 'seqread': { - 'bw' : 'read/bw', - 'total_ios' : 'read/total_ios', - 'clat_ms' : 'read/clat_ns', - 'clat_stdev' : 'read/clat_ns', - 'usr_cpu': 'usr_cpu', - 'sys_cpu': 'sys_cpu' - } - } + "randwrite": { + "iops": "write/iops", + "total_ios": "write/total_ios", + "clat_ms": "write/clat_ns", + "clat_stdev": "write/clat_ns", + "usr_cpu": "usr_cpu", + "sys_cpu": "sys_cpu", + }, + "randread": { + "iops": "read/iops", + "total_ios": "read/total_ios", + "clat_ms": "read/clat_ns", + "clat_stdev": "read/clat_ns", + "usr_cpu": "usr_cpu", + "sys_cpu": "sys_cpu", + }, + "seqwrite": { + "bw": "write/bw", + "total_ios": "write/total_ios", + "clat_ms": "write/clat_ns", + "clat_stdev": "write/clat_ns", + "usr_cpu": "usr_cpu", + "sys_cpu": "sys_cpu", + }, + "seqread": { + "bw": "read/bw", + "total_ios": "read/total_ios", + "clat_ms": "read/clat_ns", + "clat_stdev": "read/clat_ns", + "usr_cpu": "usr_cpu", + "sys_cpu": "sys_cpu", + }, +} + def filter_json_node(next_branch, jnode_list_in): - """" + """ Traverse the JSON jnode_list_in according to the next_branch: jnode_list_in: [dict] Assumption: json output of non-leaf nodes consists of either @@ -94,14 +95,14 @@ def filter_json_node(next_branch, jnode_list_in): if not next_branch: return next_node_list for n in jnode_list_in: - dotlist = next_branch.split('=') + dotlist = next_branch.split("=") if len(dotlist) > 2: - print( f"unrecognized syntax at {next_branch}") + print(f"unrecognized syntax at {next_branch}") return [] if len(dotlist) == 1: assert isinstance(n, dict) next_node_list.append(n[next_branch]) - else: # must be a sequence, take any element with key matching value + else: # must be a sequence, take any element with key matching value select_key = dotlist[0] select_value = dotlist[1] assert isinstance(n, list) @@ -109,18 +110,19 @@ def filter_json_node(next_branch, jnode_list_in): # n is a list # print 'select with key %s value %s sequence # element %s'%(select_key, select_value, e) - if select_value == '*': + if select_value == "*": next_node_list.append(e) else: v = e[select_key] if v == select_value: next_node_list.append(e) - #print('selecting: %s'%str(e)) + # print('selecting: %s'%str(e)) if len(next_node_list) == 0: print(f"{select_key}={select_value} not found") return [] return next_node_list + def process_fio_item(k, next_node_list): """ Dict of results: @@ -137,27 +139,30 @@ def process_fio_item(k, next_node_list): """ # match k: # Python version on the SV1 node does not support 'match' # case 'iops' | 'usr_cpu' | 'sys_cpu': - if re.search('iops|usr_cpu|sys_cpu|iodepth|total_ios', k): + if re.search("iops|usr_cpu|sys_cpu|iodepth|total_ios", k): return next_node_list[0] - if k == 'bw': - return next_node_list[0]/1000 - if k == 'latency_ms': - # case 'latency_ms': - unsorted_dict=next_node_list[0] - sorted_dict=dict(sorted(unsorted_dict.items(), key=lambda x:x[1], reverse=True)) - firstk=list(sorted_dict.keys())[0] + if k == "bw": + return next_node_list[0] / 1000 + if k == "latency_ms": + # case 'latency_ms': + unsorted_dict = next_node_list[0] + sorted_dict = dict( + sorted(unsorted_dict.items(), key=lambda x: x[1], reverse=True) + ) + firstk = list(sorted_dict.keys())[0] return firstk - if k == 'clat_ms': - # case 'clat_ns': - unsorted_dict=next_node_list[0] - clat_ms=unsorted_dict['mean']/1e6 + if k == "clat_ms": + # case 'clat_ns': + unsorted_dict = next_node_list[0] + clat_ms = unsorted_dict["mean"] / 1e6 return clat_ms - if k == 'clat_stdev': - # case 'clat_ns': - unsorted_dict=next_node_list[0] - clat_stdev=unsorted_dict['stddev']/1e6 + if k == "clat_stdev": + # case 'clat_ns': + unsorted_dict = next_node_list[0] + clat_stdev = unsorted_dict["stddev"] / 1e6 return clat_stdev + def combined_mean(a, b): """ Calculates the combined mean of two groups: @@ -165,48 +170,51 @@ def combined_mean(a, b): FIO already provides the (mean,stdev) of completion latency per sample Expects two tuples: (mx_1, n_1) and (mx_2,n_2), and returns a tuple. """ - mx_1,n_1 = a - mx_2,n_2 = b + mx_1, n_1 = a + mx_2, n_2 = b n_c = n_1 + n_2 - return ( (n_1 * mx_1 + n_2 * mx_2)/n_c, n_c) + return ((n_1 * mx_1 + n_2 * mx_2) / n_c, n_c) + -def combined_std_dev(a,b): +def combined_std_dev(a, b): """ Calculats the combined std dev, normally for the completion latency Expects a,b to be tuples (s_i,x_i) std dev and mean, respectively, and returns a tuple. """ - y_1,n_1 = a - y_2,n_2 = b - s_1,mx_1 = y_1 - s_2,mx_2 = y_2 - mx_c,_nc = combined_mean( (mx_1,n_1), (mx_2,n_2) ) + y_1, n_1 = a + y_2, n_2 = b + s_1, mx_1 = y_1 + s_2, mx_2 = y_2 + mx_c, _nc = combined_mean((mx_1, n_1), (mx_2, n_2)) v_1 = s_1 * s_1 v_2 = s_2 * s_2 q_1 = (n_1 - 1.0) * v_1 + n_1 * (mx_1 * mx_1) q_2 = (n_2 - 1.0) * v_2 + n_2 * (mx_2 * mx_2) q_c = q_1 + q_2 n_c = n_1 + n_2 - return ((math.sqrt( (q_c - n_c * mx_c * mx_c )/(n_c - 1.0) ), mx_c), n_c) + return ((math.sqrt((q_c - n_c * mx_c * mx_c) / (n_c - 1.0)), mx_c), n_c) + def apply_reductor(result_dict, metric): """ Applies the particular reduction to the list of values. Returns a value (scalar numeric) """ - if re.search('iops|usr_cpu|sys_cpu|bw|total_ios', metric): - return functools.reduce( add, result_dict[metric]) - if metric == 'clat_ms': - z = zip(result_dict['clat_ms'], result_dict['total_ios']) - mx,_ = functools.reduce( lambda x,y : combined_mean(x,y), z) + if re.search("iops|usr_cpu|sys_cpu|bw|total_ios", metric): + return functools.reduce(add, result_dict[metric]) + if metric == "clat_ms": + z = zip(result_dict["clat_ms"], result_dict["total_ios"]) + mx, _ = functools.reduce(lambda x, y: combined_mean(x, y), z) return mx - if metric == 'clat_stdev': - z = zip(result_dict['clat_stdev'], result_dict['clat_ms']) - zz = zip(z, result_dict['total_ios']) - zc,_ = functools.reduce( lambda x,y : combined_std_dev(x,y), zz) - sc,_ = zc + if metric == "clat_stdev": + z = zip(result_dict["clat_stdev"], result_dict["clat_ms"]) + zz = zip(z, result_dict["total_ios"]) + zc, _ = functools.reduce(lambda x, y: combined_std_dev(x, y), zz) + sc, _ = zc return sc + def reduce_result_list(result_dict, jobname): """ Applies a reduction to each of the lists of the result_dict: @@ -220,17 +228,18 @@ def reduce_result_list(result_dict, jobname): _res[metric] = apply_reductor(result_dict, metric) return _res + def process_fio_json_file(json_file, json_tree_path): """ Collect metrics from an individual JSON file, which might contain several entries, one per job """ - with open(json_file, 'r') as json_data: + with open(json_file, "r") as json_data: result_dict = {} # check for empty file f_info = os.fstat(json_data.fileno()) if f_info.st_size == 0: - print( f'JSON input file {json_file} is empty') + print(f"JSON input file {json_file} is empty") return result_dict # parse the JSON object node = json.load(json_data) @@ -238,22 +247,23 @@ def process_fio_json_file(json_file, json_tree_path): # different FIO processes result_dict['timestamp'] = str(node['timestamp']) result_dict['iodepth'] = node['global options']['iodepth'] - #result_dict['iodepth'] = node['global options']['rw'] + result_dict['jobname'] = node['global options']['rw'] + result_dict['iodepth'] = node['global options']['iodepth'] # Use the jobname to index the predef_dict for the json query - jobs_list = node['jobs'] + jobs_list = node["jobs"] print(f"Num jobs: {len(jobs_list)}") job_result = {} - for _i,job in enumerate(jobs_list): - jobname = str(job['jobname']) + for _i, job in enumerate(jobs_list): + jobname = str(job["jobname"]) if jobname in predef_dict: # this gives the paths to query for the metrics query_dict = predef_dict[jobname] else: - jobname = job['job options']['rw'] + jobname = job["job options"]["rw"] query_dict = predef_dict[jobname] - result_dict['jobname'] = jobname + #result_dict["jobname"] = jobname for k in query_dict.keys(): - json_tree_path = query_dict[k].split('/') + json_tree_path = query_dict[k].split("/") next_node_list = [job] for next_branch in json_tree_path: @@ -263,10 +273,11 @@ def process_fio_json_file(json_file, json_tree_path): job_result[k] = [] job_result[k].append(item) - reduced = reduce_result_list(job_result, result_dict['jobname']) - merged = { **result_dict, **reduced } + reduced = reduce_result_list(job_result, result_dict["jobname"]) + merged = {**result_dict, **reduced} return merged + def traverse_files(sdir, config, json_tree_path): """ Traverses the JSON files given in the config @@ -283,29 +294,32 @@ def traverse_files(sdir, config, json_tree_path): pp = pprint.PrettyPrinter(width=41, compact=True) dict_new = {} for fname in json_files: - node_list = process_fio_json_file(fname,json_tree_path) + node_list = process_fio_json_file(fname, json_tree_path) dict_new[fname] = node_list print(f"== {fname} ==") pp.pprint(node_list) return dict_new + def gen_plot(config, data, list_subtables, title): """ Generate a gnuplot script and .dat files -- assumes OSD CPU util only """ plot_dict = { - # Use the dict key as the suffix for the output file .png, - # the .dat file is the same for the different charts - 'iops_vs_lat_vs_cpu_sys': { - 'ylabel': "Latency (ms)", - 'ycolumn': '4', - 'y2label': "OSD CPU system", - 'y2column': '9'}, - 'iops_vs_lat_vs_cpu_usr': { - 'ylabel': "Latency (ms)", - 'ycolumn': '4', - 'y2label': "OSD CPU user", - 'y2column': '8'} + # Use the dict key as the suffix for the output file .png, + # the .dat file is the same for the different charts + "iops_vs_lat_vs_cpu_sys": { + "ylabel": "Latency (ms)", + "ycolumn": "4", + "y2label": "OSD CPU system", + "y2column": "9", + }, + "iops_vs_lat_vs_cpu_usr": { + "ylabel": "Latency (ms)", + "ycolumn": "4", + "y2label": "OSD CPU user", + "y2column": "8", + }, } header = """ set terminal pngcairo size 650,420 enhanced font 'Verdana,10' @@ -320,19 +334,19 @@ def gen_plot(config, data, list_subtables, title): """ template = "" - out_plot = config.replace("_list",".plot") - out_data = config.replace("_list",".dat") - # Gnuplot quirk: '_' is interpreted as a sub-index: - _title = title.replace("_","-") + out_plot = config.replace("_list", ".plot") + out_data = config.replace("_list", ".dat") + # Gnuplot quirk: '_' is interpreted as a sub-index: + _title = title.replace("_", "-") - with open(out_plot, 'w') as f: + with open(out_plot, "w") as f: f.write(header) for pk in plot_dict.keys(): out_chart = config.replace("list", pk + ".png") - ylabel = plot_dict[pk]['ylabel'] - ycol = plot_dict[pk]['ycolumn'] - y2label = plot_dict[pk]['y2label'] - y2col = plot_dict[pk]['y2column'] + ylabel = plot_dict[pk]["ylabel"] + ycol = plot_dict[pk]["ycolumn"] + y2label = plot_dict[pk]["y2label"] + y2col = plot_dict[pk]["y2column"] template += f""" set ylabel "{ylabel}" set xlabel "IOPS" @@ -347,21 +361,24 @@ def gen_plot(config, data, list_subtables, title): """ # To plot CPU util in the same response curve, we need the extra axis # This list_subtables indicates how many sub-tables the .datfile will have - # The stdev is the error column:5 + # The stdev is the error column:5 if len(list_subtables) > 0: head = f"plot '{out_data}' index 0 using 2:{ycol}:5 t '{list_subtables[0]} q-depth' w yerr axes x1y1" head += f",\\\n '' index 0 using 2:{ycol}:5 notitle w lp axes x1y1" head += f",\\\n '' index 0 using 2:{y2col} w lp axes x1y2 t 'CPU%'" - tail = ",\\\n".join([ f" '' index {i} using 2:{ycol} t '{list_subtables[i]} q-depth' w lp axes x1y1" - for i in range(1,len(list_subtables))]) + tail = ",\\\n".join([ + f" '' index {i} using 2:{ycol} t '{list_subtables[i]} q-depth' w lp axes x1y1" + for i in range(1, len(list_subtables)) + ]) template += ",\\\n".join([head, tail]) f.write(template) f.close() - with open(out_data, 'w') as f: + with open(out_data, "w") as f: f.write(data) f.close() + def initial_fio_table(dict_files, multi): """ Construct a table from the input mesurements FIO json @@ -372,16 +389,16 @@ def initial_fio_table(dict_files, multi): # Traverse each json sample for name in dict_files.keys(): item = dict_files[name] - jobname = item['jobname'] + jobname = item["jobname"] subdict = predef_dict[jobname] - _keys = [ 'iodepth' ] + [ *subdict.keys() ] + _keys = ["iodepth"] + [*subdict.keys()] # Each k is a FIO metric (iops, lat, etc) for k in _keys: - if not k in table: + if k not in table: table[k] = [] table[k].append(item[k]) if multi: - if not k in table: + if k not in table: avg[k] = 0.0 avg[k] += item[k] # Probably might use some other avg rather than arithmetic avg @@ -392,28 +409,29 @@ def initial_fio_table(dict_files, multi): avg[k] /= len(table[k]) return table, avg + def aggregate_cpu_avg(avg, table, avg_cpu): """ Depending of whether this set of results are from a Multi FIO or a typical response curve, aggregate the OSD CPU avg measurements into the main table """ - # Note: if num_files > len(avg_cpu): this is a MultiFIO + # Note: if num_files > len(avg_cpu): this is a MultiFIO if len(avg_cpu): print(f" avg_cpu list has: {len(avg_cpu)} items") - # The number of CPU items should be the same as the number of dict_files.keys() + # The number of CPU items should be the same as the number of dict_files.keys() for cpu_item in avg_cpu: - for k in cpu_item.keys(): # 'sys', 'us' normally from the OSD + for k in cpu_item.keys(): # 'sys', 'us' normally from the OSD cpu_avg_k = 0 samples = cpu_item[k] for cpu in samples.keys(): cpu_avg_k += samples[cpu] cpu_avg_k /= len(samples.keys()) - # Aggregate the CPU values in the avg table - if not k in avg: + # Aggregate the CPU values in the avg table + if k not in avg: avg[k] = 0 avg[k] += cpu_avg_k - # Aggregate the CPU values in the FIO table - if not k in table: + # Aggregate the CPU values in the FIO table + if k not in table: table[k] = [] table[k].append(cpu_avg_k) @@ -421,6 +439,7 @@ def aggregate_cpu_avg(avg, table, avg_cpu): print("Table (after aggregating OSD CPU avg data):") pp.pprint(table) + def gen_table(dict_files, config, title, avg_cpu, multi=False): """ Construct a table from the predefined keys, sorted according to the @@ -453,17 +472,21 @@ def gen_table(dict_files, config, title, avg_cpu, multi=False): # For the gnuplot .dat, each subtable ranges over num_jobs(threads) # whereas each row within a table ranges over iodepth gplot_hdr = "# " - gplot_hdr += ' '.join(table.keys()) + gplot_hdr += " ".join(table.keys()) gplot_hdr += "\n" gplot = "" - wiki = r"""{| class="wikitable" + wiki = ( + r"""{| class="wikitable" |- -! colspan="7" | """ + config.replace("_list","") + """ +! colspan="7" | """ + + config.replace("_list", "") + + """ ! colspan="2" | OSD CPU% |- ! """ - wiki += ' !! '.join(table.keys()) + ) + wiki += " !! ".join(table.keys()) wiki += "\n|-\n" for k in table.keys(): @@ -473,13 +496,13 @@ def gen_table(dict_files, config, title, avg_cpu, multi=False): # our naming convention to identify this: # fio_crimson_1osd_default_8img_fio_unrest_2job_16io_4k_randread_p5.json for name in dict_files.keys(): - m = re.match(r".*(?P\d+)job_(?P\d+)io_",name) + m = re.match(r".*(?P\d+)job_(?P\d+)io_", name) if m: # Note: 'job' (num threads) is constant within each table, # each row corresponds to increasing the iodepth, that is, each # sample run - job = int(m.group('job')) # m.group(1) - _io = int(m.group('io')) # m.group(2) + job = int(m.group("job")) # m.group(1) + _io = int(m.group("io")) # m.group(2) if job not in list_subtables: # Add a gnuplot table break (ie new block) if len(list_subtables) > 0: @@ -490,32 +513,33 @@ def gen_table(dict_files, config, title, avg_cpu, multi=False): for k in table.keys(): item = next(table_iters[k]) - if k == 'iodepth': #This metric is the first column + if k == "iodepth": # This metric is the first column gplot += f" {item} " - wiki += f' | {item} ' + wiki += f" | {item} " else: gplot += f" {item:.2f} " - wiki += f' || {item:.2f} ' + wiki += f" || {item:.2f} " gplot += "\n" wiki += "\n|-\n" if multi: - wiki += '! Avg:' + wiki += "! Avg:" for k in avg.keys(): - wiki += f' || {avg[k]:.2f} ' + wiki += f" || {avg[k]:.2f} " wiki += "\n|-\n" - if 'iops' in avg.keys(): - total = avg['iops'] * num_files + if "iops" in avg.keys(): + total = avg["iops"] * num_files else: - total = avg['bw'] * num_files - wiki += f'! Total: || {total:.2f} ' + total = avg["bw"] * num_files + wiki += f"! Total: || {total:.2f} " wiki += "\n|-\n" - # format_numeric = lambda num: f"{num:e}" if isinstance(num, int) else f"{num:,.2f}" + # format_numeric = lambda num: f"{num:e}" if isinstance(num, int) else f"{num:,.2f}" wiki += "|}\n" print(f" Wiki table: {title}") print(wiki) gen_plot(config, gplot, list_subtables, title) - print('Done') + print("Done") + def main(directory, config, json_query): """ @@ -523,13 +547,14 @@ def main(directory, config, json_query): evolved """ if not bool(json_query): - json_query='jobs/jobname=*' - json_tree_path = json_query.split('/') + json_query = "jobs/jobname=*" + json_tree_path = json_query.split("/") dicto_files = traverse_files(directory, config, json_tree_path) - print('Note: clat_ns has been converted to milliseconds') - print('Note: bw has been converted to MiBs') + print("Note: clat_ns has been converted to milliseconds") + print("Note: bw has been converted to MiBs") return dicto_files + def load_avg_cpu_json(json_fname): """ Load a .json file containing the CPU avg samples -- normally produced by the script @@ -541,7 +566,7 @@ def load_avg_cpu_json(json_fname): # check for empty file f_info = os.fstat(json_data.fileno()) if f_info.st_size == 0: - print(f'JSON input file {json_fname} is empty') + print(f"JSON input file {json_fname} is empty") return cpu_avg_list # parse the JSON: list of dicts with keys 'sys' and 'us' cpu_avg_list = json.load(json_data) @@ -549,37 +574,61 @@ def load_avg_cpu_json(json_fname): except IOError as e: raise argparse.ArgumentTypeError(str(e)) + def parse_args(): """ As it says on the tin """ - parser = argparse.ArgumentParser(description='Parse set of output json FIO results.') + parser = argparse.ArgumentParser( + description="Parse set of output json FIO results." + ) parser.add_argument( - "-c", "--config", type=str, - required=True, - help="Name of the file with the list of JSON files names to examine", default="") + "-c", + "--config", + type=str, + required=True, + help="Name of the file with the list of JSON files names to examine", + default="", + ) parser.add_argument( - "-t", "--title", type=str, - required=True, - help="Title for the response curve gnuplot chart", default="") + "-t", + "--title", + type=str, + required=True, + help="Title for the response curve gnuplot chart", + default="", + ) parser.add_argument( - "-a", "--average", type=str, - help="Name of the JSON file with the CPU avg", default="") + "-a", + "--average", + type=str, + help="Name of the JSON file with the CPU avg", + default="", + ) parser.add_argument( - "-d", "--directory", type=str, - help="result directory to evaluate", default="./") + "-d", "--directory", type=str, help="result directory to evaluate", default="./" + ) parser.add_argument( - "-q", "--query", type=str, - required=False, - help="JSON query", default="jobs/jobname=*") + "-q", + "--query", + type=str, + required=False, + help="JSON query", + default="jobs/jobname=*", + ) parser.add_argument( - '-m', '--multi', action='store_true', - required=False, - help="Indicate multiple FIO instance as opposed to response curves", default=False) + "-m", + "--multi", + action="store_true", + required=False, + help="Indicate multiple FIO instance as opposed to response curves", + default=False, + ) args = parser.parse_args() return args -if __name__=='__main__': + +if __name__ == "__main__": args = parse_args() dict_files = main(args.directory, args.config, args.query) avg_cpu = load_avg_cpu_json(args.average)