test D1 realization

This commit is contained in:
2026-08-13 03:03:57 +03:00
parent 60ffed1f6f
commit 373c5d71dc
4 changed files with 1262 additions and 0 deletions
@@ -0,0 +1,296 @@
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef ARSV_WITH_RPM
#include <rpm/header.h>
#include <rpm/rpmio.h>
#include <rpm/rpmlib.h>
#include <rpm/rpmtag.h>
#include <rpm/rpmtd.h>
#endif
int arsv_set9_decode(const char* source, unsigned** hashes, size_t* count, unsigned* bpp);
#define D1_PREFIX "set:D1"
#define D1_HEADER_LEN 8
static const char base64_alphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static size_t base64_encoded_size(size_t byte_count) {
if (byte_count > SIZE_MAX - 2) return SIZE_MAX;
size_t groups = (byte_count + 2) / 3;
if (groups > SIZE_MAX / 4) return SIZE_MAX;
size_t size = groups * 4;
size_t remainder = byte_count % 3;
if (remainder) size -= 3 - remainder;
return size;
}
static void base64_encode(const unsigned char* input, size_t input_len, char* output) {
while (input_len >= 3) {
uint32_t value = ((uint32_t)input[0] << 16) | ((uint32_t)input[1] << 8) | input[2];
output[0] = base64_alphabet[(value >> 18) & 0x3f];
output[1] = base64_alphabet[(value >> 12) & 0x3f];
output[2] = base64_alphabet[(value >> 6) & 0x3f];
output[3] = base64_alphabet[value & 0x3f];
input += 3;
input_len -= 3;
output += 4;
}
if (input_len == 1) {
uint32_t value = (uint32_t)input[0] << 16;
output[0] = base64_alphabet[(value >> 18) & 0x3f];
output[1] = base64_alphabet[(value >> 12) & 0x3f];
} else if (input_len == 2) {
uint32_t value = ((uint32_t)input[0] << 16) | ((uint32_t)input[1] << 8);
output[0] = base64_alphabet[(value >> 18) & 0x3f];
output[1] = base64_alphabet[(value >> 12) & 0x3f];
output[2] = base64_alphabet[(value >> 6) & 0x3f];
}
}
static int encode_d1(const unsigned* hashes, size_t count, unsigned bpp, char** result) {
if (!hashes || !count || !result || bpp < 10 || bpp > 32) return -EINVAL;
if (count > (SIZE_MAX - 7) / bpp) return -EOVERFLOW;
size_t bit_count = count * bpp;
size_t byte_count = (bit_count + 7) / 8;
unsigned char* bytes = calloc(byte_count, 1);
if (!bytes) return -ENOMEM;
unsigned char* output = bytes;
uint64_t bits = 0;
unsigned filled = 0;
for (size_t i = 0; i < count; ++i) {
bits |= (uint64_t)hashes[i] << filled;
filled += bpp;
while (filled >= 8) {
*output++ = (unsigned char)bits;
bits >>= 8;
filled -= 8;
}
}
if (filled) *output++ = (unsigned char)bits;
if ((size_t)(output - bytes) != byte_count) {
free(bytes);
return -EIO;
}
size_t payload_len = base64_encoded_size(byte_count);
if (payload_len == SIZE_MAX || payload_len > SIZE_MAX - D1_HEADER_LEN - 1) {
free(bytes);
return -EOVERFLOW;
}
char* encoded = malloc(D1_HEADER_LEN + payload_len + 1);
if (!encoded) {
free(bytes);
return -ENOMEM;
}
memcpy(encoded, D1_PREFIX, sizeof(D1_PREFIX) - 1);
encoded[6] = (char)('0' + bpp / 10);
encoded[7] = (char)('0' + bpp % 10);
base64_encode(bytes, byte_count, encoded + D1_HEADER_LEN);
encoded[D1_HEADER_LEN + payload_len] = '\0';
free(bytes);
*result = encoded;
return 0;
}
static int convert_set(const char* source, char** result, size_t* hash_count, unsigned* bpp) {
if (!source || strncmp(source, "set:", 4) != 0 || strncmp(source, D1_PREFIX, 6) == 0)
return -EINVAL;
unsigned* hashes = NULL;
size_t count = 0;
unsigned precision = 0;
int rc = arsv_set9_decode(source, &hashes, &count, &precision);
if (rc == 0) rc = encode_d1(hashes, count, precision, result);
free(hashes);
if (rc < 0) return rc;
if (hash_count) *hash_count = count;
if (bpp) *bpp = precision;
return 0;
}
#ifdef ARSV_WITH_RPM
struct statistics {
unsigned long headers;
unsigned long set_occurrences;
unsigned long set_bytes_old;
unsigned long set_bytes_new;
unsigned long hashes;
};
static int rewrite_version_tag(Header header, rpmTagVal tag, const char* package,
struct statistics* stats) {
struct rpmtd_s values;
memset(&values, 0, sizeof(values));
if (headerGet(header, tag, &values, HEADERGET_MINMEM) != 1) return 0;
if (rpmtdType(&values) != RPM_STRING_ARRAY_TYPE) {
rpmtdFreeData(&values);
fprintf(stderr, "%s: tag %d is not a string array\n", package, (int)tag);
return -EINVAL;
}
rpm_count_t count = rpmtdCount(&values);
const char** rewritten = calloc((size_t)count, sizeof(*rewritten));
if (!rewritten) {
rpmtdFreeData(&values);
return -ENOMEM;
}
int rc = 0;
int changed = 0;
rpmtdInit(&values);
for (rpm_count_t i = 0; i < count; ++i) {
const char* value = rpmtdNextString(&values);
if (!value) {
rc = -EINVAL;
break;
}
if (strncmp(value, "set:", 4) == 0) {
char* converted = NULL;
size_t hashes = 0;
unsigned bpp = 0;
rc = convert_set(value, &converted, &hashes, &bpp);
if (rc < 0) {
fprintf(stderr, "%s: cannot convert tag %d index %u (rc=%d)\n", package,
(int)tag, (unsigned)i, rc);
break;
}
rewritten[i] = converted;
changed = 1;
++stats->set_occurrences;
stats->set_bytes_old += strlen(value);
stats->set_bytes_new += strlen(converted);
stats->hashes += hashes;
} else {
rewritten[i] = strdup(value);
if (!rewritten[i]) {
rc = -ENOMEM;
break;
}
}
}
if (rc == 0 && changed) {
headerDel(header, tag);
if (!headerPutStringArray(header, tag, rewritten, count)) rc = -EIO;
}
for (rpm_count_t i = 0; i < count; ++i) free((void*)rewritten[i]);
free(rewritten);
rpmtdFreeData(&values);
return rc;
}
static int rewrite_pkglist(const char* input_path, const char* output_path, unsigned long limit) {
FD_t input = Fopen(input_path, "r.ufdio");
if (!input || Ferror(input)) {
fprintf(stderr, "%s: %s\n", input_path, input ? Fstrerror(input) : "cannot open");
return 1;
}
FD_t output = Fopen(output_path, "w.ufdio");
if (!output || Ferror(output)) {
fprintf(stderr, "%s: %s\n", output_path, output ? Fstrerror(output) : "cannot open");
Fclose(input);
return 1;
}
struct statistics stats = {0};
Header header;
int failed = 0;
const rpmTagVal tags[] = {
RPMTAG_REQUIREVERSION,
RPMTAG_PROVIDEVERSION,
RPMTAG_CONFLICTVERSION,
RPMTAG_OBSOLETEVERSION,
RPMTAG_RECOMMENDVERSION,
RPMTAG_SUGGESTVERSION,
RPMTAG_SUPPLEMENTVERSION,
RPMTAG_ENHANCEVERSION,
};
while ((!limit || stats.headers < limit) &&
(header = headerRead(input, HEADER_MAGIC_YES)) != NULL) {
const char* package = headerGetString(header, RPMTAG_NAME);
if (!package) package = "<unknown>";
for (size_t i = 0; i < sizeof(tags) / sizeof(tags[0]); ++i) {
int rc = rewrite_version_tag(header, tags[i], package, &stats);
if (rc < 0) {
failed = 1;
break;
}
}
if (!failed && headerWrite(output, header, HEADER_MAGIC_YES) != 0) {
fprintf(stderr, "%s: failed to write header for %s\n", output_path, package);
failed = 1;
}
headerFree(header);
if (failed) break;
++stats.headers;
}
if (!failed && Ferror(input)) {
fprintf(stderr, "%s: read error: %s\n", input_path, Fstrerror(input));
failed = 1;
}
if (Fclose(output) != 0) failed = 1;
Fclose(input);
if (failed) {
remove(output_path);
return 1;
}
fprintf(stderr,
"headers=%lu set_occurrences=%lu hashes=%lu old_set_bytes=%lu "
"new_set_bytes=%lu\n",
stats.headers, stats.set_occurrences, stats.hashes, stats.set_bytes_old,
stats.set_bytes_new);
return 0;
}
#endif
static void usage(const char* program) {
fprintf(stderr, "usage: %s --convert-set set:VALUE\n", program);
#ifdef ARSV_WITH_RPM
fprintf(stderr, " %s --rewrite INPUT OUTPUT [--max-headers N]\n", program);
#endif
}
int main(int argc, char** argv) {
if (argc == 3 && strcmp(argv[1], "--convert-set") == 0) {
char* converted = NULL;
int rc = convert_set(argv[2], &converted, NULL, NULL);
if (rc < 0) {
fprintf(stderr, "cannot convert set value (rc=%d)\n", rc);
return 1;
}
puts(converted);
free(converted);
return 0;
}
#ifdef ARSV_WITH_RPM
if ((argc == 4 || argc == 6) && strcmp(argv[1], "--rewrite") == 0) {
unsigned long limit = 0;
if (argc == 6) {
if (strcmp(argv[4], "--max-headers") != 0) {
usage(argv[0]);
return 2;
}
char* end = NULL;
errno = 0;
limit = strtoul(argv[5], &end, 10);
if (errno || !end || *end || !limit) {
fprintf(stderr, "invalid --max-headers value: %s\n", argv[5]);
return 2;
}
}
return rewrite_pkglist(argv[2], argv[3], limit);
}
#endif
usage(argv[0]);
return 2;
}
@@ -0,0 +1,350 @@
#!/usr/bin/env python3
"""Create D1 copies of the real Sisyphus x86_64 and noarch pkglist files."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[2]
DEFAULT_IMAGE = "registry.altlinux.org/sisyphus/alt:latest"
ARCHITECTURES = ("x86_64", "noarch")
SUMMARY_RE = re.compile(
r"headers=(?P<headers>\d+) set_occurrences=(?P<set_occurrences>\d+) "
r"hashes=(?P<hashes>\d+) old_set_bytes=(?P<old_set_bytes>\d+) "
r"new_set_bytes=(?P<new_set_bytes>\d+)"
)
VERSION_FORMAT = (
"[%{REQUIREVERSION}\\n]"
"[%{PROVIDEVERSION}\\n]"
"[%{CONFLICTVERSION}\\n]"
"[%{OBSOLETEVERSION}\\n]"
"[%{RECOMMENDVERSION}\\n]"
"[%{SUGGESTVERSION}\\n]"
"[%{SUPPLEMENTVERSION}\\n]"
"[%{ENHANCEVERSION}\\n]"
)
def classify_pkglist(path: Path) -> str | None:
name = path.name
if not name.endswith("_base_pkglist.classic"):
return None
if "_Sisyphus_x86%5f64_" in name or "_Sisyphus_x86_64_" in name:
return "x86_64"
if "_Sisyphus_noarch_" in name:
return "noarch"
return None
def validate_output(path: Path) -> Path:
resolved = path.expanduser().resolve()
home = Path.home().resolve()
source = ROOT.resolve()
if resolved == home or resolved == source or source in resolved.parents:
raise ValueError(f"refusing protected output path: {resolved}")
if resolved == Path("/"):
raise ValueError("refusing filesystem root as output")
return resolved
def run(
command: list[str],
*,
check: bool = True,
cwd: Path | None = None,
stdout: int | None = None,
) -> subprocess.CompletedProcess[bytes]:
result = subprocess.run(
command,
cwd=cwd,
stdout=stdout if stdout is not None else subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
if check and result.returncode != 0:
stderr = result.stderr.decode(errors="replace")
raise RuntimeError(f"command failed ({result.returncode}): {' '.join(command)}\n{stderr}")
return result
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def compile_converter(destination: Path) -> None:
include = destination.parent / "compat-include"
include.mkdir()
for name in ("rpmlib.h", "system.h", "set.h"):
(include / name).touch()
run(
[
"cc",
"-O2",
"-std=gnu11",
"-Wall",
"-Wextra",
"-Werror",
"-D_GNU_SOURCE",
"-DARSV_SET9_EXPORT",
"-DARSV_WITH_RPM",
"-I",
str(include),
"-include",
str(ROOT / "scripts/rpmsetcmp/newset_compat.h"),
str(ROOT / "reimplement/set9.c"),
str(HERE / "rewrite_sisyphus_pkglist.c"),
"-lrpm",
"-lrpmio",
"-o",
str(destination),
]
)
def find_pkglists(lists_dir: Path) -> dict[str, Path]:
found: dict[str, Path] = {}
for path in lists_dir.glob("*_base_pkglist.classic"):
architecture = classify_pkglist(path)
if architecture is None:
continue
if architecture in found:
raise RuntimeError(f"multiple Sisyphus {architecture} pkglist files")
found[architecture] = path
missing = set(ARCHITECTURES) - set(found)
if missing:
raise RuntimeError(f"missing Sisyphus pkglist for: {', '.join(sorted(missing))}")
return found
def query_header_count(path: Path) -> int:
process = subprocess.Popen(
["pkglist-query", "%{NAME}\\n", str(path)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert process.stdout is not None
count = sum(1 for _ in process.stdout)
stderr = process.stderr.read() if process.stderr else b""
status = process.wait()
if status != 0:
raise RuntimeError(f"pkglist-query failed for {path}: {stderr.decode(errors='replace')}")
return count
def query_set_counts(path: Path) -> dict[str, int]:
process = subprocess.Popen(
["pkglist-query", VERSION_FORMAT, str(path)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert process.stdout is not None
old = direct = 0
for raw_line in process.stdout:
value = raw_line.strip()
if value.startswith(b"set:D1"):
direct += 1
elif value.startswith(b"set:"):
old += 1
stderr = process.stderr.read() if process.stderr else b""
status = process.wait()
if status != 0:
raise RuntimeError(f"pkglist-query failed for {path}: {stderr.decode(errors='replace')}")
return {"set9": old, "d1": direct}
def conversion_summary(stderr: bytes) -> dict[str, int]:
text = stderr.decode(errors="replace")
match = SUMMARY_RE.search(text)
if not match:
raise RuntimeError(f"converter did not report summary:\n{text}")
return {name: int(value) for name, value in match.groupdict().items()}
def convert_one(converter: Path, source: Path, destination: Path) -> dict[str, object]:
result = run([str(converter), "--rewrite", str(source), str(destination)])
summary = conversion_summary(result.stderr)
source_headers = query_header_count(source)
output_headers = query_header_count(destination)
source_sets = query_set_counts(source)
output_sets = query_set_counts(destination)
if source_headers != output_headers or output_headers != summary["headers"]:
raise RuntimeError(
f"header count mismatch: source={source_headers} output={output_headers} "
f"converter={summary['headers']}"
)
if source_sets["d1"] != 0:
raise RuntimeError(f"source already contains {source_sets['d1']} D1 values")
if output_sets["set9"] != 0:
raise RuntimeError(f"output still contains {output_sets['set9']} set9 values")
if source_sets["set9"] != output_sets["d1"] or output_sets["d1"] != summary["set_occurrences"]:
raise RuntimeError(
"set occurrence mismatch: "
f"source={source_sets['set9']} output={output_sets['d1']} "
f"converter={summary['set_occurrences']}"
)
if summary["set_occurrences"] == 0:
raise RuntimeError("source pkglist contains no set versions")
return {
**summary,
"source": {
"path": source.name,
"size": source.stat().st_size,
"sha256": sha256_file(source),
},
"output": {
"path": destination.name,
"size": destination.stat().st_size,
"sha256": sha256_file(destination),
},
}
def image_metadata(image: str) -> dict[str, str]:
result = run(
[
"podman",
"image",
"inspect",
image,
"--format",
"{{.Digest}}|{{.Id}}",
]
)
digest, image_id = result.stdout.decode().strip().split("|", 1)
return {"name": image, "digest": digest, "id": image_id}
def inner(output: Path, image: str, image_digest: str, image_id: str) -> None:
output.mkdir(parents=True, exist_ok=True)
if any(output.iterdir()):
raise RuntimeError(f"output directory is not empty: {output}")
pkglists = find_pkglists(Path("/var/lib/apt/lists"))
staging = Path(tempfile.mkdtemp(prefix=".d1-staging-", dir=output))
try:
converter = staging / "rewrite-sisyphus-pkglist"
compile_converter(converter)
architectures: dict[str, object] = {}
for architecture in ARCHITECTURES:
destination = staging / f"Sisyphus.{architecture}.pkglist.classic"
architectures[architecture] = convert_one(
converter, pkglists[architecture], destination
)
converter.unlink()
shutil.rmtree(staging / "compat-include")
manifest = {
"format": 1,
"created_at": datetime.now(timezone.utc).isoformat(),
"image": {"name": image, "digest": image_digest, "id": image_id},
"rpm": run(["rpm", "--version"]).stdout.decode().strip(),
"apt": run(["rpmquery", "--qf", "%{VERSION}-%{RELEASE}", "apt"])
.stdout.decode()
.strip(),
"converter_source": {
"set9_sha256": sha256_file(ROOT / "reimplement/set9.c"),
"rewrite_sha256": sha256_file(HERE / "rewrite_sisyphus_pkglist.c"),
},
"architectures": architectures,
}
(staging / "manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n"
)
final = output / "d1-pkglists"
staging.replace(final)
print(final)
for architecture in ARCHITECTURES:
data = architectures[architecture]
assert isinstance(data, dict)
print(
f"{architecture}: headers={data['headers']} "
f"set_occurrences={data['set_occurrences']} "
f"output={final / f'Sisyphus.{architecture}.pkglist.classic'}"
)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
def outer(output: Path, image: str) -> None:
if output.exists() and any(output.iterdir()):
raise RuntimeError(f"output directory is not empty: {output}")
output.mkdir(parents=True, exist_ok=True)
run(["podman", "pull", image], stdout=None)
metadata = image_metadata(image)
inner_command = [
"python3",
"/src/new_version/direct_hash/apt_benchmark/run_sisyphus_pkglist.py",
"--inner",
"/out",
"--image",
image,
"--image-digest",
metadata["digest"],
"--image-id",
metadata["id"],
]
command = [
"podman",
"run",
"--rm",
"-v",
f"{ROOT}:/src:ro",
"-v",
f"{output}:/out:rw",
image,
"sh",
"-lc",
(
"apt-get update >/dev/null && "
"apt-get install -y gcc librpm-devel apt-repo-tools python3 >/dev/null && "
+ " ".join(shlex.quote(argument) for argument in inner_command)
),
]
result = subprocess.run(command, check=False)
if result.returncode != 0:
raise RuntimeError(f"container conversion failed with status {result.returncode}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Create D1 copies of current Sisyphus x86_64/noarch pkglist files"
)
parser.add_argument("output", type=Path, help="new or empty output directory")
parser.add_argument("--image", default=DEFAULT_IMAGE)
parser.add_argument("--image-digest", default="", help=argparse.SUPPRESS)
parser.add_argument("--image-id", default="", help=argparse.SUPPRESS)
parser.add_argument("--inner", action="store_true", help=argparse.SUPPRESS)
return parser.parse_args()
def main() -> int:
args = parse_args()
try:
output = validate_output(args.output)
if args.inner:
inner(output, args.image, args.image_digest, args.image_id)
else:
outer(output, args.image)
except (OSError, RuntimeError, ValueError) as error:
print(f"error: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+31
View File
@@ -546,6 +546,37 @@ static int decode_set(const struct set_meta* meta, unsigned* hash_arr) {
} }
} }
#ifdef ARSV_SET9_EXPORT
/* Test-only bridge used to translate existing repository metadata without
* reconstructing unavailable symbol names. The caller owns *hashes. */
int arsv_set9_decode(const char* source, unsigned** hashes, size_t* count, unsigned* bpp) {
if (!source || !hashes || !count || !bpp) return -EINVAL;
const char* str = source;
if (strncmp(str, "set:", 4) == 0) str += 4;
struct set_meta meta;
int rc = set_meta_init(str, &meta);
if (rc < 0) return rc;
rc = set_meta_fini(&meta);
if (rc < 0) return rc;
unsigned* values = malloc((size_t)meta.value_capacity * sizeof(*values));
if (!values) return -ENOMEM;
int decoded = decode_set(&meta, values);
if (decoded <= 0) {
free(values);
return decoded < 0 ? decoded : -EINVAL;
}
*hashes = values;
*count = (size_t)decoded;
*bpp = (unsigned)meta.bpp;
return 0;
}
#endif
/* Bounded decoded-set cache: bucketed lookup plus O(1) LRU updates. */ /* Bounded decoded-set cache: bucketed lookup plus O(1) LRU updates. */
static int downsample_set(const unsigned* hash_pt, size_t hash_cnt, unsigned* dest_pt, static int downsample_set(const unsigned* hash_pt, size_t hash_cnt, unsigned* dest_pt,
int target_bpp); int target_bpp);
+585
View File
@@ -0,0 +1,585 @@
#!/bin/bash
set -euo pipefail
export LC_ALL=C
# A/B benchmark:
# set9: librpm из Sisyphus с reimplement/set9.c и исходными pkglist Sisyphus;
# d1: librpm из того же commit с direct_hash/hash_set.c и теми же pkglist,
# заранее преобразованными run_sisyphus_pkglist.py в формат set:D1.
#
# Все измеряемые команды работают без сети. Конвертация, сборка и gencaches
# выполняются до таймера. Порядок замеров в каждом раунде: set9/d1/d1/set9.
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd)
SET9_C=${SET9_C:-$REPO_ROOT/reimplement/set9.c}
D1_C=${D1_C:-$REPO_ROOT/new_version/direct_hash/hash_set.c}
PKGLIST_CONVERTER=${PKGLIST_CONVERTER:-$REPO_ROOT/new_version/direct_hash/apt_benchmark/run_sisyphus_pkglist.py}
WORK_ROOT=${WORK_ROOT:-$HOME/sisyphus-set9-d1-bench}
RESULT_DIR=${RESULT_DIR:-$WORK_ROOT/results}
IMAGE=${IMAGE:-registry.altlinux.org/sisyphus/alt:latest}
RPM_GIT=${RPM_GIT:-https://git.altlinux.org/gears/r/rpm.git}
RPM_BRANCH=${RPM_BRANCH:-sisyphus}
PACKAGER=${PACKAGER:-krosh <gudovdo@my.msu.ru>}
CPU=${CPU:-0}
ROUNDS=${ROUNDS:-2}
RESET_WORK=${RESET_WORK:-1} # 0 — продолжить подготовку/сборки, 1 — начать заново.
OPERATIONS=${OPERATIONS:-unmet check autoremove install-rpm-build install-openuds-server install-password-store upgrade dist-upgrade}
APT_GET=${APT_GET:-/usr/lib/apt/apt-get}
APT_CACHE=${APT_CACHE:-$(command -v apt-cache 2>/dev/null || true)}
usage()
{
cat <<'EOF'
Usage: scripts/run-sisyphus-set9-d1-bench.sh
The script has no positional arguments. Configuration is passed through env:
CPU=2 ROUNDS=3 RESET_WORK=1 ./scripts/run-sisyphus-set9-d1-bench.sh
RESET_WORK=0 OPERATIONS='unmet check' ./scripts/run-sisyphus-set9-d1-bench.sh
Main variables:
WORK_ROOT, RESULT_DIR, IMAGE, RPM_GIT, RPM_BRANCH, PACKAGER,
SET9_C, D1_C, PKGLIST_CONVERTER, CPU, ROUNDS, RESET_WORK, OPERATIONS.
RESET_WORK=1 removes WORK_ROOT after running hsh --cleanup-only for old hasher
workdirs. Timed runs never update repositories and never install packages.
EOF
}
fail()
{
printf 'error: %s\n' "$*" >&2
exit 1
}
safe_remove_work_root()
{
[[ -n $WORK_ROOT && $WORK_ROOT == /* && $WORK_ROOT != / && $WORK_ROOT != "$HOME" ]] ||
fail "unsafe WORK_ROOT for removal: $WORK_ROOT"
[[ $WORK_ROOT != "$REPO_ROOT" && $REPO_ROOT != "$WORK_ROOT/"* ]] ||
fail "WORK_ROOT must not contain the source repository: $WORK_ROOT"
}
prepare_spec()
{
local spec=$1 suffix=$2 version release new_release date
version=$(sed -n 's/^Version:[[:space:]]*//p' "$spec" | sed -n '1p')
release=$(sed -n 's/^Release:[[:space:]]*//p' "$spec" | sed -n '1p')
[[ -n $version && -n $release ]] || fail "cannot read Version/Release from $spec"
new_release="$release.$suffix"
sed -i "0,/^Release:[[:space:]]*$release$/s//Release: $new_release/" "$spec"
date=$(date '+%a %b %d %Y')
sed -i "/^%changelog/a\\
* $date $PACKAGER $version-$new_release\\
- Local Sisyphus set format benchmark build.\\
" "$spec"
}
apt_options()
{
local variant=$1
APT_OPTIONS=(
-o 'Dir::Etc::main=-'
-o 'Dir::Etc::parts=-'
-o "Dir::Etc::sourcelist=$SNAPSHOT/etc-apt/sources.list"
-o "Dir::Etc::sourceparts=$SNAPSHOT/etc-apt/sources.list.d"
-o 'Dir::Etc::preferences=-'
-o 'Dir::Etc::preferencesparts=-'
-o "Dir::State::lists=$variant/apt/lists/"
-o "Dir::State::status=$COMMON/status"
-o "Dir::Cache=$variant/apt/cache/"
-o "Dir::Cache::archives=$variant/apt/cache/archives"
-o "Dir::Cache::pkgcache=$variant/apt/cache/pkgcache.bin"
-o "Dir::Cache::srcpkgcache=$variant/apt/cache/srcpkgcache.bin"
-o "RPM::RootDir=$COMMON/root"
)
}
operation_command()
{
local operation=$1 variant=$2
apt_options "$variant"
case $operation in
unmet)
COMMAND=("$APT_CACHE" -q "${APT_OPTIONS[@]}" unmet)
;;
check)
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s check)
;;
autoremove)
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s autoremove)
;;
install-rpm-build)
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s install rpm-build)
;;
install-openuds-server)
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s install openuds-server)
;;
install-password-store)
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s install password-store)
;;
upgrade)
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -o APT::Get::EnableUpgrade=true -s upgrade)
;;
dist-upgrade)
COMMAND=("$APT_GET" -qq "${APT_OPTIONS[@]}" -s dist-upgrade)
;;
*)
fail "unknown operation: $operation"
;;
esac
}
variant_dir()
{
case $1 in
set9) printf '%s' "$SET9_VARIANT" ;;
d1) printf '%s' "$D1_VARIANT" ;;
*) fail "unknown variant: $1" ;;
esac
}
variant_libdir()
{
local variant
variant=$(variant_dir "$1")
printf '%s' "$variant/lib/usr/lib64"
}
run_command()
{
local operation=$1 variant_name=$2 stdout=$3 stderr=$4
local variant libdir status
variant=$(variant_dir "$variant_name")
libdir=$(variant_libdir "$variant_name")
operation_command "$operation" "$variant"
if env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" LD_LIBRARY_PATH="$libdir" \
taskset -c "$CPU" "${COMMAND[@]}" >"$stdout" 2>"$stderr"; then
status=0
else
status=$?
fi
printf '%s' "$status"
}
run_once()
{
local operation=$1 variant_name=$2 sequence=$3 sample=$4
local variant raw stdout stderr start end status elapsed
local stdout_sha stderr_sha stdout_bytes stderr_bytes
variant=$(variant_dir "$variant_name")
raw="$RESULT_DIR/raw/$operation/$variant_name"
mkdir -p "$raw"
stdout="$raw/$sample.stdout"
stderr="$raw/$sample.stderr"
start=$(date +%s%N)
status=$(run_command "$operation" "$variant_name" "$stdout" "$stderr")
end=$(date +%s%N)
elapsed=$(awk -v start="$start" -v end="$end" \
'BEGIN { printf "%.6f", (end - start) / 1000000000 }')
stdout_sha=$(sha256sum "$stdout" | awk '{print $1}')
stderr_sha=$(sha256sum "$stderr" | awk '{print $1}')
stdout_bytes=$(stat -c %s "$stdout")
stderr_bytes=$(stat -c %s "$stderr")
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$operation" "$sequence" "$variant_name" "$sample" "$elapsed" "$status" \
"$stdout_sha" "$stdout_bytes" "$stderr_sha" "$stderr_bytes" >>"$RAW_RESULTS"
printf '%-28s sequence=%-2s %-4s sample=%-2s %ss status=%s\n' \
"$operation" "$sequence" "$variant_name" "$sample" "$elapsed" "$status"
}
prepare_snapshot()
{
local image_digest image_id converter_rel
[[ -f $SNAPSHOT/.complete ]] && return
case $PKGLIST_CONVERTER in
"$REPO_ROOT"/*) converter_rel=${PKGLIST_CONVERTER#"$REPO_ROOT/"} ;;
*) fail "PKGLIST_CONVERTER must be inside REPO_ROOT: $PKGLIST_CONVERTER" ;;
esac
printf '\n===== Preparing one immutable Sisyphus metadata snapshot =====\n'
rm -rf "$SNAPSHOT"
mkdir -p "$SNAPSHOT/container-output"
podman pull "$IMAGE"
image_digest=$(podman image inspect "$IMAGE" --format '{{.Digest}}')
image_id=$(podman image inspect "$IMAGE" --format '{{.Id}}')
podman run --rm \
-e "ARSV_IMAGE=$IMAGE" \
-e "ARSV_IMAGE_DIGEST=$image_digest" \
-e "ARSV_IMAGE_ID=$image_id" \
-e "ARSV_CONVERTER_REL=$converter_rel" \
-v "$REPO_ROOT:/src:ro" \
-v "$SNAPSHOT/container-output:/out:rw" \
"$IMAGE" sh -euc '
apt-get update >/dev/null
apt-get install -y gcc librpm-devel apt-repo-tools python3 >/dev/null
python3 "/src/$ARSV_CONVERTER_REL" \
--inner /out/conversion \
--image "$ARSV_IMAGE" \
--image-digest "$ARSV_IMAGE_DIGEST" \
--image-id "$ARSV_IMAGE_ID"
cp -a /var/lib/apt/lists /out/original-lists
cp -a /etc/apt /out/etc-apt
'
mv "$SNAPSHOT/container-output/original-lists" "$SNAPSHOT/lists"
mv "$SNAPSHOT/container-output/etc-apt" "$SNAPSHOT/etc-apt"
mv "$SNAPSHOT/container-output/conversion/d1-pkglists/manifest.json" "$SNAPSHOT/manifest.json"
mkdir -p "$SNAPSHOT/d1-pkglists"
mv "$SNAPSHOT/container-output/conversion/d1-pkglists/"*.classic "$SNAPSHOT/d1-pkglists/"
rm -rf "$SNAPSHOT/container-output"
rm -f "$SNAPSHOT/lists/lock"
rm -rf "$SNAPSHOT/lists/partial"
mkdir -p "$SNAPSHOT/lists/partial" "$SNAPSHOT/etc-apt/sources.list.d"
[[ -e $SNAPSHOT/etc-apt/sources.list ]] || : >"$SNAPSHOT/etc-apt/sources.list"
: >"$SNAPSHOT/.complete"
}
prepare_variant_lists()
{
local variant=$1 format=$2
rm -rf "$variant/apt"
mkdir -p "$variant/apt/lists" "$variant/apt/cache/archives/partial"
cp -a "$SNAPSHOT/lists/." "$variant/apt/lists/"
if [[ $format == d1 ]]; then
python3 - "$SNAPSHOT/manifest.json" "$SNAPSHOT/lists" \
"$SNAPSHOT/d1-pkglists" "$variant/apt/lists" <<'PY'
import hashlib
import json
import shutil
import sys
from pathlib import Path
manifest_path, original_dir, d1_dir, target_dir = map(Path, sys.argv[1:])
manifest = json.loads(manifest_path.read_text())
for architecture, data in manifest["architectures"].items():
source = original_dir / data["source"]["path"]
converted = d1_dir / data["output"]["path"]
target = target_dir / data["source"]["path"]
if hashlib.sha256(source.read_bytes()).hexdigest() != data["source"]["sha256"]:
raise SystemExit(f"source checksum mismatch for {architecture}: {source}")
if hashlib.sha256(converted.read_bytes()).hexdigest() != data["output"]["sha256"]:
raise SystemExit(f"D1 checksum mismatch for {architecture}: {converted}")
shutil.copyfile(converted, target)
PY
fi
}
build_variant()
{
local name=$1 source_c=$2 suffix=$3 variant source hasher repo spec
local -a librpm_rpms
variant=$(variant_dir "$name")
source="$variant/src/rpm"
hasher="$variant/hasher"
repo="$hasher/repo/x86_64/RPMS.hasher"
spec="$source/alt/rpm.spec"
printf '\n===== Building %s with %s =====\n' "$name" "$source_c"
mkdir -p "$variant/src" "$variant/logs" "$hasher"
if [[ ! -d $source/.git ]]; then
git clone --local "$SOURCE_BASE" "$source"
cp "$source_c" "$source/lib/set.c"
prepare_spec "$spec" "$suffix"
else
cmp -s "$source_c" "$source/lib/set.c" ||
fail "$source_c changed; use RESET_WORK=1"
fi
shopt -s nullglob
librpm_rpms=("$repo"/librpm7-[0-9]*".$suffix".x86_64.rpm)
if ((${#librpm_rpms[@]} == 0)); then
(
cd "$source"
gear-hsh \
--commit \
--with-stuff \
--packager="$PACKAGER" \
--no-sisyphus-check=changelog,packager,gpg \
--mountpoints=/proc \
--workdir="$hasher" \
--target=x86_64
) 2>&1 | tee "$variant/logs/rpm.log"
else
printf 'already built: %s\n' "${librpm_rpms[0]}"
fi
librpm_rpms=("$repo"/librpm7-[0-9]*".$suffix".x86_64.rpm)
((${#librpm_rpms[@]} == 1)) ||
fail "expected one librpm7 package for $name, got ${#librpm_rpms[@]}"
rm -rf "$variant/lib"
mkdir -p "$variant/lib"
(
cd "$variant/lib"
rpm2cpio "${librpm_rpms[0]}" | cpio -idm --quiet
)
[[ -e $variant/lib/usr/lib64/librpm.so.7 && -e $variant/lib/usr/lib64/librpmio.so.7 ]] ||
fail "librpm libraries were not extracted for $name"
for apt_binary in "$APT_GET" "$APT_CACHE"; do
LD_LIBRARY_PATH="$variant/lib/usr/lib64" ldd "$apt_binary" |
awk -v expected="$variant/lib/usr/lib64/librpm.so.7" '
index($0, expected) { found = 1 }
END { exit found ? 0 : 1 }
' || fail "$apt_binary does not load the built librpm for $name"
done
}
build_cache()
{
local name=$1 variant libdir start end elapsed stdout stderr status
variant=$(variant_dir "$name")
libdir=$(variant_libdir "$name")
apt_options "$variant"
stdout="$variant/logs/gencaches.stdout"
stderr="$variant/logs/gencaches.stderr"
rm -f "$variant/apt/cache/pkgcache.bin" "$variant/apt/cache/srcpkgcache.bin"
start=$(date +%s%N)
if env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" LD_LIBRARY_PATH="$libdir" \
"$APT_CACHE" -q "${APT_OPTIONS[@]}" gencaches >"$stdout" 2>"$stderr"; then
status=0
else
status=$?
fi
end=$(date +%s%N)
elapsed=$(awk -v start="$start" -v end="$end" \
'BEGIN { printf "%.6f", (end - start) / 1000000000 }')
printf '%s\t%s\t%s\n' "$name" "$elapsed" "$status" >>"$CACHE_RESULTS"
[[ $status -eq 0 && -s $variant/apt/cache/pkgcache.bin ]] ||
fail "gencaches failed for $name; see $stderr"
}
record_cache_checksums()
{
local name variant
: >"$RESULT_DIR/cache-files.before.sha256"
for name in set9 d1; do
variant=$(variant_dir "$name")
sha256sum "$variant/apt/cache/pkgcache.bin" \
"$variant/apt/cache/srcpkgcache.bin" >>"$RESULT_DIR/cache-files.before.sha256"
done
}
verify_cache_checksums()
{
local name variant
: >"$RESULT_DIR/cache-files.after.sha256"
for name in set9 d1; do
variant=$(variant_dir "$name")
sha256sum "$variant/apt/cache/pkgcache.bin" \
"$variant/apt/cache/srcpkgcache.bin" >>"$RESULT_DIR/cache-files.after.sha256"
done
cmp -s "$RESULT_DIR/cache-files.before.sha256" \
"$RESULT_DIR/cache-files.after.sha256" ||
fail "APT cache changed during timed runs"
}
write_provenance()
{
{
printf 'rpm_git=%s\n' "$RPM_GIT"
printf 'rpm_branch=%s\n' "$RPM_BRANCH"
printf 'rpm_commit=%s\n' "$(git -C "$SOURCE_BASE" rev-parse HEAD)"
printf 'image=%s\n' "$IMAGE"
printf 'image_digest=%s\n' "$(podman image inspect "$IMAGE" --format '{{.Digest}}')"
printf 'image_id=%s\n' "$(podman image inspect "$IMAGE" --format '{{.Id}}')"
printf 'apt=%s\n' "$(rpmquery --qf '%{VERSION}-%{RELEASE}' apt)"
printf 'rpm=%s\n' "$(rpm --version)"
printf 'cpu=%s\n' "$CPU"
printf 'rounds=%s\n' "$ROUNDS"
printf 'operations=%s\n' "$OPERATIONS"
sha256sum "$SET9_C" "$D1_C" "$PKGLIST_CONVERTER" \
"$REPO_ROOT/new_version/direct_hash/apt_benchmark/rewrite_sisyphus_pkglist.c"
} >"$RESULT_DIR/provenance.txt"
cp "$SNAPSHOT/manifest.json" "$RESULT_DIR/pkglist-manifest.json"
}
summarize_results()
{
python3 - "$RAW_RESULTS" "$RESULT_DIR/summary.tsv" "$RESULT_DIR/summary.md" <<'PY'
import csv
import statistics
import sys
from collections import defaultdict
from pathlib import Path
raw_path, tsv_path, markdown_path = map(Path, sys.argv[1:])
with raw_path.open(newline="") as stream:
rows = list(csv.DictReader(stream, delimiter="\t"))
groups = defaultdict(list)
for row in rows:
groups[row["operation"]].append(row)
summary = []
all_equivalent = True
for operation, items in groups.items():
by_variant = defaultdict(list)
signatures = set()
for item in items:
by_variant[item["variant"]].append(float(item["seconds"]))
signatures.add(
(
item["status"],
item["stdout_sha256"],
item["stdout_bytes"],
item["stderr_sha256"],
item["stderr_bytes"],
)
)
if set(by_variant) != {"set9", "d1"}:
raise SystemExit(f"missing variant samples for {operation}")
set9 = statistics.median(by_variant["set9"])
d1 = statistics.median(by_variant["d1"])
equivalent = len(signatures) == 1
all_equivalent &= equivalent
summary.append(
(operation, len(by_variant["set9"]), set9, d1, d1 / set9, equivalent)
)
with tsv_path.open("w", newline="") as stream:
writer = csv.writer(stream, delimiter="\t", lineterminator="\n")
writer.writerow(
["operation", "runs_per_variant", "set9_median_seconds", "d1_median_seconds", "d1/set9", "outputs_equal"]
)
for operation, count, set9, d1, ratio, equivalent in summary:
writer.writerow(
[operation, count, f"{set9:.6f}", f"{d1:.6f}", f"{ratio:.4f}", "yes" if equivalent else "NO"]
)
lines = [
"# Sisyphus set9 vs D1 benchmark",
"",
"Timed scope: resolver commands only; conversion, builds and `gencaches` are excluded.",
"Each round uses the balanced order `set9 / d1 / d1 / set9` after one warm-up per variant.",
"",
"| operation | runs/variant | set9 median, s | D1 median, s | D1/set9 | output equivalence |",
"|---|---:|---:|---:|---:|:---:|",
]
for operation, count, set9, d1, ratio, equivalent in summary:
lines.append(
f"| `{operation}` | {count} | {set9:.6f} | {d1:.6f} | {ratio:.4f} | {'yes' if equivalent else '**NO**'} |"
)
lines.extend(
[
"",
"`D1/set9 < 1` means that the D1 variant was faster.",
"Output equivalence includes exit status plus exact stdout and stderr hashes for every repeat and both variants.",
]
)
markdown_path.write_text("\n".join(lines) + "\n")
if not all_equivalent:
raise SystemExit(2)
PY
}
if [[ ${1:-} == --help || ${1:-} == -h ]]; then
usage
exit 0
fi
(($# == 0)) || fail "unexpected arguments; use --help"
for command in git podman gear-hsh hsh rpm rpmquery rpm2cpio cpio taskset awk sed \
date sha256sum stat ldd python3 cmp cp mv tee; do
command -v "$command" >/dev/null || fail "required command not found: $command"
done
[[ -n $APT_CACHE && -x $APT_CACHE ]] || fail "apt-cache not found: $APT_CACHE"
[[ -x $APT_GET ]] || fail "apt-get executable not found: $APT_GET"
[[ -f $SET9_C ]] || fail "set9 source not found: $SET9_C"
[[ -f $D1_C ]] || fail "D1 source not found: $D1_C"
[[ -f $PKGLIST_CONVERTER ]] || fail "pkglist converter not found: $PKGLIST_CONVERTER"
[[ $ROUNDS =~ ^[1-9][0-9]*$ ]] || fail "ROUNDS must be a positive integer"
taskset -c "$CPU" true >/dev/null 2>&1 || fail "CPU $CPU is unavailable to taskset"
read -r -a OPERATION_LIST <<<"$OPERATIONS"
((${#OPERATION_LIST[@]} > 0)) || fail "OPERATIONS is empty"
for operation in "${OPERATION_LIST[@]}"; do
case $operation in
unmet|check|autoremove|install-rpm-build|install-openuds-server|install-password-store|upgrade|dist-upgrade) ;;
*) fail "unknown operation in OPERATIONS: $operation" ;;
esac
done
COMMON="$WORK_ROOT/common"
SNAPSHOT="$COMMON/snapshot"
SOURCE_BASE="$WORK_ROOT/src/rpm-base"
SET9_VARIANT="$WORK_ROOT/variants/set9"
D1_VARIANT="$WORK_ROOT/variants/d1"
if ((RESET_WORK)); then
safe_remove_work_root
for old_hasher in "$SET9_VARIANT/hasher" "$D1_VARIANT/hasher"; do
[[ -d $old_hasher ]] || continue
hsh --cleanup-only --workdir="$old_hasher" >/dev/null 2>&1 || true
done
rm -rf "$WORK_ROOT"
fi
mkdir -p "$WORK_ROOT/src" "$WORK_ROOT/variants" "$COMMON/root/var/lib/rpm" "$RESULT_DIR"
: >"$COMMON/apt.conf"
: >"$COMMON/status"
prepare_snapshot
if [[ ! -d $SOURCE_BASE/.git ]]; then
git clone --branch "$RPM_BRANCH" --single-branch "$RPM_GIT" "$SOURCE_BASE"
fi
build_variant set9 "$SET9_C" arsvset9
build_variant d1 "$D1_C" arsvd1
prepare_variant_lists "$SET9_VARIANT" set9
prepare_variant_lists "$D1_VARIANT" d1
mkdir -p "$RESULT_DIR/raw"
CACHE_RESULTS="$RESULT_DIR/cache-build.tsv"
printf 'variant\tseconds\texit_status\n' >"$CACHE_RESULTS"
build_cache set9
build_cache d1
record_cache_checksums
write_provenance
RAW_RESULTS="$RESULT_DIR/raw.tsv"
printf 'operation\tsequence\tvariant\tsample\tseconds\tstatus\tstdout_sha256\tstdout_bytes\tstderr_sha256\tstderr_bytes\n' >"$RAW_RESULTS"
declare -A SAMPLE_COUNTS=([set9]=0 [d1]=0)
sequence=0
for operation in "${OPERATION_LIST[@]}"; do
printf '\n===== Warm-up: %s =====\n' "$operation"
mkdir -p "$RESULT_DIR/warmup/$operation"
for variant_name in set9 d1; do
warm_status=$(run_command "$operation" "$variant_name" \
"$RESULT_DIR/warmup/$operation/$variant_name.stdout" \
"$RESULT_DIR/warmup/$operation/$variant_name.stderr")
printf '%s\n' "$warm_status" >"$RESULT_DIR/warmup/$operation/$variant_name.status"
done
printf '===== Timed ABBA: %s =====\n' "$operation"
for ((round = 1; round <= ROUNDS; ++round)); do
for variant_name in set9 d1 d1 set9; do
sequence=$((sequence + 1))
SAMPLE_COUNTS[$variant_name]=$((SAMPLE_COUNTS[$variant_name] + 1))
run_once "$operation" "$variant_name" "$sequence" "${SAMPLE_COUNTS[$variant_name]}"
done
done
SAMPLE_COUNTS[set9]=0
SAMPLE_COUNTS[d1]=0
done
verify_cache_checksums
if ! summarize_results; then
fail "variant/repeat outputs differ; inspect $RESULT_DIR/raw and $RESULT_DIR/summary.md"
fi
printf '\nDone. Results:\n'
printf ' %s\n' "$RESULT_DIR/summary.md" "$RESULT_DIR/summary.tsv" \
"$RESULT_DIR/raw.tsv" "$RESULT_DIR/cache-build.tsv" "$RESULT_DIR/provenance.txt" \
"$RESULT_DIR/pkglist-manifest.json"