From 52403ae6b970866ccb0eb39dfbc61c7c2fb8c130 Mon Sep 17 00:00:00 2001
From: Andrei Stan <andreistan2003@gmail.com>
Date: Sun, 17 Nov 2024 19:26:42 +0200
Subject: [PATCH] checker.py: Check speedup

Signed-off-by: Andrei Stan <andreistan2003@gmail.com>
---
 tests/checker.py | 62 ++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 49 insertions(+), 13 deletions(-)

diff --git a/tests/checker.py b/tests/checker.py
index 50caa84..9763732 100644
--- a/tests/checker.py
+++ b/tests/checker.py
@@ -27,6 +27,8 @@ SERIAL_TIMEOUT = 5
 
 TEST_SIZES = [10, 100, 1_000, 10_000, 20_000]
 
+test_duration = {}
+passed_tests = set()
 
 """Two millisecond for each probe"""
 TRACE_LOG_TIMESLICE = 1 / 500
@@ -148,7 +150,7 @@ def file_exists_probe_check(probe: List[bool]) -> bool:
 def run_once_and_check_output(binary: str, in_file_path: str, out_file_path: str,
                               ref_file_path: str, sort_out_file: bool,
                               threads: int = 1, test_timeout: float = 1,
-                              trace_logs: bool = False) -> bool:
+                              trace_logs: bool = False) -> (bool, int):
     """Run the test once and check the output file with the reference file
 
     Also, delete the output file before running to ensure all the content
@@ -160,8 +162,10 @@ def run_once_and_check_output(binary: str, in_file_path: str, out_file_path: str
     except FileNotFoundError:
         pass
 
+    duration = -1
     with subprocess.Popen([binary, in_file_path, out_file_path, f'{threads}'],
             stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setpgrp) as proc_firewall_res:
+        start = time.time_ns()
         try:
             """Check periodically if the output file is sorted correctly"""
             if trace_logs:
@@ -197,22 +201,23 @@ def run_once_and_check_output(binary: str, in_file_path: str, out_file_path: str
                 if err != "":
                     print(err)
                     proc_firewall_res.kill()
-                    return False
+                    return False, duration
 
             else:
                 _, _ = proc_firewall_res.communicate(timeout=test_timeout)
+                duration = time.time_ns() - start
         except subprocess.TimeoutExpired:
             print("Time expired for process!! Killing ...")
             proc_firewall_res.kill()
-            return False
+            return False, duration
         except KeyboardInterrupt:
             # Test was CTRL-C'ed
             proc_firewall_res.kill()
-            return False
+            return False, duration
 
-    return files_are_identical(out_file_path, ref_file_path, sort_out_file)
+    return files_are_identical(out_file_path, ref_file_path, sort_out_file), duration
 
-def check(test_name: str, sort_out_file: bool, threads: int,
+def check(test_name: str, test_size: int, sort_out_file: bool, threads: int,
           test_timeout: float = 1, number_of_runs: int = 1,
           trace_logs: bool = False):
     """Check a test file.
@@ -239,23 +244,52 @@ def check(test_name: str, sort_out_file: bool, threads: int,
     if threads == 1:
         number_of_runs = 1
 
+    if  threads // 2 >= 1 and test_size >= 1000 and \
+        (threads // 2, test_size) not in passed_tests:
+        print(f"Test with {threads // 2} consumers must pass first.")
+        return False
+
     # Running `number_of_runs` times and check the output every time.
     # We do this in order to check the program output is consistent.
+    time_avg = 0
     for _ in range(number_of_runs):
-        if not run_once_and_check_output(firewall_path, in_file_path,
+        res, dur = run_once_and_check_output(firewall_path, in_file_path,
                                          out_file_path, ref_file_path,
                                          True,
                                          threads=threads,
-                                         test_timeout=test_timeout):
+                                         test_timeout=test_timeout)
+        time_avg += dur / 1000
+        if not res:
             return False
 
+    # Save current avarage runtime of the test configuration
+    test_duration[(threads, test_size)] = time_avg / number_of_runs
+
+    # Check if a previous test was run with less threads in order to check speedup
+    # Tests of under 1k packets will not be checked for speedup
+    if threads // 2 >= 1 and test_size >= 1000:
+        if (threads // 2, test_size) in passed_tests:
+            prev_test_duration = test_duration[(threads // 2, test_size)]
+            current_test_duration = test_duration[(threads, test_size)]
+
+            speedup = (prev_test_duration - current_test_duration) / (current_test_duration)
+
+            # Doubling the number of threads should yield at least 20% speedup
+            if speedup < 0.2:
+                print(f"Test ({test_size} packets, {threads} threads) took {current_test_duration} us, "
+                      f"Test ({test_size} packets, {threads // 2} threads) took {prev_test_duration} us "
+                      f"with a speedup of {speedup} which is below 0.2")
+                return False
+
+
     if trace_logs:
-        if not run_once_and_check_output(firewall_path, in_file_path,
+        res, _ = run_once_and_check_output(firewall_path, in_file_path,
                                          out_file_path, ref_file_path,
                                          sort_out_file,
                                          threads=threads,
                                          test_timeout=test_timeout,
-                                         trace_logs=trace_logs):
+                                         trace_logs=trace_logs)
+        if not res:
             return False
     return True
 
@@ -274,7 +308,8 @@ def check_and_grade(test_size: int, sort_out_file: bool = False,
         "s" if threads > 1 else " ")
     result_format += " " + 22 * "." + " "
 
-    if check(test_name, sort_out_file, threads, test_timeout, 20, trace_logs):
+    if check(test_name, test_size, sort_out_file, threads, test_timeout, 20, trace_logs):
+        passed_tests.add((threads, test_size))
         print(result_format + "passed ... {}".format(points))
         TOTAL += points
     else:
@@ -294,7 +329,8 @@ def main():
     # Test out serial implementation: 10 points.
     check_and_grade(TEST_SIZES[0], threads=1, points=3)
     check_and_grade(TEST_SIZES[2], threads=1, points=3)
-    check_and_grade(TEST_SIZES[4], threads=1, points=4)
+    check_and_grade(TEST_SIZES[3], threads=1, points=2)
+    check_and_grade(TEST_SIZES[4], threads=1, points=2)
 
     # Test out parallel implementation, but without the restriction of having
     # correctly sorted output: 50 points (2 x 5 x 5).
@@ -305,8 +341,8 @@ def main():
     # Test out parallel implementation, this time with the restriction of having
     # correctly sored output: 30 points (5 x 6)
     for test_size in TEST_SIZES[2:]:
+        check_and_grade(test_size, threads=2, sort_out_file=False, points=5, trace_logs=True)
         check_and_grade(test_size, threads=4, sort_out_file=False, points=5, trace_logs=True)
-        check_and_grade(test_size, threads=8, sort_out_file=False, points=5, trace_logs=True)
 
     TOTAL = int(TOTAL)
     print("\nTotal:" + 67 * " " + f" {TOTAL}/100")
-- 
GitLab