WIP smth broke in decode

This commit is contained in:
2026-07-17 02:09:29 +03:00
parent 0d2d02f967
commit 67e7fec3e0
182 changed files with 2572 additions and 4 deletions
+401
View File
@@ -0,0 +1,401 @@
#!/usr/bin/env python3
"""Benchmark rpmsetcmp from a selected set.c on hardcoded ELF pairs."""
from __future__ import annotations
import argparse
import csv
import os
import shlex
import shutil
import statistics
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Sequence
import compare_realization as common
REPO_ROOT = Path(__file__).resolve().parents[1]
RUN_CMP_SOURCE = REPO_ROOT / "scripts" / "run_cmp.cpp"
DEFAULT_RPM_ROOT = REPO_ROOT / "rpm-build"
DEFAULT_RESULT_ROOT = REPO_ROOT / "res_cmp"
LIBC_CANDIDATES = (
"/usr/lib/libc.so.6",
"/lib64/libc.so.6",
"/usr/lib64/libc.so.6",
"/lib/libc.so.6",
"/lib/x86_64-linux-gnu/libc.so.6",
)
@dataclass(frozen=True)
class ComparisonCase:
name: str
binary_candidates: tuple[str, ...]
library_candidates: tuple[str, ...]
# The benchmark corpus is intentionally fixed here rather than supplied via CLI.
HARDCODED_CASES = (
ComparisonCase("true-libc", ("/usr/bin/true", "/bin/true"), LIBC_CANDIDATES),
ComparisonCase("ls-libc", ("/usr/bin/ls", "/bin/ls"), LIBC_CANDIDATES),
ComparisonCase("bash-libc", ("/usr/bin/bash", "/bin/bash"), LIBC_CANDIDATES),
ComparisonCase(
"python-libc",
("/usr/bin/python3", "/usr/local/bin/python3"),
LIBC_CANDIDATES,
),
)
SUMMARY_FIELDS = (
"case",
"binary",
"library",
"runs",
"cmp_result",
"outputs_consistent",
"median_rpmsetcmp_ns",
)
@dataclass(frozen=True)
class ResolvedCase:
name: str
binary: Path
library: Path
@dataclass(frozen=True)
class PreparedCase:
case: ResolvedCase
provider_set: str
required_set: str
@dataclass(frozen=True)
class ComparisonSummary:
prepared: PreparedCase
runs: int
cmp_result: int
outputs_consistent: bool
median_ns: int | float
def as_row(self) -> dict[str, str]:
return {
"case": self.prepared.case.name,
"binary": str(self.prepared.case.binary),
"library": str(self.prepared.case.library),
"runs": str(self.runs),
"cmp_result": str(self.cmp_result),
"outputs_consistent": "yes" if self.outputs_consistent else "no",
"median_rpmsetcmp_ns": common.format_number(self.median_ns),
}
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Compile a selected set.c and benchmark only rpmsetcmp on the "
"hardcoded binary/library corpus."
)
)
parser.add_argument("set_c", type=Path, help="path to the set.c implementation")
parser.add_argument(
"-n",
"--runs",
type=common.positive_int,
default=10,
help="rpmsetcmp process runs per hardcoded case (default: 10)",
)
parser.add_argument(
"--rpm-root",
type=Path,
default=DEFAULT_RPM_ROOT,
help="ALT rpm source root",
)
parser.add_argument(
"--res-dir",
type=Path,
default=DEFAULT_RESULT_ROOT,
help="result root (default: repository res_cmp/)",
)
parser.add_argument("--name", help="implementation directory name under res_cmp/")
parser.add_argument(
"--bpp",
type=common.bpp_value,
help="force one bpp value while preparing provider/required sets",
)
parser.add_argument("--cc", default="cc", help="C compiler (default: cc)")
parser.add_argument("--cxx", default="g++", help="C++ compiler (default: g++)")
return parser.parse_args(argv)
def first_existing(candidates: Sequence[str]) -> Path | None:
for candidate in candidates:
path = Path(candidate)
if path.is_file():
return path.resolve()
return None
def resolve_cases(cases: Sequence[ComparisonCase]) -> list[ResolvedCase]:
resolved: list[ResolvedCase] = []
for case in cases:
binary = first_existing(case.binary_candidates)
library = first_existing(case.library_candidates)
if binary is None or library is None:
missing = "binary" if binary is None else "library"
print(f"warning: skipping {case.name}: no hardcoded {missing} path exists", file=sys.stderr)
continue
resolved.append(ResolvedCase(common.safe_name(case.name), binary, library))
if not resolved:
raise RuntimeError("none of the hardcoded binary/library cases is available")
return resolved
def compile_cmp_runner(build_dir: Path, rpm_root: Path, cxx: str) -> Path:
runner = build_dir / "run_cmp"
command = [
cxx,
"-O2",
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
str(RUN_CMP_SOURCE),
str(build_dir / "set.o"),
str(build_dir / "rpmmalloc.o"),
"-o",
str(runner),
]
log: list[str] = ["\n[rpmsetcmp runner]", "$ " + shlex.join(command)]
completed = subprocess.run(command, cwd=rpm_root, text=True, capture_output=True)
if completed.stdout:
log.append(completed.stdout.rstrip())
if completed.stderr:
log.append(completed.stderr.rstrip())
log.append(f"[exit {completed.returncode}]")
build_log = build_dir / "build.log"
with build_log.open("a", encoding="utf-8") as stream:
stream.write("\n".join(log) + "\n")
if completed.returncode != 0:
raise RuntimeError(f"run_cmp compilation failed; see {build_log}")
return runner
def run_set_once(
runner: Path,
target: Path,
sets_dir: Path,
slug: str,
bpp: int | None,
) -> common.ParsedRun:
command = [str(runner)]
if bpp is not None:
command.extend(("--bpp", str(bpp)))
command.append(str(target))
completed = subprocess.run(command, text=True, capture_output=True)
stdout_path = sets_dir / f"{slug}.tsv"
stderr_path = sets_dir / f"{slug}.stderr"
stdout_path.write_text(completed.stdout, encoding="utf-8")
stderr_path.write_text(completed.stderr, encoding="utf-8")
if completed.returncode != 0:
raise RuntimeError(f"run_set failed for {target}; see {stderr_path}")
return common.parse_run_set_output(completed.stdout)
def same_file(left: str, right: Path) -> bool:
try:
return os.path.samefile(left, right)
except OSError:
return Path(left).resolve() == right.resolve()
def prepare_cases(
cases: Sequence[ResolvedCase],
run_set: Path,
sets_dir: Path,
bpp: int | None,
) -> list[PreparedCase]:
sets_dir.mkdir(parents=True)
cache: dict[Path, common.ParsedRun] = {}
used_slugs: set[str] = set()
def inspect(target: Path) -> common.ParsedRun:
parsed = cache.get(target)
if parsed is not None:
return parsed
slug = common.target_slug(target, used_slugs)
parsed = run_set_once(run_set, target, sets_dir, slug, bpp)
cache[target] = parsed
return parsed
prepared: list[PreparedCase] = []
for case in cases:
library_run = inspect(case.library)
provider_rows = [row for row in library_run.rows if row.get("role") == "provided"]
if len(provider_rows) != 1:
raise RuntimeError(
f"expected one provider set for {case.library}, got {len(provider_rows)}"
)
binary_run = inspect(case.binary)
required_rows = [
row
for row in binary_run.rows
if row.get("role") == "required"
and row.get("object")
and same_file(row["object"], case.library)
]
if len(required_rows) != 1:
raise RuntimeError(
f"expected one {case.library.name} requirement from {case.binary}, "
f"got {len(required_rows)}"
)
provider_row = provider_rows[0]
required_row = required_rows[0]
if provider_row["bpp"] != required_row["bpp"]:
raise RuntimeError(
f"bpp mismatch for {case.name}: provider={provider_row['bpp']} "
f"required={required_row['bpp']}"
)
prepared.append(
PreparedCase(
case=case,
provider_set=provider_row["set"],
required_set=required_row["set"],
)
)
return prepared
def parse_cmp_output(output: str) -> tuple[int, int]:
values: dict[str, str] = {}
for line in output.splitlines():
if "\t" not in line:
continue
key, value = line.split("\t", 1)
values[key] = value
try:
result = int(values["cmp_result"])
elapsed = int(values["rpmsetcmp_ns"])
except (KeyError, ValueError) as exception:
raise RuntimeError("invalid run_cmp output") from exception
if elapsed < 0:
raise RuntimeError("run_cmp returned a negative duration")
return result, elapsed
def benchmark_case(
runner: Path,
prepared: PreparedCase,
runs: int,
output_dir: Path,
) -> ComparisonSummary:
output_dir.mkdir(parents=True)
results: list[int] = []
timings: list[int] = []
for run_number in range(1, runs + 1):
completed = subprocess.run(
[str(runner), prepared.provider_set, prepared.required_set],
text=True,
capture_output=True,
)
stem = f"run-{run_number:03d}"
stdout_path = output_dir / f"{stem}.tsv"
stderr_path = output_dir / f"{stem}.stderr"
stdout_path.write_text(completed.stdout, encoding="utf-8")
stderr_path.write_text(completed.stderr, encoding="utf-8")
if completed.returncode != 0:
raise RuntimeError(
f"run_cmp failed for {prepared.case.name} on run {run_number}; "
f"see {stderr_path}"
)
result, elapsed = parse_cmp_output(completed.stdout)
results.append(result)
timings.append(elapsed)
return ComparisonSummary(
prepared=prepared,
runs=runs,
cmp_result=results[0],
outputs_consistent=len(set(results)) == 1,
median_ns=statistics.median(timings),
)
def write_summary(path: Path, summaries: Sequence[ComparisonSummary]) -> None:
with path.open("w", newline="", encoding="utf-8") as stream:
writer = csv.DictWriter(stream, fieldnames=list(SUMMARY_FIELDS), delimiter="\t")
writer.writeheader()
for summary in summaries:
writer.writerow(summary.as_row())
def print_summary(summaries: Sequence[ComparisonSummary]) -> None:
print("\t".join(SUMMARY_FIELDS))
for summary in summaries:
row = summary.as_row()
print("\t".join(row[field] for field in SUMMARY_FIELDS))
def write_metadata(
path: Path,
set_source: Path,
rpm_root: Path,
runs: int,
cases: Sequence[ResolvedCase],
) -> None:
lines = [
f"set_c\t{set_source}",
f"rpm_root\t{rpm_root}",
f"runs\t{runs}",
]
for case in cases:
lines.append(f"case\t{case.name}\t{case.binary}\t{case.library}")
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def main(argv: Sequence[str]) -> int:
args = parse_args(argv)
try:
common.require_commands((args.cc, args.cxx, "eu-readelf", "eu-elfclassify", "nm"))
set_source = common.resolve_file(args.set_c, "set.c")
rpm_root = common.resolve_directory(args.rpm_root, "rpm root")
common.resolve_file(rpm_root / "system.h", "system.h")
cases = resolve_cases(HARDCODED_CASES)
name = common.safe_name(args.name) if args.name else common.default_name(set_source)
result_dir = args.res_dir.expanduser().resolve() / name
if result_dir.exists():
shutil.rmtree(result_dir)
build_dir = result_dir / "build"
sets_dir = result_dir / "sets"
runs_dir = result_dir / "runs"
build_dir.mkdir(parents=True)
runs_dir.mkdir()
run_set = common.compile_runner(set_source, rpm_root, build_dir, args.cc, args.cxx)
run_cmp = compile_cmp_runner(build_dir, rpm_root, args.cxx)
prepared = prepare_cases(cases, run_set, sets_dir, args.bpp)
summaries = [
benchmark_case(run_cmp, item, args.runs, runs_dir / item.case.name)
for item in prepared
]
write_summary(result_dir / "summary.tsv", summaries)
write_metadata(result_dir / "metadata.tsv", set_source, rpm_root, args.runs, cases)
print_summary(summaries)
print(f"\nresults\t{result_dir}")
return 0
except (OSError, RuntimeError, ValueError) as exception:
print(f"compare_cmp_realization: {exception}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
+477
View File
@@ -0,0 +1,477 @@
#!/usr/bin/env python3
"""Build one set.c implementation and benchmark it through run_set.cpp.
The benchmark stores every raw run_set.cpp stdout/stderr stream and a TSV
summary with median set.c API timings under res/<implementation>/.
"""
from __future__ import annotations
import argparse
import csv
import hashlib
import io
import shlex
import shutil
import statistics
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Sequence
REPO_ROOT = Path(__file__).resolve().parents[1]
RUN_SET_SOURCE = REPO_ROOT / "scripts" / "run_set.cpp"
DEFAULT_RPM_ROOT = REPO_ROOT / "rpm-build"
TIMING_FIELDS = (
"set_new_ns",
"set_add_total_ns",
"set_fini_ns",
"set_free_ns",
"set_api_total_ns",
)
SUMMARY_FIELDS = (
"target",
"kind",
"runs",
"sets_per_run",
"outputs_consistent",
*(f"median_{field}" for field in TIMING_FIELDS),
)
# Each tuple describes one logical target. The first existing path is used.
DEFAULT_TARGET_GROUPS = (
("/usr/bin/true", "/bin/true"),
("/usr/bin/ls", "/bin/ls"),
("/usr/bin/bash", "/bin/bash"),
("/usr/bin/python3", "/usr/local/bin/python3"),
("/usr/lib/libc.so.6", "/lib64/libc.so.6", "/lib/x86_64-linux-gnu/libc.so.6"),
("/usr/lib/libm.so.6", "/lib64/libm.so.6", "/lib/x86_64-linux-gnu/libm.so.6"),
("/usr/lib/libz.so.1", "/usr/lib64/libz.so.1", "/lib/x86_64-linux-gnu/libz.so.1"),
("/usr/lib/libstdc++.so.6", "/usr/lib64/libstdc++.so.6", "/lib/x86_64-linux-gnu/libstdc++.so.6"),
)
@dataclass(frozen=True)
class ParsedRun:
kind: str
rows: tuple[dict[str, str], ...]
totals: dict[str, int]
output_signature: tuple[tuple[str, ...], ...]
@dataclass(frozen=True)
class TargetSummary:
target: Path
kind: str
runs: int
sets_per_run: int
outputs_consistent: bool
medians: dict[str, int | float]
def as_row(self) -> dict[str, str]:
row = {
"target": str(self.target),
"kind": self.kind,
"runs": str(self.runs),
"sets_per_run": str(self.sets_per_run),
"outputs_consistent": "yes" if self.outputs_consistent else "no",
}
row.update(
{f"median_{field}": format_number(value) for field, value in self.medians.items()}
)
return row
def positive_int(value: str) -> int:
number = int(value)
if number < 1:
raise argparse.ArgumentTypeError("must be greater than zero")
return number
def bpp_value(value: str) -> int:
number = int(value)
if not 10 <= number <= 32:
raise argparse.ArgumentTypeError("must be in [10, 32]")
return number
def safe_name(value: str) -> str:
name = "".join(character if character.isalnum() or character in "._-" else "-" for character in value)
name = name.strip(".-")
if not name or name in {".", ".."}:
raise ValueError(f"invalid result name: {value!r}")
return name
def default_name(set_source: Path) -> str:
return safe_name(f"{set_source.parent.name}-{set_source.stem}")
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Compile a selected set.c, run scripts/run_set.cpp repeatedly on ELF targets, "
"and store raw outputs plus median set.c timings."
)
)
parser.add_argument("set_c", type=Path, help="path to the set.c implementation")
parser.add_argument("-n", "--runs", type=positive_int, default=10, help="runs per ELF target (default: 10)")
parser.add_argument("--rpm-root", type=Path, default=DEFAULT_RPM_ROOT, help="ALT rpm source root")
parser.add_argument("--res-dir", type=Path, default=REPO_ROOT / "res", help="result root (default: repository res/)")
parser.add_argument("--name", help="implementation directory name under res/")
parser.add_argument(
"--target",
action="append",
type=Path,
default=[],
help="ELF target; repeat to override the built-in binary/library set",
)
parser.add_argument("--bpp", type=bpp_value, help="force one bpp value for every run_set invocation")
parser.add_argument("--cc", default="cc", help="C compiler (default: cc)")
parser.add_argument("--cxx", default="g++", help="C++ compiler (default: g++)")
return parser.parse_args(argv)
def resolve_file(path: Path, description: str) -> Path:
resolved = path.expanduser().resolve()
if not resolved.is_file():
raise RuntimeError(f"{description} is not a file: {path}")
return resolved
def resolve_directory(path: Path, description: str) -> Path:
resolved = path.expanduser().resolve()
if not resolved.is_dir():
raise RuntimeError(f"{description} is not a directory: {path}")
return resolved
def select_default_targets() -> list[Path]:
selected: list[Path] = []
seen: set[Path] = set()
for candidates in DEFAULT_TARGET_GROUPS:
for candidate in candidates:
path = Path(candidate)
if not path.is_file():
continue
resolved = path.resolve()
if resolved not in seen:
selected.append(resolved)
seen.add(resolved)
break
return selected
def resolve_targets(explicit: Iterable[Path]) -> list[Path]:
supplied = list(explicit)
targets = [resolve_file(path, "target") for path in supplied] if supplied else select_default_targets()
unique: list[Path] = []
seen: set[Path] = set()
for target in targets:
if target not in seen:
unique.append(target)
seen.add(target)
if not unique:
raise RuntimeError("no ELF targets were found; pass at least one --target")
return unique
def require_commands(commands: Iterable[str]) -> None:
missing = [command for command in commands if shutil.which(command) is None]
if missing:
raise RuntimeError("missing required commands: " + ", ".join(missing))
def create_header_shim(rpm_root: Path, include_dir: Path) -> None:
include_dir.mkdir(parents=True, exist_ok=True)
by_name: dict[str, Path] = {}
for header in sorted(rpm_root.glob("*/*.h")):
existing = by_name.get(header.name)
if existing is not None and existing.resolve() != header.resolve():
raise RuntimeError(
f"duplicate rpm header basename {header.name}: {existing} and {header}"
)
by_name[header.name] = header
if not by_name:
raise RuntimeError(f"no */*.h headers found below {rpm_root}")
for name, source in by_name.items():
(include_dir / name).symlink_to(source.resolve())
def run_build_command(
command: Sequence[str], cwd: Path, log: list[str]
) -> subprocess.CompletedProcess[str]:
log.append("$ " + shlex.join(str(part) for part in command))
completed = subprocess.run(command, cwd=cwd, text=True, capture_output=True)
if completed.stdout:
log.append(completed.stdout.rstrip())
if completed.stderr:
log.append(completed.stderr.rstrip())
log.append(f"[exit {completed.returncode}]")
return completed
def compile_runner(
set_source: Path,
rpm_root: Path,
build_dir: Path,
cc: str,
cxx: str,
) -> Path:
include_dir = build_dir / "include" / "rpm"
create_header_shim(rpm_root, include_dir)
set_object = build_dir / "set.o"
malloc_object = build_dir / "rpmmalloc.o"
runner = build_dir / "run_set"
rpmmalloc_source = resolve_file(rpm_root / "rpmio" / "rpmmalloc.c", "rpmmalloc.c")
log: list[str] = [f"set_c={set_source}", f"rpm_root={rpm_root}"]
common_flags = [
"-O2",
"-std=gnu11",
"-include",
"stddef.h",
f"-I{rpm_root}",
f"-I{include_dir}",
]
compatibility_flags = [
"-D_GNU_SOURCE=1",
"-DSTDC_HEADERS=1",
"-DHAVE_STRING_H=1",
"-DHAVE_SETENV=1",
"-DHAVE_STPCPY=1",
"-DHAVE_STPNCPY=1",
"-DHAVE_S_IFSOCK=1",
"-DHAVE_S_ISLNK=1",
"-DHAVE_S_ISSOCK=1",
]
def attempt(extra_flags: Sequence[str], label: str) -> bool:
log.append(f"\n[{label}]")
set_command = [
cc,
*common_flags,
*extra_flags,
"-c",
str(set_source),
"-o",
str(set_object),
]
malloc_command = [
cc,
*common_flags,
*extra_flags,
"-include",
"stdarg.h",
"-c",
str(rpmmalloc_source),
"-o",
str(malloc_object),
]
link_command = [
cxx,
"-O2",
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
str(RUN_SET_SOURCE),
str(set_object),
str(malloc_object),
"-o",
str(runner),
]
for command in (set_command, malloc_command, link_command):
if run_build_command(command, rpm_root, log).returncode != 0:
return False
return True
succeeded = attempt((), "minimal ALT-style build")
if not succeeded:
set_object.unlink(missing_ok=True)
malloc_object.unlink(missing_ok=True)
runner.unlink(missing_ok=True)
succeeded = attempt(compatibility_flags, "modern libc compatibility retry")
build_log = build_dir / "build.log"
build_log.write_text("\n".join(log) + "\n", encoding="utf-8")
if not succeeded:
raise RuntimeError(f"compilation failed; see {build_log}")
return runner
def parse_run_set_output(output: str) -> ParsedRun:
lines = output.splitlines()
try:
table_start = next(index for index, line in enumerate(lines) if line.startswith("role\t"))
except StopIteration as exception:
raise RuntimeError("run_set output has no TSV result table") from exception
metadata: dict[str, str] = {}
for line in lines[:table_start]:
if "\t" not in line:
continue
key, value = line.split("\t", 1)
metadata[key] = value
rows = tuple(csv.DictReader(io.StringIO("\n".join(lines[table_start:])), delimiter="\t"))
if not rows:
raise RuntimeError("run_set output contains no result rows")
totals = {field: 0 for field in TIMING_FIELDS}
for row in rows:
for field in TIMING_FIELDS:
try:
totals[field] += int(row[field])
except (KeyError, ValueError) as exception:
raise RuntimeError(f"invalid {field} in run_set output") from exception
signature_fields = ("role", "object", "labels", "bpp", "set")
signature = tuple(tuple(row[field] for field in signature_fields) for row in rows)
return ParsedRun(
kind=metadata.get("kind", "unknown"),
rows=rows,
totals=totals,
output_signature=signature,
)
def target_slug(target: Path, used: set[str]) -> str:
base = safe_name(target.name)
candidate = base
if candidate in used:
digest = hashlib.sha256(str(target).encode()).hexdigest()[:8]
candidate = f"{base}-{digest}"
used.add(candidate)
return candidate
def benchmark_target(
runner: Path,
target: Path,
runs: int,
output_dir: Path,
bpp: int | None,
) -> TargetSummary:
output_dir.mkdir(parents=True, exist_ok=True)
parsed_runs: list[ParsedRun] = []
for run_number in range(1, runs + 1):
command = [str(runner)]
if bpp is not None:
command.extend(("--bpp", str(bpp)))
command.append(str(target))
completed = subprocess.run(command, text=True, capture_output=True)
stem = f"run-{run_number:03d}"
(output_dir / f"{stem}.tsv").write_text(completed.stdout, encoding="utf-8")
(output_dir / f"{stem}.stderr").write_text(completed.stderr, encoding="utf-8")
if completed.returncode != 0:
raise RuntimeError(
f"run_set failed for {target} on run {run_number}; "
f"see {output_dir / f'{stem}.stderr'}"
)
parsed_runs.append(parse_run_set_output(completed.stdout))
signatures = {parsed.output_signature for parsed in parsed_runs}
kinds = {parsed.kind for parsed in parsed_runs}
row_counts = {len(parsed.rows) for parsed in parsed_runs}
medians = {
field: statistics.median(parsed.totals[field] for parsed in parsed_runs)
for field in TIMING_FIELDS
}
return TargetSummary(
target=target,
kind=parsed_runs[0].kind if len(kinds) == 1 else "inconsistent",
runs=runs,
sets_per_run=len(parsed_runs[0].rows) if len(row_counts) == 1 else -1,
outputs_consistent=len(signatures) == 1 and len(kinds) == 1 and len(row_counts) == 1,
medians=medians,
)
def format_number(value: int | float) -> str:
number = float(value)
return str(int(number)) if number.is_integer() else f"{number:.1f}"
def write_summary(path: Path, summaries: Iterable[TargetSummary]) -> None:
with path.open("w", newline="", encoding="utf-8") as stream:
writer = csv.DictWriter(stream, fieldnames=SUMMARY_FIELDS, delimiter="\t")
writer.writeheader()
for summary in summaries:
writer.writerow(summary.as_row())
def print_summary(summaries: Iterable[TargetSummary]) -> None:
fields = (
"target",
"kind",
"runs",
"sets_per_run",
"outputs_consistent",
"median_set_api_total_ns",
)
print("\t".join(fields))
for summary in summaries:
row = summary.as_row()
print("\t".join(row[field] for field in fields))
def write_metadata(
path: Path,
set_source: Path,
rpm_root: Path,
runner: Path,
runs: int,
targets: Iterable[Path],
) -> None:
lines = [
f"set_c\t{set_source}",
f"rpm_root\t{rpm_root}",
f"runner\t{runner}",
f"runs\t{runs}",
]
lines.extend(f"target\t{target}" for target in targets)
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def main(argv: Sequence[str]) -> int:
args = parse_args(argv)
try:
require_commands((args.cc, args.cxx, "eu-readelf", "eu-elfclassify", "nm"))
set_source = resolve_file(args.set_c, "set.c")
rpm_root = resolve_directory(args.rpm_root, "rpm root")
resolve_file(rpm_root / "system.h", "system.h")
targets = resolve_targets(args.target)
name = safe_name(args.name) if args.name else default_name(set_source)
result_root = args.res_dir.expanduser().resolve()
result_dir = result_root / name
if result_dir.exists():
shutil.rmtree(result_dir)
build_dir = result_dir / "build"
runs_dir = result_dir / "runs"
build_dir.mkdir(parents=True)
runs_dir.mkdir()
runner = compile_runner(set_source, rpm_root, build_dir, args.cc, args.cxx)
used_slugs: set[str] = set()
summaries: list[TargetSummary] = []
for target in targets:
slug = target_slug(target, used_slugs)
summaries.append(
benchmark_target(runner, target, args.runs, runs_dir / slug, args.bpp)
)
write_summary(result_dir / "summary.tsv", summaries)
write_metadata(result_dir / "metadata.tsv", set_source, rpm_root, runner, args.runs, targets)
print_summary(summaries)
print(f"\nresults\t{result_dir}")
return 0
except (OSError, RuntimeError, ValueError) as exception:
print(f"compare_realization: {exception}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
+48
View File
@@ -0,0 +1,48 @@
#include <chrono>
#include <cstdint>
#include <iostream>
#include <string>
extern "C" {
#include "../rpm-build/lib/set.h"
}
namespace {
using Clock = std::chrono::steady_clock;
using Nanoseconds = std::chrono::nanoseconds;
void usage(const char* program)
{
std::cerr << "Usage: " << program << " PROVIDER_SET REQUIRED_SET\n";
}
} // namespace
int main(int argc, char** argv)
{
if (argc != 3) {
usage(argv[0]);
return 2;
}
const std::string provider_set = argv[1];
const std::string required_set = argv[2];
const auto start = Clock::now();
const int result = rpmsetcmp(provider_set.c_str(), required_set.c_str());
const auto finish = Clock::now();
const std::int64_t elapsed =
std::chrono::duration_cast<Nanoseconds>(finish - start).count();
std::cout << "provider_set\t" << provider_set << '\n';
std::cout << "required_set\t" << required_set << '\n';
std::cout << "cmp_result\t" << result << '\n';
std::cout << "rpmsetcmp_ns\t" << elapsed << '\n';
if (result == -3 || result == -4) {
std::cerr << "run_cmp: rpmsetcmp rejected an input set (result=" << result << ")\n";
return 1;
}
return 0;
}
+585
View File
@@ -0,0 +1,585 @@
#include <algorithm>
#include <chrono>
#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <map>
#include <optional>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
extern "C" {
#include "../rpm-build/lib/set.h"
}
namespace {
using Clock = std::chrono::steady_clock;
using Nanoseconds = std::chrono::nanoseconds;
using LabelSet = std::set<std::string>;
struct CommandResult {
int exit_code;
std::string output;
};
struct Timings {
std::int64_t set_new_ns = 0;
std::int64_t set_add_total_ns = 0;
std::int64_t set_fini_ns = 0;
std::int64_t set_free_ns = 0;
std::int64_t total_ns() const
{
return set_new_ns + set_add_total_ns + set_fini_ns + set_free_ns;
}
};
struct SetResult {
std::string value;
Timings timings;
};
struct Options {
std::filesystem::path input;
std::optional<int> bpp;
};
std::int64_t elapsed_ns(Clock::time_point start, Clock::time_point finish)
{
return std::chrono::duration_cast<Nanoseconds>(finish - start).count();
}
std::string shortened(const std::string& value, std::size_t limit = 1200)
{
if (value.size() <= limit) {
return value;
}
return value.substr(0, limit) + "\n... output truncated ...";
}
CommandResult run_command(const std::vector<std::string>& command,
const std::map<std::string, std::string>& environment = {})
{
if (command.empty()) {
throw std::runtime_error("empty command");
}
int output_pipe[2];
if (pipe(output_pipe) != 0) {
throw std::runtime_error("pipe failed: " + std::string(std::strerror(errno)));
}
const pid_t child = fork();
if (child < 0) {
const int saved_errno = errno;
close(output_pipe[0]);
close(output_pipe[1]);
throw std::runtime_error("fork failed: " + std::string(std::strerror(saved_errno)));
}
if (child == 0) {
close(output_pipe[0]);
if (dup2(output_pipe[1], STDOUT_FILENO) < 0 || dup2(output_pipe[1], STDERR_FILENO) < 0) {
_exit(126);
}
close(output_pipe[1]);
setenv("LC_ALL", "C", 1);
for (const auto& [name, value] : environment) {
setenv(name.c_str(), value.c_str(), 1);
}
std::vector<char*> argv;
argv.reserve(command.size() + 1);
for (const std::string& argument : command) {
argv.push_back(const_cast<char*>(argument.c_str()));
}
argv.push_back(nullptr);
execvp(argv[0], argv.data());
_exit(127);
}
close(output_pipe[1]);
std::string output;
char buffer[16384];
while (true) {
const ssize_t count = read(output_pipe[0], buffer, sizeof(buffer));
if (count > 0) {
output.append(buffer, static_cast<std::size_t>(count));
continue;
}
if (count < 0 && errno == EINTR) {
continue;
}
if (count < 0) {
const int saved_errno = errno;
close(output_pipe[0]);
waitpid(child, nullptr, 0);
throw std::runtime_error("read from child failed: " + std::string(std::strerror(saved_errno)));
}
break;
}
close(output_pipe[0]);
int status = 0;
while (waitpid(child, &status, 0) < 0) {
if (errno != EINTR) {
throw std::runtime_error("waitpid failed: " + std::string(std::strerror(errno)));
}
}
int exit_code = 128;
if (WIFEXITED(status)) {
exit_code = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
exit_code += WTERMSIG(status);
}
return {exit_code, std::move(output)};
}
CommandResult checked_command(const std::vector<std::string>& command,
const std::map<std::string, std::string>& environment = {})
{
CommandResult result = run_command(command, environment);
if (result.exit_code != 0) {
std::ostringstream message;
message << command.front() << " exited with " << result.exit_code;
if (!result.output.empty()) {
message << ":\n" << shortened(result.output);
}
throw std::runtime_error(message.str());
}
return result;
}
std::string strip_symbol_version(std::string symbol)
{
const std::size_t at = symbol.find('@');
if (at != std::string::npos) {
symbol.resize(at);
}
return symbol;
}
bool allowed_symbol_type(const std::string& type)
{
static const std::unordered_set<std::string> allowed = {
"NOTYPE", "OBJECT", "FUNC", "COMMON", "TLS", "IFUNC", "GNU_IFUNC",
};
return allowed.count(type) != 0;
}
bool allowed_symbol_binding(const std::string& binding)
{
static const std::unordered_set<std::string> allowed = {
"GLOBAL", "WEAK", "UNIQUE", "GNU_UNIQUE",
};
return allowed.count(binding) != 0;
}
bool allowed_symbol_visibility(const std::string& visibility)
{
return visibility == "DEFAULT" || visibility == "PROTECTED";
}
bool special_symbol(const std::string& symbol)
{
static const std::unordered_set<std::string> ignored = {
"__bss_start", "_edata", "_end", "_fini", "_init",
};
return ignored.count(symbol) != 0;
}
LabelSet provided_labels(const std::filesystem::path& library)
{
const CommandResult result = checked_command(
{"eu-readelf", "--wide", "--dyn-syms", library.string()});
LabelSet labels;
std::istringstream output(result.output);
std::string line;
while (std::getline(output, line)) {
std::istringstream fields(line);
std::string number;
std::string value;
std::string size;
std::string type;
std::string binding;
std::string visibility;
std::string section;
std::string symbol;
if (!(fields >> number >> value >> size >> type >> binding >> visibility >> section >> symbol)) {
continue;
}
if (number.empty() || number.back() != ':') {
continue;
}
std::uint64_t symbol_value = 0;
try {
symbol_value = std::stoull(value, nullptr, 16);
} catch (const std::exception&) {
continue;
}
if (symbol_value == 0 && type != "TLS") {
continue;
}
if (!allowed_symbol_type(type) || !allowed_symbol_binding(binding) ||
!allowed_symbol_visibility(visibility)) {
continue;
}
symbol = strip_symbol_version(std::move(symbol));
if (symbol.empty() || special_symbol(symbol) ||
symbol.find_first_of("()@") != std::string::npos) {
continue;
}
labels.insert(std::move(symbol));
}
return labels;
}
LabelSet weak_undefined_labels(const std::filesystem::path& executable)
{
const CommandResult result = checked_command({"nm", "--dynamic", executable.string()});
LabelSet weak;
std::istringstream output(result.output);
std::string line;
while (std::getline(output, line)) {
std::istringstream fields(line);
std::vector<std::string> parts;
std::string part;
while (fields >> part) {
parts.push_back(part);
}
if (parts.size() != 2 || parts[0].size() != 1 ||
std::string("wWvV").find(parts[0][0]) == std::string::npos) {
continue;
}
weak.insert(strip_symbol_version(parts[1]));
}
return weak;
}
std::string program_interpreter(const std::filesystem::path& executable)
{
const CommandResult result = checked_command(
{"eu-readelf", "--program-headers", executable.string()});
const std::string marker = "[Requesting program interpreter: ";
const std::size_t start = result.output.find(marker);
if (start == std::string::npos) {
throw std::runtime_error("ELF executable has no PT_INTERP entry: " + executable.string());
}
const std::size_t value_start = start + marker.size();
const std::size_t end = result.output.find(']', value_start);
if (end == std::string::npos || end == value_start) {
throw std::runtime_error("cannot parse PT_INTERP for " + executable.string());
}
return result.output.substr(value_start, end - value_start);
}
bool same_path(const std::string& lhs, const std::filesystem::path& rhs)
{
std::error_code error;
if (std::filesystem::equivalent(lhs, rhs, error)) {
return true;
}
return std::filesystem::path(lhs).lexically_normal() == rhs.lexically_normal();
}
std::map<std::string, LabelSet> required_labels_by_provider(
const std::filesystem::path& executable)
{
const std::string interpreter = program_interpreter(executable);
const std::map<std::string, std::string> environment = {
{"LD_BIND_NOW", "1"},
{"LD_DEBUG", "bindings"},
{"LD_TRACE_LOADED_OBJECTS", "1"},
{"LD_WARN", "1"},
};
const CommandResult result = checked_command(
{interpreter, executable.string()}, environment);
const LabelSet weak = weak_undefined_labels(executable);
std::map<std::string, LabelSet> labels_by_provider;
std::istringstream output(result.output);
std::string line;
while (std::getline(output, line)) {
const std::string binding_marker = "binding file ";
const std::size_t binding = line.find(binding_marker);
if (binding == std::string::npos) {
continue;
}
const std::size_t source_start = binding + binding_marker.size();
const std::size_t source_end = line.find(" [", source_start);
const std::size_t to = source_end == std::string::npos
? std::string::npos
: line.find(" to ", source_end);
const std::size_t provider_start = to == std::string::npos
? std::string::npos
: to + std::string(" to ").size();
const std::size_t provider_end = provider_start == std::string::npos
? std::string::npos
: line.find(" [", provider_start);
const std::size_t symbol_marker = provider_end == std::string::npos
? std::string::npos
: line.find(" symbol ", provider_end);
if (source_end == std::string::npos || to == std::string::npos ||
provider_start == std::string::npos || provider_end == std::string::npos ||
symbol_marker == std::string::npos) {
continue;
}
const std::string source = line.substr(source_start, source_end - source_start);
const std::string provider = line.substr(provider_start, provider_end - provider_start);
if (!same_path(source, executable) || source == provider) {
continue;
}
std::size_t symbol_start = symbol_marker + std::string(" symbol ").size();
if (symbol_start < line.size() && (line[symbol_start] == '`' || line[symbol_start] == '\'')) {
++symbol_start;
}
std::size_t symbol_end = line.find('\'', symbol_start);
if (symbol_end == std::string::npos) {
symbol_end = line.find_first_of(" \t[", symbol_start);
}
if (symbol_end == std::string::npos) {
symbol_end = line.size();
}
std::string symbol = strip_symbol_version(
line.substr(symbol_start, symbol_end - symbol_start));
if (symbol.empty() || weak.count(symbol) != 0) {
continue;
}
labels_by_provider[provider].insert(std::move(symbol));
}
for (auto it = labels_by_provider.begin(); it != labels_by_provider.end();) {
if (it->second.empty()) {
it = labels_by_provider.erase(it);
} else {
++it;
}
}
return labels_by_provider;
}
int suggested_bpp(std::size_t provider_label_count)
{
if (provider_label_count < 1) {
provider_label_count = 1;
}
std::size_t value = provider_label_count - 1;
int bits = 0;
while (value != 0) {
++bits;
value >>= 1;
}
return std::min(32, bits + 10);
}
SetResult build_set(const LabelSet& labels, int bpp)
{
if (labels.empty()) {
throw std::runtime_error("cannot build a set from zero labels");
}
Timings timings;
auto start = Clock::now();
struct set* value = set_new();
auto finish = Clock::now();
timings.set_new_ns = elapsed_ns(start, finish);
if (value == nullptr) {
throw std::runtime_error("set_new returned NULL");
}
start = Clock::now();
for (const std::string& label : labels) {
set_add(value, label.c_str());
}
finish = Clock::now();
timings.set_add_total_ns = elapsed_ns(start, finish);
start = Clock::now();
const char* payload = set_fini(value, bpp);
finish = Clock::now();
timings.set_fini_ns = elapsed_ns(start, finish);
if (payload == nullptr) {
set_free(value);
throw std::runtime_error("set_fini returned NULL");
}
std::string encoded = "set:" + std::string(payload);
std::free(const_cast<char*>(payload));
start = Clock::now();
value = set_free(value);
finish = Clock::now();
timings.set_free_ns = elapsed_ns(start, finish);
(void)value;
return {std::move(encoded), timings};
}
enum class ElfKind {
executable,
shared_library,
};
ElfKind classify_elf(const std::filesystem::path& input)
{
const CommandResult executable = run_command(
{"eu-elfclassify", "--executable", input.string()});
if (executable.exit_code == 0) {
return ElfKind::executable;
}
if (executable.exit_code == 127) {
throw std::runtime_error("eu-elfclassify is required but was not found");
}
const CommandResult shared = run_command(
{"eu-elfclassify", "--shared", input.string()});
if (shared.exit_code == 0) {
return ElfKind::shared_library;
}
throw std::runtime_error("input is not a supported dynamic ELF executable or shared library: " +
input.string());
}
int parse_bpp(const std::string& value)
{
std::size_t parsed = 0;
int bpp = 0;
try {
bpp = std::stoi(value, &parsed);
} catch (const std::exception&) {
throw std::runtime_error("invalid bpp: " + value);
}
if (parsed != value.size() || bpp < 10 || bpp > 32) {
throw std::runtime_error("bpp must be an integer in [10, 32]");
}
return bpp;
}
void usage(const char* program)
{
std::cerr << "Usage: " << program << " [--bpp 10..32] ELF_PATH\n";
}
Options parse_options(int argc, char** argv)
{
Options options;
for (int index = 1; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--help" || argument == "-h") {
usage(argv[0]);
std::exit(0);
}
if (argument == "--bpp") {
if (++index >= argc) {
throw std::runtime_error("--bpp requires a value");
}
options.bpp = parse_bpp(argv[index]);
continue;
}
if (!argument.empty() && argument.front() == '-') {
throw std::runtime_error("unknown option: " + argument);
}
if (!options.input.empty()) {
throw std::runtime_error("exactly one ELF path is required");
}
options.input = argument;
}
if (options.input.empty()) {
throw std::runtime_error("ELF path is required");
}
return options;
}
void print_header(const std::filesystem::path& input, ElfKind kind,
const std::optional<int>& bpp)
{
std::cout << "input\t" << input.string() << '\n';
std::cout << "kind\t"
<< (kind == ElfKind::executable ? "executable" : "shared-library") << '\n';
std::cout << "bpp_mode\t" << (bpp.has_value() ? "override" : "auto") << '\n';
std::cout << "role\tobject\tlabels\tbpp\tset\tset_new_ns\tset_add_total_ns\t"
"set_fini_ns\tset_free_ns\tset_api_total_ns\n";
}
void print_row(const std::string& role, const std::string& object,
std::size_t label_count, int bpp, const SetResult& result)
{
const Timings& timings = result.timings;
std::cout << role << '\t' << object << '\t' << label_count << '\t' << bpp << '\t'
<< result.value << '\t' << timings.set_new_ns << '\t'
<< timings.set_add_total_ns << '\t' << timings.set_fini_ns << '\t'
<< timings.set_free_ns << '\t' << timings.total_ns() << '\n';
}
int run(const Options& options)
{
std::error_code error;
const std::filesystem::path input = std::filesystem::canonical(options.input, error);
if (error || !std::filesystem::is_regular_file(input)) {
throw std::runtime_error("input is not a readable regular file: " + options.input.string());
}
if (input.string().find_first_of("\t\r\n") != std::string::npos) {
throw std::runtime_error("input path contains characters unsupported by TSV output");
}
const ElfKind kind = classify_elf(input);
print_header(input, kind, options.bpp);
if (kind == ElfKind::shared_library) {
const LabelSet labels = provided_labels(input);
if (labels.empty()) {
throw std::runtime_error("no provided labels found in " + input.string());
}
const int bpp = options.bpp.value_or(suggested_bpp(labels.size()));
print_row("provided", input.string(), labels.size(), bpp, build_set(labels, bpp));
return 0;
}
const std::map<std::string, LabelSet> requirements = required_labels_by_provider(input);
if (requirements.empty()) {
throw std::runtime_error("no external dynamic symbol bindings found in " + input.string());
}
for (const auto& [provider, labels] : requirements) {
std::size_t provider_count = 0;
try {
provider_count = provided_labels(provider).size();
} catch (const std::exception& exception) {
std::cerr << "warning: cannot inspect provider " << provider << ": "
<< exception.what() << '\n';
}
const int bpp = options.bpp.value_or(
suggested_bpp(provider_count == 0 ? labels.size() : provider_count));
print_row("required", provider, labels.size(), bpp, build_set(labels, bpp));
}
return 0;
}
} // namespace
int main(int argc, char** argv)
{
try {
return run(parse_options(argc, argv));
} catch (const std::exception& exception) {
std::cerr << "run_set: " << exception.what() << '\n';
usage(argv[0]);
return 2;
}
}