blob: 78b2964d1d0702dd2e5167f2158a06224bf89557 [file] [edit]
#!/usr/bin/env python3
# Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
import argparse
import json
import re
import shutil
import subprocess
import sys
from collections import defaultdict
def parse_duration(duration_str):
if not duration_str:
return 0.0
match = re.match(r"([\d\.]+)(ms|m|s)?", duration_str)
if not match:
return 0.0
val = float(match.group(1))
unit = match.group(2)
if unit == "m":
return val * 60.0
elif unit == "ms":
return val / 1000.0
return val
def shorten_name(name, remove_prefix):
if not remove_prefix:
return name
prefix = "com.android.tools.r8."
if name.startswith(prefix):
return "..." + name[len(prefix):]
return name
def extract_class_name(test_result):
test_id_struct = test_result.get("testIdStructured", {})
coarse = test_id_struct.get("coarseName", "")
fine = test_id_struct.get("fineName", "")
if coarse and fine:
return f"{coarse}.{fine}"
test_id = test_result.get("testId", "")
match = re.match(r":r8!junit:([^#:]+)", test_id)
if match:
return match.group(1)
return "Unknown"
def extract_test_name(test_result):
test_id = test_result.get("testId", "")
prefix = ":r8!junit:"
if test_id.startswith(prefix):
return test_id[len(prefix):]
return test_id
def check_rdb_auth():
"""Returns True if rdb is authenticated, False otherwise."""
try:
result = subprocess.run(["rdb", "auth-info"],
capture_output=True,
text=True)
if result.returncode != 0 or "Logged in as" not in result.stdout:
return False
return True
except Exception:
return False
def main():
parser = argparse.ArgumentParser(
description="Generate a Markdown table of test times from RDB.")
parser.add_argument("-i",
"--input",
help="Path to an existing RDB JSON file.")
parser.add_argument(
"-b",
"--build",
help="The RDB build ID to query (e.g., build-8680470238573953425).")
parser.add_argument(
"--limit",
type=int,
default=25,
help="Number of top classes or tests to display (default: 25)")
parser.add_argument(
"--remove-prefix",
dest="remove_prefix",
action="store_true",
default=True,
help=
"Shorten the class/test name prefix 'com.android.tools.r8.' to '...' (default)."
)
parser.add_argument("--no-remove-prefix",
dest="remove_prefix",
action="store_false",
help="Do not shorten the prefix.")
parser.add_argument(
"--per-class",
dest="per_class",
action="store_true",
default=True,
help="Aggregate the test times per test class (default).")
parser.add_argument("--no-per-class",
dest="per_class",
action="store_false",
help="Do not aggregate. Show individual test cases.")
args = parser.parse_args()
if not args.input and not args.build:
print(
"Error: You must provide either --input <file> or --build <build_id>.",
file=sys.stderr)
parser.print_help()
sys.exit(1)
lines_source = []
if args.input:
try:
with open(args.input, "r") as f:
lines_source = f.readlines()
except FileNotFoundError:
print(f"Error: Input file '{args.input}' not found.",
file=sys.stderr)
sys.exit(1)
else:
# Before running query, check if rdb is in PATH
if not shutil.which("rdb"):
print("Error: 'rdb' command not found in PATH.", file=sys.stderr)
sys.exit(1)
# Check if rdb is authenticated
if not check_rdb_auth():
print("Error: You do not appear to be logged into ResultDB.",
file=sys.stderr)
print(
"Please run 'rdb auth-login' in your terminal to authenticate first.",
file=sys.stderr)
sys.exit(1)
cmd = ["rdb", "query", args.build, "-json"]
print(f"Running: {' '.join(cmd)} ...", file=sys.stderr)
try:
process = subprocess.run(cmd,
capture_output=True,
text=True,
check=True)
lines_source = process.stdout.splitlines()
except subprocess.CalledProcessError as e:
print(f"Error running rdb: {e.stderr}", file=sys.stderr)
sys.exit(e.returncode)
# Process results based on mode
if args.per_class:
class_times = defaultdict(float)
class_counts = defaultdict(int)
for line in lines_source:
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
test_result = data.get("testResult", {})
class_name = extract_class_name(test_result)
duration_str = test_result.get("duration")
if duration_str:
duration = parse_duration(duration_str)
class_times[class_name] += duration
class_counts[class_name] += 1
except Exception:
pass
sorted_results = sorted(class_times.items(),
key=lambda x: x[1],
reverse=True)
markdown_lines = []
markdown_lines.append(
"| Class Name | Total Duration | Test Count | Avg Duration |")
markdown_lines.append("| :--- | :---: | :---: | :---: |")
for class_name, total_dur in sorted_results[:args.limit]:
count = class_counts[class_name]
avg_dur = total_dur / count if count > 0 else 0.0
total_dur_str = f"**{total_dur:.3f}s**" if total_dur > 100 else f"{total_dur:.3f}s"
display_name = shorten_name(class_name, args.remove_prefix)
markdown_lines.append(
f"| `{display_name}` | {total_dur_str} | {count} | {avg_dur:.3f}s |"
)
else:
individual_tests = []
for line in lines_source:
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
test_result = data.get("testResult", {})
test_name = extract_test_name(test_result)
duration_str = test_result.get("duration")
if duration_str:
duration = parse_duration(duration_str)
individual_tests.append((test_name, duration))
except Exception:
pass
sorted_results = sorted(individual_tests,
key=lambda x: x[1],
reverse=True)
markdown_lines = []
markdown_lines.append("| Test Name | Duration |")
markdown_lines.append("| :--- | :---: |")
for test_name, dur in sorted_results[:args.limit]:
dur_str = f"**{dur:.3f}s**" if dur > 100 else f"{dur:.3f}s"
display_name = shorten_name(test_name, args.remove_prefix)
markdown_lines.append(f"| `{display_name}` | {dur_str} |")
if not sorted_results:
print("No tests found or processed.", file=sys.stderr)
sys.exit(0)
markdown_content = "\n".join(markdown_lines) + "\n"
glow_path = shutil.which("glow")
if glow_path and sys.stdout.isatty():
try:
process = subprocess.Popen([glow_path, "--width", "0", "-"],
stdin=subprocess.PIPE,
text=True)
process.communicate(input=markdown_content)
except Exception:
print(markdown_content, end="")
else:
print(markdown_content, end="")
if __name__ == "__main__":
main()