Skip to content

Commit face387

Browse files
committed
[add] kicked off the redisbench-admin compare <....> tool
1 parent c2b53fe commit face387

File tree

12 files changed

+394
-227
lines changed

12 files changed

+394
-227
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "redisbench-admin"
3-
version = "0.1.4"
3+
version = "0.1.5"
44
description = "Redis benchmark run helper. A wrapper around ftsb_redisearch ( future versions will also support redis-benchmark and memtier_benchmark )."
55
authors = ["filipecosta90 <[email protected]>"]
66

redisbench_admin/cli.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33

44
import toml
55

6-
from redisbench_admin.compare.compare import create_compare_arguments, compare_command_logic
7-
from redisbench_admin.export.export import create_export_arguments, export_command_logic
8-
from redisbench_admin.run.run import create_run_arguments, run_command_logic
6+
from redisbench_admin.compare.args import create_compare_arguments
7+
from redisbench_admin.compare.compare import compare_command_logic
8+
from redisbench_admin.export.args import create_export_arguments
9+
from redisbench_admin.export.export import export_command_logic
10+
from redisbench_admin.run.args import create_run_arguments
11+
from redisbench_admin.run.run import run_command_logic
912

1013

1114
def populate_with_poetry_data():
@@ -27,7 +30,10 @@ def main():
2730
parser = argparse.ArgumentParser(
2831
description=project_description,
2932
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
33+
# common arguments to all tools
3034
parser.add_argument('--version', default=False, action='store_true', help='print version and exit')
35+
parser.add_argument('--local-dir', type=str, default="./", help='local dir to use as storage')
36+
3137
if requested_tool == "run":
3238
parser = create_run_arguments(parser)
3339
elif requested_tool == "compare":
@@ -56,13 +62,10 @@ def main():
5662

5763
argv = sys.argv[2:]
5864
args = parser.parse_args(args=argv)
59-
if args.version:
60-
print("{project_name} {project_version}".format(project_name=project_name, project_version=project_version))
61-
sys.exit(0)
6265

6366
if requested_tool == "run":
6467
run_command_logic(args)
6568
if requested_tool == "compare":
66-
export_command_logic(args)
67-
if requested_tool == "export":
6869
compare_command_logic(args)
70+
if requested_tool == "export":
71+
export_command_logic(args)

redisbench_admin/compare/args.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
def create_compare_arguments(parser):
2+
parser.add_argument('--baseline-file', type=str, required=True,
3+
help="baseline benchmark output file to read results from. can be a local file or a remote link.")
4+
parser.add_argument('--comparison-file', type=str, required=True,
5+
help="comparison benchmark output file to read results from. can be a local file or a remote link.")
6+
parser.add_argument('--use-result', type=str, default="median-result",
7+
help="for each key-metric, use either worst-result, best-result, or median-result")
8+
parser.add_argument('--steps', type=str, default="setup,benchmark",
9+
help="comma separated list of steps to be analyzed given the benchmark result files")
10+
parser.add_argument('--enable-fail-above', default=False, action='store_true',
11+
help="enables failing test if percentage of change is above threshold on any of the benchmark steps being analysed")
12+
parser.add_argument('--fail-above-pct-change', type=float, default=10.0,
13+
help='Fail above if any of the key-metrics presents an regression in percentage of change (from 0.0-100.0)')
14+
return parser
Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,139 @@
1+
import json
2+
import os
3+
import sys
4+
5+
import pandas as pd
6+
7+
from redisbench_admin.utils.utils import retrieve_local_or_remote_input_json
8+
9+
10+
def get_key_results_and_values(baseline_json, step, use_result):
11+
selected_run = None
12+
metrics = {}
13+
for name, value in baseline_json["key-results"][step][use_result][0].items():
14+
if name == "run-name":
15+
selected_run = value
16+
else:
17+
metrics[name] = value
18+
return selected_run, metrics
19+
20+
121
def compare_command_logic(args):
2-
pass
22+
baseline_file = args.baseline_file
23+
comparison_file = args.comparison_file
24+
local_path = os.path.abspath(args.local_dir)
25+
use_result = args.use_result
26+
included_steps = args.steps.split(",")
27+
max_pct_change = args.fail_above_pct_change
28+
max_negative_pct_change = max_pct_change * -1.0
29+
enabled_fail = args.enable_fail_above
30+
31+
baseline_json = retrieve_local_or_remote_input_json(baseline_file, local_path, "--baseline-file")
32+
if baseline_json is None:
33+
print('Error while retrieving {}! Exiting..'.format(baseline_file))
34+
sys.exit(1)
35+
36+
comparison_json = retrieve_local_or_remote_input_json(comparison_file, local_path, "--comparison-file")
37+
if comparison_json is None:
38+
print('Error while retrieving {}! Exiting..'.format(comparison_file))
39+
sys.exit(1)
40+
41+
##### Comparison starts here #####
42+
baseline_key_results_steps = baseline_json["key-results"].keys()
43+
comparison_key_results_steps = comparison_json["key-results"].keys()
44+
baseline_df_config = generate_comparison_dataframe_configs(baseline_json["benchmark-config"],
45+
baseline_key_results_steps)
46+
comparison_df_config = generate_comparison_dataframe_configs(comparison_json["benchmark-config"],
47+
comparison_key_results_steps)
48+
49+
percentange_change_map = {}
50+
for step in baseline_key_results_steps:
51+
if step in included_steps:
52+
df_dict = {}
53+
percentange_change_map[step] = {}
54+
print("##############################")
55+
print("Comparing {} step".format(step))
56+
key_result_run_name, baseline_metrics = get_key_results_and_values(baseline_json, step, use_result)
57+
key_result_run_name, comparison_metrics = get_key_results_and_values(comparison_json, step, use_result)
58+
for baseline_metric_name, baseline_metric_value in baseline_metrics.items():
59+
comparison_metric_value = None
60+
if baseline_metric_name in comparison_metrics:
61+
comparison_metric_value = comparison_metrics[baseline_metric_name]
62+
df_dict[baseline_metric_name] = [baseline_metric_value, comparison_metric_value]
63+
df = pd.DataFrame(df_dict, index=["baseline", "comparison"])
64+
print("Percentage of change for comparison on {}".format(step))
65+
df = df.append(df.pct_change().rename(index={'comparison': 'pct_change'}).loc['pct_change'] * 100.0)
66+
67+
for metric_name, items in df.iteritems():
68+
69+
lower_is_better = baseline_df_config[step]["sorting_metric_sorting_direction_map"][metric_name]
70+
71+
multiplier = 1.0
72+
# if lower is better than negative changes are and performance improvement
73+
if lower_is_better:
74+
multiplier = -1.0
75+
76+
pct_change = items.get("pct_change") * multiplier
77+
df.at['pct_change', metric_name] = pct_change
78+
percentange_change_map[step][metric_name] = pct_change
79+
80+
print(df)
81+
if enabled_fail:
82+
failing_metrics_serie = df.loc['pct_change'] <= max_negative_pct_change
83+
failing_metrics = df.loc['pct_change'][failing_metrics_serie]
84+
ammount_of_failing_metrics = len (failing_metrics)
85+
if ammount_of_failing_metrics > 0:
86+
df_keys = df.keys()
87+
# print(df.loc['pct_change'][0])
88+
# print([0])
89+
print( "There was a total of {} metrics that presented a regression above {} %".format(ammount_of_failing_metrics,max_pct_change) )
90+
for pos,failed in enumerate(failing_metrics_serie):
91+
if failed:
92+
print("\tMetric '{}' failed. with an percentage of change of {:.2f} %".format(df_keys[pos],df.loc['pct_change'][pos]))
93+
sys.exit(1)
94+
else:
95+
print("Skipping step: {} due to command line argument --steps not containing it ({})".format(step, ",".join(
96+
included_steps)))
97+
98+
99+
def generate_comparison_dataframe_configs(benchmark_config, steps):
100+
step_df_dict = {}
101+
for step in steps:
102+
step_df_dict[step] = {}
103+
step_df_dict[step]["df_dict"] = {"run-name": []}
104+
step_df_dict[step]["sorting_metric_names"] = []
105+
step_df_dict[step]["sorting_metric_sorting_direction"] = []
106+
step_df_dict[step]["sorting_metric_sorting_direction_map"] = {}
107+
step_df_dict[step]["metric_json_path"] = []
108+
for metric in benchmark_config["key-metrics"]:
109+
step = metric["step"]
110+
metric_name = metric["metric-name"]
111+
metric_json_path = metric["metric-json-path"]
112+
step_df_dict[step]["sorting_metric_names"].append(metric_name)
113+
step_df_dict[step]["metric_json_path"].append(metric_json_path)
114+
step_df_dict[step]["df_dict"][metric_name] = []
115+
step_df_dict[step]["sorting_metric_sorting_direction"].append(
116+
False if metric["comparison"] == "higher-better" else True)
117+
step_df_dict[step]["sorting_metric_sorting_direction_map"][metric_name] = False if metric[
118+
"comparison"] == "higher-better" else True
119+
return step_df_dict
3120

4121

5-
def create_compare_arguments(parser):
6-
return parser
122+
def from_resultsDF_to_key_results_dict(resultsDataFrame, step, step_df_dict):
123+
key_results_dict = {}
124+
key_results_dict["table"] = json.loads(resultsDataFrame.to_json(orient='records'))
125+
best_result = resultsDataFrame.head(n=1)
126+
worst_result = resultsDataFrame.tail(n=1)
127+
first_sorting_col = step_df_dict[step]["sorting_metric_names"][0]
128+
first_sorting_median = resultsDataFrame[first_sorting_col].median()
129+
result_index = resultsDataFrame[first_sorting_col].sub(first_sorting_median).abs().idxmin()
130+
median_result = resultsDataFrame.loc[[result_index]]
131+
key_results_dict["best-result"] = json.loads(best_result.to_json(orient='records'))
132+
key_results_dict["median-result"] = json.loads(
133+
median_result.to_json(orient='records'))
134+
key_results_dict["worst-result"] = json.loads(worst_result.to_json(orient='records'))
135+
key_results_dict["reliability-analysis"] = {
136+
'var': json.loads(resultsDataFrame.var().to_json()),
137+
'stddev': json.loads(
138+
resultsDataFrame.std().to_json())}
139+
return key_results_dict

redisbench_admin/export/args.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def create_export_arguments(parser):
2+
return parser

redisbench_admin/export/export.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
11
def export_command_logic(args):
22
pass
3-
4-
5-
def create_export_arguments(parser):
6-
return parser

redisbench_admin/run/args.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
def create_run_arguments(parser):
2+
parser.add_argument('--benchmark-config-file', type=str, required=True,
3+
help="benchmark config file to read instructions from. can be a local file or a remote link")
4+
parser.add_argument('--workers', type=str, default=0,
5+
help='number of workers to use during the benchark. If set to 0 it will auto adjust based on the machine number of VCPUs')
6+
parser.add_argument('--repetitions', type=int, default=1,
7+
help='number of repetitions to run')
8+
parser.add_argument('--benchmark-requests', type=int, default=0,
9+
help='Number of total requests to issue (0 = all of the present in input file)')
10+
parser.add_argument('--upload-results-s3', default=False, action='store_true',
11+
help="uploads the result files and configuration file to public benchmarks.redislabs bucket. Proper credentials are required")
12+
parser.add_argument('--redis-url', type=str, default="redis://localhost:6379", help='The url for Redis connection')
13+
parser.add_argument('--deployment-type', type=str, default="docker-oss",
14+
help='one of docker-oss,docker-oss-cluster,docker-enterprise,oss,oss-cluster,enterprise')
15+
parser.add_argument('--deployment-shards', type=int, default=1,
16+
help='number of database shards used in the deployment')
17+
parser.add_argument('--output-file-prefix', type=str, default="", help='prefix to quickly tag some files')
18+
return parser

redisbench_admin/run/ftsb_redisearch/__init__.py

Whitespace-only changes.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
import os
3+
import subprocess
4+
import sys
5+
6+
7+
def get_run_options():
8+
environ = os.environ.copy()
9+
stdoutPipe = subprocess.PIPE
10+
stderrPipe = subprocess.STDOUT
11+
stdinPipe = subprocess.PIPE
12+
options = {
13+
'stderr': stderrPipe,
14+
'env': environ,
15+
}
16+
return options
17+
18+
19+
def run_ftsb_redisearch(redis_url, ftsb_redisearch_path, setup_run_json_output_fullpath, options, input_file,
20+
workers):
21+
##################
22+
# Setup commands #
23+
##################
24+
output_json = None
25+
ftsb_args = []
26+
ftsb_args += [ftsb_redisearch_path, "--host={}".format(redis_url),
27+
"--input={}".format(input_file), "--workers={}".format(workers),
28+
"--json-out-file={}".format(setup_run_json_output_fullpath)]
29+
ftsb_process = subprocess.Popen(args=ftsb_args, **options)
30+
if ftsb_process.poll() is not None:
31+
print('Error while issuing setup commands. FTSB process is not alive. Exiting..')
32+
sys.exit(1)
33+
output = ftsb_process.communicate()
34+
if ftsb_process.returncode != 0:
35+
print('FTSB process returned non-zero exit status {}. Exiting..'.format(ftsb_process.returncode))
36+
print('catched output:\n\t{}'.format(output))
37+
sys.exit(1)
38+
with open(setup_run_json_output_fullpath) as json_result:
39+
output_json = json.load(json_result)
40+
return output_json

0 commit comments

Comments
 (0)