229 lines
8.2 KiB
Python
229 lines
8.2 KiB
Python
"""Isolated synthetic archive benchmark; never reads the configured Files store.
|
|
|
|
Run from this repository: python tests/benchmark_archive_storage.py.
|
|
Uses the existing temporary SQLite/local-storage recovery fixture so durable
|
|
ledger, checksum verification, commit settlement and compensation stay enabled.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import cProfile
|
|
from contextlib import ExitStack
|
|
from collections import Counter
|
|
from io import BytesIO
|
|
import json
|
|
from pathlib import Path
|
|
import pstats
|
|
import random
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
from unittest.mock import patch
|
|
import zipfile
|
|
|
|
import pyzipper
|
|
from sqlalchemy import event
|
|
|
|
from govoplan_files.backend.change_tracking import register_files_change_tracking
|
|
from govoplan_files.backend.storage.archives import (
|
|
extract_archive_upload,
|
|
_read_selected_zip_members,
|
|
)
|
|
from govoplan_files.backend.storage.native_zip import native_zip_library
|
|
from test_storage_recovery import StorageRecoveryTests
|
|
|
|
|
|
def storage_probe(
|
|
member_count: int,
|
|
member_size: int,
|
|
profile: bool,
|
|
baseline_revision: str | None = None,
|
|
):
|
|
case = StorageRecoveryTests()
|
|
case.setUp()
|
|
try:
|
|
register_files_change_tracking()
|
|
data = BytesIO()
|
|
rng = random.Random(20260907)
|
|
with zipfile.ZipFile(data, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
|
for index in range(member_count):
|
|
archive.writestr(
|
|
f"documents/{index:04d}.dat", rng.randbytes(member_size)
|
|
)
|
|
queries = []
|
|
event.listen(
|
|
case.engine,
|
|
"before_cursor_execute",
|
|
lambda conn, cursor, statement, parameters, context, executemany: (
|
|
queries.append(statement)
|
|
),
|
|
)
|
|
profiler = cProfile.Profile()
|
|
if profile:
|
|
profiler.enable()
|
|
with ExitStack() as overrides:
|
|
overrides.enter_context(
|
|
patch(
|
|
"govoplan_files.backend.storage.files.get_storage_backend",
|
|
return_value=case.backend,
|
|
)
|
|
)
|
|
if baseline_revision:
|
|
# Read an earlier local revision into this isolated process;
|
|
# never replace shared worktree files or connect to live data.
|
|
source = subprocess.run(
|
|
[
|
|
"git",
|
|
"show",
|
|
f"{baseline_revision}:src/govoplan_files/backend/storage/files.py",
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout
|
|
namespace = {"__name__": "govoplan_files.backend.storage.files"}
|
|
exec(
|
|
compile(source, f"{baseline_revision}:files.py", "exec"), namespace
|
|
)
|
|
namespace.update(
|
|
get_storage_backend=lambda: case.backend,
|
|
_storage_backend_name=lambda: case.backend.name,
|
|
_storage_bucket_name=lambda: "",
|
|
)
|
|
overrides.enter_context(
|
|
patch(
|
|
"govoplan_files.backend.storage.archives.create_file_asset",
|
|
namespace["create_file_asset"],
|
|
)
|
|
)
|
|
started = time.perf_counter()
|
|
result = extract_archive_upload(
|
|
case.session,
|
|
tenant_id="tenant-1",
|
|
owner_type="user",
|
|
owner_id="user-1",
|
|
user_id="user-1",
|
|
archive_data=data.getvalue(),
|
|
filename="synthetic.zip",
|
|
folder="imported",
|
|
campaign_id=None,
|
|
)
|
|
storage_done = time.perf_counter()
|
|
storage_queries = len(queries)
|
|
case.session.commit()
|
|
completed = time.perf_counter()
|
|
if profile:
|
|
profiler.disable()
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"probe": "durable-storage",
|
|
"storage_revision": baseline_revision or "working-tree",
|
|
"members": len(result),
|
|
"member_bytes": member_size,
|
|
"extract_store_seconds": round(storage_done - started, 4),
|
|
"settlement_seconds": round(completed - storage_done, 4),
|
|
"total_seconds": round(completed - started, 4),
|
|
"store_queries": storage_queries,
|
|
"total_queries": len(queries),
|
|
"queries_by_table": dict(
|
|
Counter(
|
|
(
|
|
match.group(1)
|
|
if (
|
|
match := re.search(
|
|
r"(?:FROM|INTO|UPDATE)\s+([a-z_]+)", statement
|
|
)
|
|
)
|
|
else "other"
|
|
)
|
|
for statement in queries
|
|
)
|
|
),
|
|
}
|
|
)
|
|
)
|
|
if profile:
|
|
pstats.Stats(profiler).strip_dirs().sort_stats("cumulative").print_stats(24)
|
|
finally:
|
|
case.doCleanups()
|
|
|
|
|
|
def zipcrypto_probe(size: int):
|
|
binary = shutil.which("zip")
|
|
if not binary:
|
|
print(
|
|
json.dumps(
|
|
{"probe": "zipcrypto", "skipped": "fixture ZIP writer unavailable"}
|
|
)
|
|
)
|
|
return
|
|
# Public fixture password only: never pass a user credential to a process.
|
|
with tempfile.TemporaryDirectory(prefix="files-synthetic-zipcrypto-") as temporary:
|
|
path = Path(temporary)
|
|
content = random.Random(20260907).randbytes(size)
|
|
(path / "synthetic.bin").write_bytes(content)
|
|
subprocess.run(
|
|
[binary, "-q", "-P", "fixture-only", "synthetic.zip", "synthetic.bin"],
|
|
cwd=path,
|
|
check=True,
|
|
)
|
|
for implementation in (pyzipper.AESZipFile, zipfile.ZipFile):
|
|
started = time.perf_counter()
|
|
with implementation(path / "synthetic.zip") as archive:
|
|
result = archive.read("synthetic.bin", pwd=b"fixture-only")
|
|
assert result == content
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"probe": "zipcrypto",
|
|
"reader": implementation.__module__,
|
|
"bytes": size,
|
|
"seconds": round(time.perf_counter() - started, 4),
|
|
}
|
|
)
|
|
)
|
|
if native_zip_library() is not None:
|
|
started = time.perf_counter()
|
|
result = list(
|
|
_read_selected_zip_members(
|
|
path / "synthetic.zip",
|
|
selected_files={"synthetic.bin"},
|
|
password="fixture-only",
|
|
max_file_bytes=size,
|
|
max_total_bytes=size,
|
|
)
|
|
)
|
|
assert result == [("synthetic.bin", content)]
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"probe": "zipcrypto",
|
|
"reader": "libarchive-with-independent-crc",
|
|
"bytes": size,
|
|
"seconds": round(time.perf_counter() - started, 4),
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--members", type=int, default=100)
|
|
parser.add_argument("--member-size", type=int, default=4096)
|
|
parser.add_argument("--zipcrypto-bytes", type=int, default=4 * 1024 * 1024)
|
|
parser.add_argument("--profile", action="store_true")
|
|
parser.add_argument(
|
|
"--baseline-storage-revision",
|
|
help="Read a trusted earlier local Git revision for isolated storage comparison, without changing the worktree",
|
|
)
|
|
args = parser.parse_args()
|
|
storage_probe(
|
|
args.members, args.member_size, args.profile, args.baseline_storage_revision
|
|
)
|
|
if args.zipcrypto_bytes:
|
|
zipcrypto_probe(args.zipcrypto_bytes)
|