fix(chart): skip unused synastry overview and doomed report subprocesses

Synastry charts now skip the unused external overview and run together. Reports skip PyJHora subprocesses when that library is missing, and a full heavy-compute slot logs which paths hold it. Splitting quotas stays a simulation.

Gate, growth, and report pytest passed, as did the frontend route tests, tsc, lint, and the A/B script.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-15 08:02:39 +08:00
co-authored by Grok (agent loop) Cursor
parent 9274491681
commit 336286e428
19 changed files with 1126 additions and 37 deletions
+272
View File
@@ -0,0 +1,272 @@
"""A/B for issue #64. A is this branch with the PyJHora short-circuit forced off
(the staging control flow: always spawn). B is the short-circuit on.
Chart skip/concurrency is measured on the real chart call. The gate schemes
O2/O3/O4 are simulated only and are not implemented.
Volatile fields dropped before the report byte compare:
generated_at, created_at, timestamp, elapsed_seconds, elapsed_ms, duration_ms, request_id.
"""
from __future__ import annotations
import json
import os
import random
import statistics
import subprocess
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts"))
os.environ.setdefault("JYOTISH_API_CHART_CACHE_TTL_SECONDS", "0")
os.environ.setdefault("VEDASTRO_ENABLE_NETWORK", "0")
os.environ.setdefault("JYOTISH_GATE_CHART_OVERVIEW", "0")
import annual_pyjhora_replay as replay # noqa: E402
import tajika as tajika_top # noqa: E402
from scripts.jyotish_api_server import JyotishAPIHandler # noqa: E402
from scripts.jyotish_engine import reset_oracle_progress_snapshot_cache # noqa: E402
from scripts.professional_report_reference import build_professional_report_reference # noqa: E402
from scripts.tajika import reset_external_pyjhora_python_cache # noqa: E402
BIRTH = {
"year": 1990,
"month": 1,
"day": 1,
"hour": 12,
"minute": 0,
"second": 0,
"lat": 28.6139,
"lon": 77.2090,
"tz": 5.5,
"ayanamsa": "lahiri",
"node_mode": "true",
}
PARTNER = {**BIRTH, "year": 1992, "day": 2}
VOLATILE = {"generated_at", "created_at", "timestamp", "elapsed_seconds", "elapsed_ms", "duration_ms", "request_id"}
class _Counter:
def __init__(self) -> None:
self.n = 0
self._real = subprocess.run
self._lock = threading.Lock()
def install(self) -> None:
def _run(*args, **kwargs):
with self._lock:
self.n += 1
return self._real(*args, **kwargs)
subprocess.run = _run # type: ignore[assignment]
for module in list(sys.modules.values()):
bound = getattr(module, "subprocess", None)
if getattr(bound, "run", None) is self._real:
bound.run = _run # type: ignore[union-attr]
def reset(self) -> None:
with self._lock:
self.n = 0
def _median(values: list[float]) -> float:
return statistics.median(values) if values else 0.0
def _strip(value):
if isinstance(value, dict):
return {key: _strip(item) for key, item in value.items() if key not in VOLATILE}
if isinstance(value, list):
return [_strip(item) for item in value]
return value
def _canon(value) -> str:
return json.dumps(_strip(value), sort_keys=True, ensure_ascii=True, default=str)
def _chart_summary(chart: dict) -> dict:
planets = chart.get("planets") if isinstance(chart, dict) else None
moon = planets.get("Moon") if isinstance(planets, dict) else None
lon = None
if isinstance(moon, dict):
lon = moon.get("lon", moon.get("longitude"))
return {
"ascendant": chart.get("ascendant") if isinstance(chart, dict) else None,
"planets": planets,
"moon_longitude": lon,
}
def _one_chart(body: dict) -> tuple[float, dict]:
started = time.perf_counter()
chart = object.__new__(JyotishAPIHandler)._compute_chart_sync(body)
return time.perf_counter() - started, chart
def _measure_charts(counter: _Counter) -> dict:
serial_times = []
concurrent_times = []
serial_subs = []
concurrent_subs = []
same = True
for _ in range(3):
counter.reset()
started = time.perf_counter()
_, self_a = _one_chart(BIRTH)
_, partner_a = _one_chart(PARTNER)
serial_times.append(time.perf_counter() - started)
serial_subs.append(counter.n)
counter.reset()
started = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as pool:
self_b = pool.submit(_one_chart, {**BIRTH, "skip_vedastro_main_entry_overview": True})
partner_b = pool.submit(_one_chart, {**PARTNER, "skip_vedastro_main_entry_overview": True})
_, self_chart = self_b.result()
_, partner_chart = partner_b.result()
concurrent_times.append(time.perf_counter() - started)
concurrent_subs.append(counter.n)
same = same and _canon(_chart_summary(self_a)) == _canon(_chart_summary(self_chart))
same = same and _canon(_chart_summary(partner_a)) == _canon(_chart_summary(partner_chart))
return {
"serial_seconds_median": round(_median(serial_times), 4),
"concurrent_skip_seconds_median": round(_median(concurrent_times), 4),
"serial_subprocesses_median": _median(serial_subs),
"concurrent_skip_subprocesses_median": _median(concurrent_subs),
"summary_bytes_match": same,
}
def _measure_report(counter: _Counter, *, spawn: bool) -> dict:
times = []
subs = []
payload = None
body = {**BIRTH, "format": "markdown", "edition": "full_data", "languages": ["zh", "en"], "include_fact_tables": True}
for _ in range(3):
replay.reset_jhora_import_cache()
if spawn:
replay._jhora_spec_cache = True
reset_oracle_progress_snapshot_cache()
reset_external_pyjhora_python_cache()
tajika_top.reset_external_pyjhora_python_cache()
counter.reset()
started = time.perf_counter()
payload = build_professional_report_reference(
object.__new__(JyotishAPIHandler),
body,
)
times.append(time.perf_counter() - started)
subs.append(counter.n)
return {
"seconds_median": round(_median(times), 4),
"subprocesses_median": _median(subs),
"payload": payload,
}
def _simulate() -> list[dict]:
"""One fixed minute. O2 adds a second pool and is the only scheme that touches BUG-717."""
rng = random.Random(64)
arrivals = [("report", index * 30.0, 13.0) for index in range(2)]
arrivals += [("consult", rng.random() * 60.0, 0.2) for _ in range(20)]
arrivals += [("score", rng.random() * 60.0, 8.0) for _ in range(6)]
arrivals.sort(key=lambda item: item[1])
def run(mode: str) -> dict:
heavy: list[tuple[float, str]] = []
light: list[float] = []
rejected = {"consult": 0, "report": 0, "score": 0}
accepted = {"consult": 0, "report": 0, "score": 0}
peak = 0
def occupy(at: float) -> tuple[int, int]:
running = [item for item in heavy if item[0] > at]
lights = [item for item in light if item > at]
return len(running), len(lights)
for kind, arrival, duration in arrivals:
heavy_n, light_n = occupy(arrival)
heavy = [item for item in heavy if item[0] > arrival]
light = [item for item in light if item > arrival]
placed = False
stamp = arrival
if mode == "O2" and kind == "consult":
if light_n < 2:
light.append(arrival + duration)
placed = True
elif mode == "O3" and kind == "consult":
start = arrival
while start <= arrival + 1.5:
heavy_n, _light_n = occupy(start)
if heavy_n < 2:
heavy.append((start + duration, kind))
placed = True
stamp = start
break
nxt = min(item[0] for item in heavy)
if nxt > arrival + 1.5:
break
start = nxt
elif mode == "O4" and kind != "consult":
heavy_jobs = sum(1 for _end, slot in heavy if slot != "consult")
if heavy_jobs < 1 and heavy_n < 2:
heavy.append((arrival + duration, kind))
placed = True
else:
if heavy_n < 2:
heavy.append((arrival + duration, kind))
placed = True
if placed:
accepted[kind] += 1
else:
rejected[kind] += 1
running, lights = occupy(stamp)
peak = max(peak, running + lights)
def rate(kind: str) -> str:
total = accepted[kind] + rejected[kind]
return "0" if total == 0 else f"{rejected[kind] / total:.2f}"
return {
"scheme": mode,
"consult_429": rate("consult"),
"report_429": rate("report"),
"score_429": rate("score"),
"peak": peak,
"touches_bug_717": mode == "O2",
}
return [run(mode) for mode in ("O1", "O2", "O3", "O4")]
def main() -> None:
counter = _Counter()
counter.install()
charts = _measure_charts(counter)
forced = _measure_report(counter, spawn=True)
native = _measure_report(counter, spawn=False)
report_match = _canon(forced["payload"]) == _canon(native["payload"])
schemes = _simulate()
out = {
"charts": charts,
"report_forced_subprocess": {key: value for key, value in forced.items() if key != "payload"},
"report_short_circuit": {key: value for key, value in native.items() if key != "payload"},
"report_bytes_match": report_match,
"schemes": schemes,
"jhora_installed": False,
}
text = json.dumps(out, ensure_ascii=False, indent=2)
dest = ROOT / "docs" / "testing" / "api-chart-subprocess-gate-ab-20261013.json"
dest.write_text(text + "\n", encoding="utf-8")
print(text)
if __name__ == "__main__":
main()