|
|
|
@@ -0,0 +1,257 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Bench and canonical dump for the 2026-10-13 rectification scoring speedup.
|
|
|
|
|
|
|
|
|
|
Read-only aside from the dump files the caller asks for. Cross-process dumps
|
|
|
|
|
must be launched with PYTHONHASHSEED=0. The baseline tree has no
|
|
|
|
|
include_internal parameter; that call falls back to the old signature.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import hashlib
|
|
|
|
|
import json
|
|
|
|
|
import statistics
|
|
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
from datetime import date
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any, Callable
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
if str(ROOT) not in sys.path:
|
|
|
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
|
|
|
|
from scripts.rectification.api_service import block_scan, diagnostics, score_candidates
|
|
|
|
|
from scripts.rectification.contracts import normalize_rectification_request
|
|
|
|
|
from scripts.rectification import event_probes
|
|
|
|
|
from scripts.rectification import scoring_service
|
|
|
|
|
from scripts import active_rectification_event_engine as event_engine
|
|
|
|
|
|
|
|
|
|
FROZEN_TODAY = date(2026, 9, 16)
|
|
|
|
|
EVENT_SPECS = (
|
|
|
|
|
("education", "education_start", "2008-01-01", "2008-12-31", "year"),
|
|
|
|
|
("career", "career_entry", "2012-06-01", "2012-06-30", "month"),
|
|
|
|
|
("relocation", "relocation", "2014-03-01", "2014-03-31", "month"),
|
|
|
|
|
("relationship", "relationship_commitment", "2016-10-01", "2016-10-31", "month"),
|
|
|
|
|
("career", "promotion", "2018-01-01", "2018-12-31", "year"),
|
|
|
|
|
("finance", "asset_change", "2019-05-01", "2019-05-31", "month"),
|
|
|
|
|
("family", "family_event", "2020-08-01", "2020-08-31", "month"),
|
|
|
|
|
("health", "self_health_event", "2022-01-01", "2022-12-31", "year"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FrozenDate(date):
|
|
|
|
|
@classmethod
|
|
|
|
|
def today(cls) -> date:
|
|
|
|
|
return FROZEN_TODAY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _events() -> list[dict[str, Any]]:
|
|
|
|
|
rows = []
|
|
|
|
|
for index, (domain, kind, start, end, precision) in enumerate(EVENT_SPECS, start=1):
|
|
|
|
|
rows.append({
|
|
|
|
|
"id": f"00000000-0000-4000-8000-{index:012d}",
|
|
|
|
|
"domain": domain,
|
|
|
|
|
"event_kind": kind,
|
|
|
|
|
"date_start": start,
|
|
|
|
|
"date_end": end,
|
|
|
|
|
"precision": precision,
|
|
|
|
|
"summary": "虚构",
|
|
|
|
|
})
|
|
|
|
|
return rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_request(start: str, end: str, **extra: Any) -> dict[str, Any]:
|
|
|
|
|
body = {
|
|
|
|
|
"birth_date": "1990-01-01",
|
|
|
|
|
"start_time": start,
|
|
|
|
|
"end_time": end,
|
|
|
|
|
"lat": 39.9,
|
|
|
|
|
"lon": 116.4,
|
|
|
|
|
"tz": 8,
|
|
|
|
|
"events": _events(),
|
|
|
|
|
**extra,
|
|
|
|
|
}
|
|
|
|
|
return normalize_rectification_request(body, today=FROZEN_TODAY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _strip(value: Any, keys: set[str]) -> Any:
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
return {key: _strip(item, keys) for key, item in value.items() if key not in keys}
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
return [_strip(item, keys) for item in value]
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _canonical(value: Any) -> bytes:
|
|
|
|
|
return json.dumps(value, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode("ascii")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _score(request: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
return score_candidates(request, include_internal=True)
|
|
|
|
|
except TypeError:
|
|
|
|
|
return score_candidates(request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _call(kind: str, request: dict[str, Any], *, internal: bool = False) -> dict[str, Any]:
|
|
|
|
|
if kind == "score":
|
|
|
|
|
# Bench times the product call. Dump asks for the internal matrix so
|
|
|
|
|
# the baseline tree, which always returned it, stays comparable.
|
|
|
|
|
if internal:
|
|
|
|
|
return _score(request)
|
|
|
|
|
return score_candidates(request)
|
|
|
|
|
if kind == "block":
|
|
|
|
|
return block_scan(request)
|
|
|
|
|
if kind == "diagnostics":
|
|
|
|
|
return diagnostics(request)
|
|
|
|
|
raise ValueError(kind)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def scenarios() -> list[dict[str, Any]]:
|
|
|
|
|
from tests.test_rectification_engine_memoization import public_score_request
|
|
|
|
|
|
|
|
|
|
rows = [
|
|
|
|
|
{"name": "12:00-12:20", "kind": "score", "request": make_request("12:00", "12:20")},
|
|
|
|
|
{"name": "11:45-12:15", "kind": "score", "request": make_request("11:45", "12:15")},
|
|
|
|
|
{"name": "11:00-13:00", "kind": "score", "request": make_request("11:00", "13:00")},
|
|
|
|
|
{"name": "23:20-00:40", "kind": "score", "request": make_request("23:20", "00:40")},
|
|
|
|
|
{"name": "declined-relationship-career", "kind": "score", "request": make_request(
|
|
|
|
|
"11:30", "12:30", declined_domains=["relationship", "career"],
|
|
|
|
|
)},
|
|
|
|
|
{"name": "refresh-probes-columns", "kind": "score", "request": make_request(
|
|
|
|
|
"11:30", "12:30", refresh_probes=True, column_times=["11:40", "12:10", "12:20"],
|
|
|
|
|
)},
|
|
|
|
|
{"name": "minute-step-2", "kind": "score", "request": make_request("10:00", "13:00", minute_step=2)},
|
|
|
|
|
{"name": "block-step-10", "kind": "block", "request": make_request("00:00", "23:59", minute_step=10)},
|
|
|
|
|
{"name": "block-step-5", "kind": "block", "request": make_request("10:00", "14:00", minute_step=5)},
|
|
|
|
|
{"name": "public-score-request", "kind": "score", "request": normalize_rectification_request(
|
|
|
|
|
public_score_request(), today=FROZEN_TODAY,
|
|
|
|
|
)},
|
|
|
|
|
{"name": "diagnostics-11:00-13:00", "kind": "diagnostics", "request": make_request("11:00", "13:00")},
|
|
|
|
|
]
|
|
|
|
|
return rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _strip_keys(kind: str) -> set[str]:
|
|
|
|
|
# diagnostics no longer repeats the receipt. Drop it on both sides so the
|
|
|
|
|
# remaining payload is what must stay byte-identical.
|
|
|
|
|
if kind == "diagnostics":
|
|
|
|
|
return {"column_compare_ms", "candidate_decision_receipt"}
|
|
|
|
|
return {"column_compare_ms"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _SegmentTimer:
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.totals = {
|
|
|
|
|
"guided_collect_windows": 0.0,
|
|
|
|
|
"merge_transition_proximity": 0.0,
|
|
|
|
|
"get_current_narayana_dasha": 0.0,
|
|
|
|
|
"derive_functional_benefic_malefic": 0.0,
|
|
|
|
|
"build_candidate_static_context": 0.0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def wrap(self, owner: Any, name: str, key: str) -> Callable[[], None]:
|
|
|
|
|
original = getattr(owner, name)
|
|
|
|
|
|
|
|
|
|
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
|
|
|
started = time.perf_counter()
|
|
|
|
|
try:
|
|
|
|
|
return original(*args, **kwargs)
|
|
|
|
|
finally:
|
|
|
|
|
self.totals[key] += time.perf_counter() - started
|
|
|
|
|
|
|
|
|
|
setattr(owner, name, wrapped)
|
|
|
|
|
|
|
|
|
|
def restore() -> None:
|
|
|
|
|
setattr(owner, name, original)
|
|
|
|
|
|
|
|
|
|
return restore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _median(samples: list[float]) -> float:
|
|
|
|
|
return round(statistics.median(samples), 4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bench() -> dict[str, Any]:
|
|
|
|
|
jobs = [
|
|
|
|
|
("12:00-12:20", "score", make_request("12:00", "12:20")),
|
|
|
|
|
("11:45-12:15", "score", make_request("11:45", "12:15")),
|
|
|
|
|
("11:00-13:00", "score", make_request("11:00", "13:00")),
|
|
|
|
|
("block-step-10", "block", make_request("00:00", "23:59", minute_step=10)),
|
|
|
|
|
]
|
|
|
|
|
rows = []
|
|
|
|
|
event_probes.date = _FrozenDate
|
|
|
|
|
try:
|
|
|
|
|
for name, kind, request in jobs:
|
|
|
|
|
samples = []
|
|
|
|
|
segments = []
|
|
|
|
|
sizes = []
|
|
|
|
|
counts = []
|
|
|
|
|
for _ in range(5):
|
|
|
|
|
timer = _SegmentTimer()
|
|
|
|
|
restores = [
|
|
|
|
|
timer.wrap(event_probes, "guided_collect_windows", "guided_collect_windows"),
|
|
|
|
|
timer.wrap(scoring_service, "merge_transition_proximity", "merge_transition_proximity"),
|
|
|
|
|
timer.wrap(event_engine.narayana_dasha, "get_current_narayana_dasha", "get_current_narayana_dasha"),
|
|
|
|
|
timer.wrap(event_engine.functional_benefics, "derive_functional_benefic_malefic", "derive_functional_benefic_malefic"),
|
|
|
|
|
timer.wrap(event_engine, "build_candidate_static_context", "build_candidate_static_context"),
|
|
|
|
|
]
|
|
|
|
|
started = time.perf_counter()
|
|
|
|
|
try:
|
|
|
|
|
result = _call(kind, request)
|
|
|
|
|
finally:
|
|
|
|
|
for restore in restores:
|
|
|
|
|
restore()
|
|
|
|
|
elapsed = time.perf_counter() - started
|
|
|
|
|
samples.append(elapsed)
|
|
|
|
|
segments.append({key: round(value, 4) for key, value in timer.totals.items()})
|
|
|
|
|
sizes.append(len(_canonical(_strip(result, _strip_keys(kind)))))
|
|
|
|
|
counts.append(result.get("candidate_count") or len(result.get("candidate_scores") or []))
|
|
|
|
|
median_segments: dict[str, float] = {}
|
|
|
|
|
for key in segments[0]:
|
|
|
|
|
median_segments[key] = _median([item[key] for item in segments])
|
|
|
|
|
rows.append({
|
|
|
|
|
"window": name,
|
|
|
|
|
"candidate_count": counts[0],
|
|
|
|
|
"median_seconds": _median(samples),
|
|
|
|
|
"segment_median_seconds": median_segments,
|
|
|
|
|
"response_bytes": int(statistics.median(sizes)),
|
|
|
|
|
})
|
|
|
|
|
finally:
|
|
|
|
|
event_probes.date = date
|
|
|
|
|
return {"runs": 5, "rows": rows}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def dump(out_dir: Path) -> list[dict[str, str]]:
|
|
|
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
written = []
|
|
|
|
|
event_probes.date = _FrozenDate
|
|
|
|
|
try:
|
|
|
|
|
for row in scenarios():
|
|
|
|
|
result = _call(row["kind"], row["request"], internal=True)
|
|
|
|
|
payload = _strip(result, _strip_keys(row["kind"]))
|
|
|
|
|
raw = _canonical(payload)
|
|
|
|
|
digest = hashlib.sha256(raw).hexdigest()
|
|
|
|
|
path = out_dir / f"{row['name'].replace(':', '')}.json"
|
|
|
|
|
path.write_bytes(raw)
|
|
|
|
|
written.append({"name": row["name"], "sha256": digest, "bytes": str(len(raw))})
|
|
|
|
|
print(f"{row['name']}\t{digest}\t{len(raw)}")
|
|
|
|
|
finally:
|
|
|
|
|
event_probes.date = date
|
|
|
|
|
return written
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
|
parser.add_argument("command", choices=("bench", "dump"))
|
|
|
|
|
parser.add_argument("--out", type=Path, default=ROOT / "artifacts" / "rect-scoring-perf-20261013")
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
if args.command == "bench":
|
|
|
|
|
print(json.dumps(bench(), ensure_ascii=True, indent=2))
|
|
|
|
|
else:
|
|
|
|
|
dump(args.out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|