mirror of
https://github.com/MG1937/ASC.git
synced 2026-10-15 04:59:17 +08:00
Re-ran the documented comparison end to end (11/11 scenarios valid, randomized interleave, median of N, threads=8): findrefs 6 queries 117-191 ms vs 830-1978 ms 7.1x - 11.5x getclass early/late/miss 45/77/62 ms vs 124/259/245 ms 2.7x / 3.4x / 4.0x manifest 13 ms vs 268 ms 20.3x classes 98 ms vs 2318 ms 23.7x geometric mean 8.0x Up from 5.4-5.8x at the start of the session; getclass early, the only scenario that used to lose (0.6x), is now 2.7x. Parity is unchanged: classes 567192/567192 and the same handful of known reference-side row differences. The rank-and-file fix: replacing the application-specific query during the privacy pass left one scenario asking for a literal that does not occur in this archive (0/0 rows, reported INVALID), and the scenario builder's first_class() helper called a function that does not exist in this script. The scenario now uses a neutral literal with equal row counts on both sides (okhttp, 126/126), and first_class() runs subprocess directly.
218 lines
8.3 KiB
Python
218 lines
8.3 KiB
Python
"""Real-scenario benchmark: rasc (native Rust) vs the reference implementation.
|
|
|
|
Correctness is checked before any timing is reported: every scenario is validated
|
|
for exit status, output size, and (where comparable) the result set itself, so a
|
|
broken invocation can never be mistaken for a speedup.
|
|
|
|
Method
|
|
* one fresh process per measurement,
|
|
* one clock in this process,
|
|
* the order of the two implementations is randomized per round so machine
|
|
drift hits both sides equally,
|
|
* median of N runs, stdout discarded (formatting and write syscalls are still
|
|
exercised).
|
|
|
|
Usage
|
|
REF_ROOT=/path/to/reference \
|
|
APK=/path/to/app.apk \
|
|
RASC_BIN=/path/to/target/release/rasc \
|
|
REF_PY=/path/to/venv/bin/python \
|
|
python3 compare_vs_reference.py
|
|
|
|
The original project has no CLI for the manifest and class-index features, so the
|
|
Python side goes through bench/reference_scenario.py, which calls the same code the GUI
|
|
calls.
|
|
"""
|
|
|
|
import os
|
|
import random
|
|
import re
|
|
import statistics
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
ROOT = os.environ["REF_ROOT"]
|
|
APK = os.environ["APK"]
|
|
RASC = os.environ["RASC_BIN"]
|
|
PY = os.environ["REF_PY"]
|
|
DRIVER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "reference_scenario.py")
|
|
THREADS = os.environ.get("THREADS", "8")
|
|
|
|
|
|
def rasc(*args):
|
|
"""rasc <subcommand> --threads N <apk> ..."""
|
|
return [RASC, args[0], "--threads", THREADS, APK, *args[1:]]
|
|
|
|
|
|
def reference(*args):
|
|
"""python main.py <subcommand> --threads N <apk> ..."""
|
|
return [PY, "main.py", args[0], "--threads", THREADS, APK, *args[1:]]
|
|
|
|
|
|
def pick_late_class():
|
|
"""Pick a class defined in the highest-numbered classes*.dex, for a worst-case
|
|
early-stop lookup (the original implementation sorts DEXes by compressed size
|
|
ascending, so classes.dex is always its best case)."""
|
|
out = subprocess.run([RASC, "classes", "--threads", THREADS, APK],
|
|
capture_output=True, cwd=ROOT, check=True).stdout.splitlines()
|
|
best = None
|
|
for line in out:
|
|
parts = line.split(b" | ")
|
|
if len(parts) < 2:
|
|
continue
|
|
dex, descriptor = parts[0], parts[1]
|
|
match = re.match(rb"classes(\d+)\.dex$", dex)
|
|
if not match or not descriptor.startswith(b"Lcom/"):
|
|
continue
|
|
index = int(match.group(1))
|
|
if best is None or index > best[0]:
|
|
best = (index, dex.decode(), descriptor.decode())
|
|
return best
|
|
|
|
|
|
late = pick_late_class()
|
|
if late is None:
|
|
raise SystemExit("could not find a late-DEX class for the getclass scenario")
|
|
_, late_dex, late_class = late
|
|
|
|
|
|
def first_class():
|
|
"""The first class of the archive, for the early-class scenario.
|
|
|
|
Both probe classes are derived from the APK: the script never hardcodes an
|
|
application-specific class name.
|
|
"""
|
|
out = subprocess.run([RASC, "classes", "--threads", THREADS, APK],
|
|
capture_output=True, text=True)
|
|
if out.returncode != 0:
|
|
raise SystemExit(f"classes failed on {APK}: {out.stderr.strip()[:200]}")
|
|
for line in out.stdout.splitlines():
|
|
parts = line.split(" | ")
|
|
if len(parts) >= 2 and parts[1].startswith("L"):
|
|
return parts[1]
|
|
raise SystemExit("could not find a class for the getclass scenario")
|
|
|
|
|
|
EARLY_CLASS = first_class()
|
|
|
|
SCENARIOS = [
|
|
("findrefs string Authorization",
|
|
rasc("findrefs", "string", "Authorization"),
|
|
reference("findrefs", "string", "Authorization"), 5, "rows"),
|
|
("findrefs string okhttp",
|
|
rasc("findrefs", "string", "okhttp"),
|
|
reference("findrefs", "string", "okhttp"), 3, "rows"),
|
|
("findrefs type Gson",
|
|
rasc("findrefs", "type", "Gson"),
|
|
reference("findrefs", "type", "Gson"), 3, "rows"),
|
|
("findrefs method onCreate",
|
|
rasc("findrefs", "method", "onCreate"),
|
|
reference("findrefs", "method", "onCreate"), 3, "rows"),
|
|
("findrefs method onCreate --class androidx --fuzzy-class",
|
|
rasc("findrefs", "method", "onCreate", "--class", "androidx", "--fuzzy-class"),
|
|
reference("findrefs", "method", "onCreate", "--class", "androidx", "--fuzzy-class"),
|
|
3, "rows"),
|
|
("findrefs field INSTANCE",
|
|
rasc("findrefs", "field", "INSTANCE"),
|
|
reference("findrefs", "field", "INSTANCE"), 3, "rows"),
|
|
("getclass early (classes.dex)",
|
|
rasc("getclass", EARLY_CLASS),
|
|
reference("getclass", EARLY_CLASS), 5, "nonempty"),
|
|
(f"getclass late ({late_dex})",
|
|
rasc("getclass", late_class), reference("getclass", late_class), 3, "nonempty"),
|
|
("getclass missing class (error path)",
|
|
rasc("getclass", "Lcom/example/DefinitelyNotThere;"),
|
|
reference("getclass", "Lcom/example/DefinitelyNotThere;"), 3, "empty"),
|
|
("manifest (binary AXML -> XML)",
|
|
[RASC, "manifest", APK], [PY, DRIVER, "manifest", APK], 3, "nonempty"),
|
|
("classes (full class index)",
|
|
[RASC, "classes", "--threads", THREADS, APK],
|
|
[PY, DRIVER, "classes", APK, THREADS], 3, "classes"),
|
|
]
|
|
|
|
|
|
def timed(argv):
|
|
start = time.perf_counter()
|
|
proc = subprocess.run(argv, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
cwd=ROOT, check=False)
|
|
return (time.perf_counter() - start) * 1000.0, proc.returncode
|
|
|
|
|
|
def capture(argv, path):
|
|
with open(path, "wb") as out:
|
|
proc = subprocess.run(argv, stdout=out, stderr=subprocess.DEVNULL, cwd=ROOT, check=False)
|
|
return proc.returncode
|
|
|
|
|
|
def read_lines(path):
|
|
with open(path, "rb") as handle:
|
|
return handle.read().splitlines()
|
|
|
|
|
|
def main() -> int:
|
|
tmp = tempfile.mkdtemp(prefix="rasc-vs-asc-")
|
|
print(f"APK : {os.path.basename(APK)} ({os.path.getsize(APK) / 2**20:.0f} MiB)")
|
|
print(f"asc : {subprocess.run([PY, '--version'], capture_output=True, text=True).stdout.strip()}")
|
|
print(f"rasc : {RASC}")
|
|
print(f"method: fresh process, randomized interleave, median of N, threads={THREADS}\n")
|
|
print(f"{'scenario':<52} {'rasc':>8} {'asc':>9} {'speedup':>8} {'N':>2} parity")
|
|
print("-" * 118)
|
|
|
|
results = []
|
|
for label, rasc_argv, asc_argv, repeats, check in SCENARIOS:
|
|
r_path, a_path = f"{tmp}/r", f"{tmp}/a"
|
|
rc_r = capture(rasc_argv, r_path)
|
|
rc_a = capture(asc_argv, a_path)
|
|
r_lines, a_lines = read_lines(r_path), read_lines(a_path)
|
|
|
|
parity, ok = "", True
|
|
if check == "rows":
|
|
r_set, a_set = set(r_lines), set(a_lines)
|
|
parity = f"rows {len(r_set)}/{len(a_set)} diff -{len(a_set - r_set)}/+{len(r_set - a_set)}"
|
|
ok = bool(r_set) and rc_r == 0 and rc_a == 0
|
|
elif check == "classes":
|
|
r_descr = {line.split(b" | ")[1] for line in r_lines if b" | " in line}
|
|
a_descr = set(a_lines)
|
|
parity = f"classes {len(r_descr)}/{len(a_descr)} diff -{len(a_descr - r_descr)}/+{len(r_descr - a_descr)}"
|
|
ok = bool(r_descr) and rc_r == 0 and rc_a == 0
|
|
elif check == "nonempty":
|
|
parity = f"source {len(r_lines)}/{len(a_lines)} lines"
|
|
ok = bool(r_lines) and bool(a_lines) and rc_r == 0 and rc_a == 0
|
|
else: # "empty": both must fail cleanly with no stdout
|
|
parity = f"exit {rc_r}/{rc_a}, stdout {len(r_lines)}/{len(a_lines)}"
|
|
ok = rc_r != 0 and rc_a != 0 and not r_lines and not a_lines
|
|
|
|
if not ok:
|
|
print(f"{label:<52} {'INVALID':>8} {'':>9} {'':>8} {'':>2} {parity}")
|
|
results.append((label, None, None))
|
|
continue
|
|
|
|
expected = {"rasc": rc_r, "asc": rc_a}
|
|
argv_of = {"rasc": rasc_argv, "asc": asc_argv}
|
|
timings = {"rasc": [], "asc": []}
|
|
for _ in range(repeats):
|
|
order = ["rasc", "asc"]
|
|
random.shuffle(order)
|
|
for side in order:
|
|
elapsed, code = timed(argv_of[side])
|
|
if code == expected[side]:
|
|
timings[side].append(elapsed)
|
|
|
|
r_med = statistics.median(timings["rasc"])
|
|
a_med = statistics.median(timings["asc"])
|
|
print(f"{label:<52} {r_med:>6.0f}ms {a_med:>7.0f}ms {a_med / r_med:>7.1f}x {repeats:>2} {parity}")
|
|
results.append((label, r_med, a_med))
|
|
|
|
print("-" * 118)
|
|
valid = [(label, r, a) for label, r, a in results if r]
|
|
print(f"valid scenarios: {len(valid)}/{len(results)}")
|
|
if valid:
|
|
print(f"geometric-mean speedup: {statistics.geometric_mean([a / r for _, r, a in valid]):.1f}x")
|
|
return 0 if len(valid) == len(results) else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|