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:
co-authored by
Grok (agent loop)
Cursor
parent
9274491681
commit
336286e428
@@ -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()
|
||||
Reference in New Issue
Block a user