diff --git a/tests/macro_benchmark/harness/harness.py b/tests/macro_benchmark/harness/harness.py index c7db01301..40376a807 100755 --- a/tests/macro_benchmark/harness/harness.py +++ b/tests/macro_benchmark/harness/harness.py @@ -39,9 +39,11 @@ class _QuerySuite: # what the QuerySuite can work with KNOWN_KEYS = {"config", "setup", "itersetup", "run", "iterteardown", "teardown", "common"} - summary = "Macro benchmark summary:\n" \ - "{:>27}{:>27}{:>27}{:>27}{:>27}{:>27}\n".format( - "scenario_name", "query_parsing_time", + FORMAT = ["{:>24}", "{:>28}", "{:>22}", "{:>24}", "{:>28}", + "{:>16}", "{:>16}"] + FULL_FORMAT = "".join(FORMAT) + "\n" + summary = FULL_FORMAT.format( + "group_name", "scenario_name", "query_parsing_time", "query_planning_time", "query_plan_execution_time", WALL_TIME, CPU_TIME) @@ -186,7 +188,7 @@ class _QuerySuite: return group_scenarios - def run(self, scenario, scenario_name, runner): + def run(self, scenario, group_name, scenario_name, runner): log.debug("QuerySuite.run() with scenario: %s", scenario) scenario_config = scenario.get("config") scenario_config = next(scenario_config()) if scenario_config else {} @@ -252,20 +254,21 @@ class _QuerySuite: # TODO value outlier detection and warning across iterations execute("teardown") runner.stop() - self.append_scenario_summary(scenario_name, measurement_sums, - num_iterations) + self.append_scenario_summary(group_name, scenario_name, + measurement_sums, num_iterations) return measurements - def append_scenario_summary(self, scenario_name, measurement_sums, - num_iterations): - self.summary += "{:>27}".format(scenario_name) - for key in ("query_parsing_time", "query_planning_time", - "query_plan_execution_time", WALL_TIME, CPU_TIME): + def append_scenario_summary(self, group_name, scenario_name, + measurement_sums, num_iterations): + self.summary += self.FORMAT[0].format(group_name) + self.summary += self.FORMAT[1].format(scenario_name) + for i, key in enumerate(("query_parsing_time", "query_planning_time", + "query_plan_execution_time", WALL_TIME, CPU_TIME)): if key not in measurement_sums: time = "-" else: time = "{:.10f}".format(measurement_sums[key] / num_iterations) - self.summary += "{:>27}".format(time) + self.summary += self.FORMAT[i + 2].format(time) self.summary += "\n" def runners(self): @@ -523,7 +526,7 @@ def main(): for (group, scenario_name), scenario in filtered_scenarios.items(): log.info("Executing group.scenario '%s.%s' with elements %s", group, scenario_name, list(scenario.keys())) - for iter_result in suite.run(scenario, scenario_name, runner): + for iter_result in suite.run(scenario, group, scenario_name, runner): iter_result["group"] = group iter_result["scenario"] = scenario_name results.append(iter_result) @@ -534,7 +537,8 @@ def main(): run.update(args.additional_run_fields) for result in results: jail.store_data(result) - print("\n\n{}\n".format(suite.summary)) + print("\n\nMacro benchmark summary:") + print("{}\n".format(suite.summary)) with open(os.path.join(DIR_PATH, ".harness_summary"), "w") as f: print(suite.summary, file=f) diff --git a/tests/qa/continuous_integration b/tests/qa/continuous_integration index 37d3dc2c3..775840231 100755 --- a/tests/qa/continuous_integration +++ b/tests/qa/continuous_integration @@ -8,7 +8,7 @@ everything which is needed for the CI environment. List of responsibilities: * execute default suites * terminate execution if any of internal scenarios fails - * creates the report file that is needed by the Jenkins plugin + * creates the report file that is needed by the Apollo plugin to post the status on Phabricator. (.quality_assurance_status) """ @@ -43,18 +43,46 @@ def get_newest_path(folder, suffix): return os.path.join(folder, name_list.pop()) -def generate_status(suite, f): +def generate_status(suite, f, required = False): """ :param suite: Test suite name. :param f: Json file with status report. + :param required: Adds status ticks to the message if required. :return: Status string. """ result = json.load(f) total = result["total"] passed = result["passed"] - return ("SUITE: %s, PASSED SCENARIOS: %s, TOTAL SCENARIOS: %s (%.2f%%)" % - (suite, passed, total, 100.0 * passed / total)), passed, total + ratio = passed / total + msg = "{} / {} //({:.2%})//".format(passed, total, ratio) + if required: + if passed == total: + msg += " {icon check color=green}" + else: + msg += " {icon times color=red}" + return (msg, passed, total) + + +def generate_remarkup(data): + """ + :param data: Tabular data to convert to remarkup. + + :return: Remarkup formatted status string. + """ + ret = "==== Quality assurance status: ====\n\n" + ret += "<table>\n" + for row in data: + ret += " <tr>\n" + for item in row: + if row == data[0]: + fmt = " <th>{}</th>\n" + else: + fmt = " <td>{}</td>\n" + ret += fmt.format(item) + ret += " </tr>\n" + ret += "</table>\n" + return ret if __name__ == "__main__": @@ -73,13 +101,14 @@ if __name__ == "__main__": suite_suffix.format(memgraph_suite)) log.info("Memgraph result path is {}".format(memgraph_result_path)) + # status table headers + status_data = [["Suite", "Scenarios"]] + # read internal scenarios with open(memgraph_result_path) as f: memgraph_status, memgraph_passed, memgraph_total \ - = generate_status(memgraph_suite, f) - - # create status message - qa_status_message = "Quality Assurance Status\n" + memgraph_status + "\n" + = generate_status(memgraph_suite, f, required = True) + status_data.append([memgraph_suite, memgraph_status]) # read extra scenarios for suite in extra_suites: @@ -87,7 +116,10 @@ if __name__ == "__main__": log.info("Extra suite '{}' result path is {}".format(suite, result_path)) with open(result_path) as f: suite_status, _, _ = generate_status(suite, f) - qa_status_message += suite_status + "\n" + status_data.append([suite, suite_status]) + + # create status message + qa_status_message = generate_remarkup(status_data) # create the report file with open(qa_status_path, "w") as f: diff --git a/tools/apollo/build_diff b/tools/apollo/build_diff index 2ec14464a..26897d01d 100644 --- a/tools/apollo/build_diff +++ b/tools/apollo/build_diff @@ -3,7 +3,11 @@ # From the manpage: "If the -j option is given without an argument, make will not limit the number of jobs that can run simultaneously." # That means that the whole build will be started simultaneously and IT WILL CRASH YOUR COMPUTER! -cd ../.. +cd ../../.. + +cp -r memgraph parent + +cd memgraph TIMEOUT=600 ./init bash -c "doxygen Doxyfile >/dev/null 2>/dev/null" @@ -14,11 +18,20 @@ TIMEOUT=1000 make -j$THREADS cd .. mkdir build_release -cd build_release +cd build_release cmake -DCMAKE_BUILD_TYPE=release .. TIMEOUT=1000 make -j$THREADS memgraph_link_target -cd ../tools/apollo +cd ../../parent + +git checkout HEAD~1 +TIMEOUT=600 ./init + +cd build +cmake -DCMAKE_BUILD_TYPE=release .. +TIMEOUT=1000 make -j$THREADS memgraph_link_target + +cd ../../memgraph/tools/apollo ./generate diff diff --git a/tools/apollo/cppcheck b/tools/apollo/cppcheck index 4f504bb13..d2baf7982 100755 --- a/tools/apollo/cppcheck +++ b/tools/apollo/cppcheck @@ -24,5 +24,5 @@ cat "$errfile" >&2 len="$( cat "$errfile" | wc -l )" if [ $len -gt 0 ]; then - echo -e "Cppcheck errors:\n$( cat "$errfile" )" > "$errfile" + echo -e "==== Cppcheck errors: ====\n\n\`\`\`\n$( cat "$errfile" )\n\`\`\`" > "$errfile" fi diff --git a/tools/apollo/generate b/tools/apollo/generate index 05c89ff1c..914fea54a 100755 --- a/tools/apollo/generate +++ b/tools/apollo/generate @@ -35,6 +35,7 @@ OUTPUT_DIR = os.path.join(BUILD_DIR, "apollo") # output lists ARCHIVES = [] RUNS = [] +DATA_PROCESS = [] # generation mode if len(sys.argv) >= 2: @@ -47,6 +48,10 @@ def run_cmd(cmd, cwd): ret = subprocess.run(cmd, cwd = cwd, stdout = subprocess.PIPE, check = True) return ret.stdout.decode("utf-8") +def find_memgraph_binary(loc): + return run_cmd(["find", ".", "-maxdepth", "1", "-executable", "-type", + "f", "-name", "memgraph*"], loc).split("\n")[0][2:] + def generate_run(name, typ = "run", supervisor = "", commands = "", arguments = "", enable_network = False, outfile_paths = "", infile = ""): @@ -77,12 +82,9 @@ if os.path.exists(OUTPUT_DIR): os.makedirs(OUTPUT_DIR) # store memgraph binary to archive -binary_name = run_cmd(["find", ".", "-maxdepth", "1", "-executable", "-type", - "f", "-name", "memgraph*"], BUILD_DIR).split("\n")[0][2:] -binary_link_name = run_cmd(["find", ".", "-maxdepth", "1", "-executable", "-type", - "l", "-name", "memgraph*"], BUILD_DIR).split("\n")[0][2:] +binary_name = find_memgraph_binary(BUILD_DIR) binary_path = os.path.join(BUILD_DIR, binary_name) -binary_link_path = os.path.join(BUILD_DIR, binary_link_name) +binary_link_path = os.path.join(BUILD_DIR, "memgraph") config_path = os.path.join(BASE_DIR, "config") config_copy_path = os.path.join(BUILD_DIR, "config") if os.path.exists(config_copy_path): @@ -183,24 +185,55 @@ if mode == "release": BUILD_RELEASE_DIR = os.path.join(BASE_DIR, "build") else: BUILD_RELEASE_DIR = os.path.join(BASE_DIR, "build_release") -binary_release_name = run_cmd(["find", ".", "-maxdepth", "1", "-executable", - "-type", "f", "-name", "memgraph*"], BUILD_RELEASE_DIR).split("\n")[0][2:] +binary_release_name = find_memgraph_binary(BUILD_RELEASE_DIR) binary_release_path = os.path.join(BUILD_RELEASE_DIR, binary_release_name) binary_release_link_path = os.path.join(BUILD_RELEASE_DIR, "memgraph") # macro benchmark tests +MACRO_BENCHMARK_ARGS = "QuerySuite MemgraphRunner --groups aggregation" macro_bench_path = os.path.join(BASE_DIR, "tests", "macro_benchmark") stress_common = os.path.join(BASE_DIR, "tests", "stress", "common.py") infile = create_archive("macro_benchmark", [binary_release_path, macro_bench_path, stress_common, config_path], cwd = WORKSPACE_DIR) supervisor = "./{}/tests/macro_benchmark/harness/harness.py".format(BASE_DIR_NAME) -args = "QuerySuite MemgraphRunner --groups aggregation --RunnerBin " + binary_release_path +args = MACRO_BENCHMARK_ARGS + " --RunnerBin " + binary_release_path outfile_paths = "\./{}/tests/macro_benchmark/harness/\.harness_summary".format( BASE_DIR_NAME) RUNS.append(generate_run("macro_benchmark", supervisor = supervisor, arguments = args, infile = infile, outfile_paths = outfile_paths)) +# macro benchmark parent tests +if mode == "diff": + PARENT_DIR = os.path.join(WORKSPACE_DIR, "parent") + BUILD_PARENT_DIR = os.path.join(PARENT_DIR, "build") + binary_parent_name = find_memgraph_binary(BUILD_PARENT_DIR) + binary_parent_path = os.path.join(BUILD_PARENT_DIR, binary_parent_name) + parent_config_path = os.path.join(PARENT_DIR, "config") + parent_macro_bench_path = os.path.join(PARENT_DIR, "tests", "macro_benchmark") + parent_stress_common = os.path.join(PARENT_DIR, "tests", "stress", "common.py") + infile = create_archive("macro_benchmark_parent", [binary_parent_path, + parent_macro_bench_path, parent_stress_common, parent_config_path], + cwd = WORKSPACE_DIR) + supervisor = "./parent/tests/macro_benchmark/harness/harness.py" + args = MACRO_BENCHMARK_ARGS + " --RunnerBin " + binary_parent_path + outfile_paths = "\./parent/tests/macro_benchmark/harness/\.harness_summary" + RUNS.append(generate_run("macro_benchmark_parent", supervisor = supervisor, + arguments = args, infile = infile, outfile_paths = outfile_paths)) + + # macro benchmark comparison data process + script_path = os.path.join(BASE_DIR, "tools", "apollo", + "macro_benchmark_summary") + infile = create_archive("macro_benchmark_summary", [script_path], + cwd = WORKSPACE_DIR) + cmd = "./memgraph/tools/apollo/macro_benchmark_summary " \ + "macro_benchmark/memgraph/tests/macro_benchmark/harness/.harness_summary " \ + "macro_benchmark_parent/parent/tests/macro_benchmark/harness/.harness_summary " \ + ".harness_summary" + outfile_paths = "\./.harness_summary" + DATA_PROCESS.append(generate_run("macro_benchmark_summary", typ = "data process", + commands = cmd, infile = infile, outfile_paths = outfile_paths)) + # stress tests stress_path = os.path.join(BASE_DIR, "tests", "stress") infile = create_archive("stress", [binary_release_path, @@ -212,4 +245,4 @@ RUNS.append(generate_run("stress", commands = cmd, infile = infile)) # store ARCHIVES and RUNS store_metadata(OUTPUT_DIR, "archives", ARCHIVES) -store_metadata(OUTPUT_DIR, "runs", RUNS) +store_metadata(OUTPUT_DIR, "runs", RUNS + DATA_PROCESS) diff --git a/tools/apollo/macro_benchmark_summary b/tools/apollo/macro_benchmark_summary new file mode 100755 index 000000000..2a396f53b --- /dev/null +++ b/tools/apollo/macro_benchmark_summary @@ -0,0 +1,111 @@ +#!/usr/bin/python3 +import os +import sys + +def convert2float(val): + try: + return float(val) + except: + return val + +def parse_file(fname): + with open(fname) as f: + data = f.readlines() + ret = [] + for row in data: + row = row.strip() + if row == "": continue + ret.append(list(map(convert2float, row.split()))) + return ret + +def strip_integers(row): + return list(filter(lambda x: type(x) == str, row)) + +def find_item(data, header, row): + headers = data[0] + row = strip_integers(row) + pos_x = -1 + for i in range(len(data)): + s = strip_integers(data[i]) + if s != row: continue + pos_x = i + break + if pos_x == -1: return None + pos_y = -1 + for j in range(len(headers)): + if headers[j] != header: continue + pos_y = j + break + if pos_y == -1: return None + return data[pos_x][pos_y] + +def compare_values(data_cur, data_prev): + ret = [] + headers = data_cur[0] + for i in range(len(data_cur)): + ret.append([]) + row_cur = data_cur[i] + for j in range(len(row_cur)): + item_cur = row_cur[j] + if type(item_cur) == str: + item = " ".join(item_cur.split("_")).capitalize() + else: + item_prev = find_item(data_prev, headers[j], row_cur) + if item_prev != None: + if item_prev != 0.0: + diff = (item_cur - item_prev) / item_prev + else: + diff = 0.0 + if diff < -0.05: + sign = " {icon arrow-down color=green}" + elif diff > 0.05: + sign = " {icon arrow-up color=red}" + else: + sign = "" + item = "{:.9f} //({:+.2%})//{}".format(item_cur, diff, sign) + else: + item = "{:.9f} //(new)// {{icon plus color=blue}}".format(item_cur) + ret[i].append(item) + return ret + +def generate_remarkup(data): + ret = "==== Macro benchmark summary: ====\n\n" + ret += "<table>\n" + for row in data: + ret += " <tr>\n" + for item in row: + if row == data[0]: + fmt = " <th>{}</th>\n" + else: + fmt = " <td>{}</td>\n" + ret += fmt.format(item) + ret += " </tr>\n" + ret += "</table>\n" + return ret + +if len(sys.argv) > 4 or len(sys.argv) < 3: + print("usage: {} current_values previous_values output_file".format(sys.argv[0])) + print(" output_file is optional, if not specified the script outputs") + print(" to stdout, if set to '-' then it overwrites current_values") + sys.exit(1) + +if len(sys.argv) == 4: + infile_cur, infile_prev, outfile = sys.argv[1:] +else: + infile_cur, infile_prev = sys.argv[1:] + outfile = "" + +data_cur = parse_file(infile_cur) +data_prev = parse_file(infile_prev) + +markup = generate_remarkup(compare_values(data_cur, data_prev)) + +if outfile == "": + sys.stdout.write(markup) + sys.exit(0) + +if outfile == "-": + outfile = infile_cur + +with open(outfile, "w") as f: + f.write(generate_remarkup(compare_values(data_cur, data_prev)))