[llvm] [BOLT][Utils] Add nfc-stat-parser.py (PR #71979)

Amir Ayupov via llvm-commits llvm-commits at lists.llvm.org
Fri Nov 10 19:09:21 PST 2023


https://github.com/aaupov updated https://github.com/llvm/llvm-project/pull/71979

>From 4bb31ab219cbe147c9aa5414cff03ba9871ab48c Mon Sep 17 00:00:00 2001
From: Amir Ayupov <aaupov at fb.com>
Date: Fri, 10 Nov 2023 12:24:58 -0800
Subject: [PATCH 1/2] [BOLT][Utils] Add nfc-stat-parser.py

Add a utility to parse output from llvm-bolt-wrapper script and
detect individual and aggregate time/memory swings. The wrapper reports
wall time and peak RSS for each BOLT invocation.

Exit code:
The utility exits with non-zero exit code if any individual test has
time or memory swing larger than `threshold_single` (default 10%), or
the aggregate (geometric mean) swing larger than `threshold_agg`
(default 5%). Short tests where BOLT wall time is less than
`check_longer_than` seconds (default 0.5s) are excluded from threshold
calculation.

Output:
The script prints test results exceeding the individual threshold, and
geomean values if it exceeds aggregate results. In `--verbose` mode all
individual results are printed (short time results are marked with '?').

Example usage:
```
$ cd ~/llvm-build # build folder where NFC testing was invoked
$ python3 ~/llvm-project/bolt/utils/nfc-stat-parser.py \
  --check_longer_than 0.1 `find -name timing.log`
./tools/bolt/test/runtime/X86/exceptions-pic.test/ -88.46% -0.13%
Geomean -19.78% +0.37%
$ echo $?
1
```
---
 bolt/utils/nfc-stat-parser.py | 125 ++++++++++++++++++++++++++++++++++
 1 file changed, 125 insertions(+)
 create mode 100755 bolt/utils/nfc-stat-parser.py

diff --git a/bolt/utils/nfc-stat-parser.py b/bolt/utils/nfc-stat-parser.py
new file mode 100755
index 000000000000000..01ff352278ba649
--- /dev/null
+++ b/bolt/utils/nfc-stat-parser.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+import argparse
+import csv
+import re
+import sys
+import os
+from statistics import geometric_mean
+
+TIMING_LOG_RE = re.compile(r"(.*)/(.*).tmp(.*)")
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="BOLT NFC stat parser",
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+    )
+    parser.add_argument(
+        "input", nargs="+", help="timing.log files produced by llvm-bolt-wrapper"
+    )
+    parser.add_argument(
+        "--check_longer_than",
+        default=0.5,
+        type=float,
+        help="Only warn on tests longer than X seconds for at least one side",
+    )
+    parser.add_argument(
+        "--threshold_single",
+        default=10,
+        type=float,
+        help="Threshold for a single test result swing, abs percent",
+    ),
+    parser.add_argument(
+        "--threshold_agg",
+        default=5,
+        type=float,
+        help="Threshold for geomean test results swing, abs percent",
+    ),
+    parser.add_argument("--verbose", "-v", action="store_true")
+    args = parser.parse_args()
+
+    def fmt_delta(value, exc_threshold, above_bound=True):
+        formatted_value = format(value, "+.2%")
+        if not above_bound:
+            formatted_value += "?"
+        elif exc_threshold and sys.stdout.isatty():  # terminal supports colors
+            return f"\033[1m{formatted_value}\033[0m"
+        return formatted_value
+
+    # Ratios for geomean computation
+    time_ratios = []
+    mem_ratios = []
+    # Whether any test exceeds the single test threshold (mem or time)
+    threshold_single = False
+    # Whether geomean exceeds aggregate test threshold (mem or time)
+    threshold_agg = False
+
+    if args.verbose:
+        print(f"# Individual test threshold: +-{args.threshold_single}%")
+        print(f"# Aggregate (geomean) test threshold: +-{args.threshold_agg}%")
+        print(
+            f"# Checking time swings for tests with runtime >"
+            f"{args.check_longer_than}s - otherwise marked as ?"
+        )
+        print("Test/binary BOLT_wall_time BOLT_max_rss")
+
+    for input_file in args.input:
+        input_dir = os.path.dirname(input_file)
+        with open(input_file) as timing_file:
+            timing_reader = csv.reader(timing_file, delimiter=";")
+            for row in timing_reader:
+                test_name = row[0]
+                m = TIMING_LOG_RE.match(row[0])
+                if m:
+                    test_name = f"{input_dir}/{m.groups()[1]}/{m.groups()[2]}"
+                else:
+                    # Prepend input dir to unparsed test name
+                    test_name = input_dir + "#" + test_name
+                time_a, time_b = float(row[1]), float(row[3])
+                mem_a, mem_b = int(row[2]), int(row[4])
+                # Check if time is above bound for at least one side
+                time_above_bound = any(
+                    [x > args.check_longer_than for x in [time_a, time_b]]
+                )
+                # Compute B/A ratios (for % delta and geomean)
+                time_ratio = time_b / time_a
+                mem_ratio = mem_b / mem_a
+                # Keep ratios for geomean
+                if time_above_bound and time_ratio > 0:  # must be >0 for gmean
+                    time_ratios += [time_ratio]
+                mem_ratios += [mem_ratio]
+                # Deltas: (B/A)-1 = (B-A)/A
+                time_delta = time_ratio - 1
+                mem_delta = mem_ratio - 1
+                # Check individual test results vs single test threshold
+                time_exc = (
+                    100.0 * abs(time_delta) > args.threshold_single and time_above_bound
+                )
+                mem_exc = 100.0 * abs(mem_delta) > args.threshold_single
+                if time_exc or mem_exc:
+                    threshold_single = True
+                # Print deltas with formatting in verbose mode
+                if args.verbose or time_exc or mem_exc:
+                    print(
+                        test_name,
+                        fmt_delta(time_delta, time_exc, time_above_bound),
+                        fmt_delta(mem_delta, mem_exc),
+                    )
+
+    time_gmean_delta = geometric_mean(time_ratios) - 1
+    mem_gmean_delta = geometric_mean(mem_ratios) - 1
+    time_agg_threshold = 100.0 * abs(time_gmean_delta) > args.threshold_agg
+    mem_agg_threshold = 100.0 * abs(mem_gmean_delta) > args.threshold_agg
+    if time_agg_threshold or mem_agg_threshold:
+        threshold_agg = True
+    if time_agg_threshold or mem_agg_threshold or args.verbose:
+        print(
+            "Geomean",
+            fmt_delta(time_gmean_delta, time_agg_threshold),
+            fmt_delta(mem_gmean_delta, mem_agg_threshold),
+        )
+    exit(threshold_single or threshold_agg)
+
+
+if __name__ == "__main__":
+    main()

>From 857188eba95473ecde808e618b67d19d1ede0851 Mon Sep 17 00:00:00 2001
From: Amir Ayupov <aaupov at fb.com>
Date: Fri, 10 Nov 2023 19:08:34 -0800
Subject: [PATCH 2/2] Check division by zero

---
 bolt/utils/nfc-stat-parser.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/bolt/utils/nfc-stat-parser.py b/bolt/utils/nfc-stat-parser.py
index 01ff352278ba649..694ffd88fe57700 100755
--- a/bolt/utils/nfc-stat-parser.py
+++ b/bolt/utils/nfc-stat-parser.py
@@ -82,8 +82,8 @@ def fmt_delta(value, exc_threshold, above_bound=True):
                     [x > args.check_longer_than for x in [time_a, time_b]]
                 )
                 # Compute B/A ratios (for % delta and geomean)
-                time_ratio = time_b / time_a
-                mem_ratio = mem_b / mem_a
+                time_ratio = time_b / time_a if time_a else float('nan')
+                mem_ratio = mem_b / mem_a if mem_a else float('nan')
                 # Keep ratios for geomean
                 if time_above_bound and time_ratio > 0:  # must be >0 for gmean
                     time_ratios += [time_ratio]



More information about the llvm-commits mailing list