Copy the full-data packet once per report, not once per domain (issue #122, BUG-1471)
read_full_data_domain deep-copied the shared worksheets on every call; a long report reads five domains twice, so ten copies for read-only use. The shared part is now copied once (full_data_domain_base) and reused for every domain's facts and their leaf check; each domain's facts are still copied out so domains share no objects. Long reports byte-identical on four fictional charts; with #121, -24–29% end to end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoz1Qk3y9buDv1mkmTu
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f8fa4d66ef
commit
3eeb898071
@@ -1,5 +1,11 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-28 — 个人长报告生成快约四分之一
|
||||
|
||||
- 第一次生成一份完整的个人长报告,本机实测从约 26 秒降到约 20 秒;报告内容一字不变。
|
||||
- 两处改动:报告出稿的提速开关默认打开(issue #121);报告和咨询数据卡不再为每个领域把整份数据各复制一遍(issue #122)。
|
||||
- Skill 版本不变(BUG-1335、BUG-1471)。
|
||||
|
||||
## 2026-10-28 — 星盘页页头整齐了,深色模式下几处灰底和白边去掉
|
||||
|
||||
- 首页输入框在深色模式下不再有一块灰底。
|
||||
|
||||
@@ -20065,3 +20065,18 @@
|
||||
- 复发自:BUG-1466(深色变体未覆盖)
|
||||
- 修复版本:分支 `codex/ui-polish-140-20261028`。
|
||||
|
||||
## BUG-1471 | 长报告与咨询数据卡每个领域都把整份数据包深拷贝一遍
|
||||
|
||||
- 状态:resolved(代码 + 回归测试;待部署)
|
||||
- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #118 代码体检 → #122)
|
||||
- 影响面:个人长报告(模型写的「读盘」前置事实)、咨询数据卡;`scripts/pl9_full_data_export.py` `read_full_data_domain`、`scripts/consult_card_domain_facts.py`、`scripts/report_render/reading_facts.py`。
|
||||
- 现象:长报告冷生成慢;profile 口径下整包 deepcopy 合计约 9 s,其中 `read_full_data_domain` 约 5 s。
|
||||
- 触发条件:任何长报告生成、任何带数据卡的咨询轮。
|
||||
- 根因:`read_full_data_domain` 每调一次就把 `worksheets` 等共享部分整份深拷贝。长报告读 5 个领域,生成事实一遍、逐条核对又一遍,共 10 份;读的人全是只读。
|
||||
- 修复:新增 `full_data_domain_base`,同一个数据包只深拷一次;`read_full_data_domain(..., base=)` 复用它。`build_consult_card_facts` 每次调用只建一次,长报告的事实与核对共用同一份。各领域产出的事实再各自深拷贝(体量小),保持「不同领域的结果互不共享对象」与改前一致。
|
||||
- 验证:`scripts/research/report_reference_perf_ab.py --mask-pyjhora`(四张虚构盘各三次)改前 / 改后、开关开 / 关全部组合下完整报告逐字节相同;与改前相比,叠加 #121 后端到端快 24%~29%(中位约 26%),单论本单在开关打开时再快 1%~7%。JSON 与参考版两项附加检查在改前代码上连跑两次即不同(输出自身跨进程不确定),无法用跨代码 sha 比对,已注明。新测试 `tests/test_consult_card_full_source.py::test_all_domains_share_one_packet_copy_and_still_match_the_per_domain_path`:只建一次共享拷贝、结果与逐领域路径一致、领域间不共享对象。Python 快速门全部 Python 步骤通过(前端 npm 步因本工作树无依赖未跑,本单无前端改动)。
|
||||
- 防复发:上面的测试;只读用途的「视图」不要每次整份深拷贝,多处读同一包时共享一份。
|
||||
- 关联:BUG-1335(同一份报告的出稿与哈希重复劳动,#121 已开快路径)。溯源哈希仍在出稿后由质量门重算一次以核对,这是有意的完整性检查,未合并。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/report-fast-default-20261028`。
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ judgment table; the card builder does that from these leaves.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from pl9_full_data_export import read_full_data_domain
|
||||
from pl9_full_data_export import full_data_domain_base, read_full_data_domain
|
||||
|
||||
PRECISION_SENTENCE = "出生时间精度不够,不参与"
|
||||
RAHU_NOTE = "罗计不单独定功能吉凶"
|
||||
@@ -783,10 +785,16 @@ def _factors(domain: str, timing: dict[str, Any], karakas: dict[str, dict[str, A
|
||||
return factors
|
||||
|
||||
|
||||
def build_domain_facts(packet: dict, domain: str, *, minute_confirmed: bool) -> dict[str, Any]:
|
||||
def build_domain_facts(
|
||||
packet: dict,
|
||||
domain: str,
|
||||
*,
|
||||
minute_confirmed: bool,
|
||||
base: Mapping | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if domain not in PACKET_DOMAIN:
|
||||
raise KeyError(domain)
|
||||
view = read_full_data_domain(packet, PACKET_DOMAIN[domain])
|
||||
view = read_full_data_domain(packet, PACKET_DOMAIN[domain], base=base)
|
||||
varga = {
|
||||
"career": "D10", "wealth": "D2", "marriage": "D9", "health": "D6",
|
||||
"children": "D7", "parents": "D12", "education": "D24", "migration": "D4",
|
||||
@@ -824,7 +832,9 @@ def build_domain_facts(packet: dict, domain: str, *, minute_confirmed: bool) ->
|
||||
encoded = json.dumps(facts, ensure_ascii=False, default=str)
|
||||
if "tajika_named_yogas" in encoded:
|
||||
raise RuntimeError("consult card facts must not read tajika_named_yogas")
|
||||
return facts
|
||||
# Leaves may be objects of the view. With a shared base, two domains' facts
|
||||
# would share them; each domain's facts stay their own, as before.
|
||||
return facts if base is None else copy.deepcopy(facts)
|
||||
|
||||
|
||||
def build_consult_card_facts(
|
||||
@@ -833,13 +843,16 @@ def build_consult_card_facts(
|
||||
*,
|
||||
minute_confirmed: bool | None,
|
||||
body: dict | None = None,
|
||||
base: Mapping | None = None,
|
||||
) -> dict[str, Any]:
|
||||
confirmed = minute_confirmed_from_body(body) if minute_confirmed is None else minute_confirmed
|
||||
rows = []
|
||||
if base is None and any(domain in PACKET_DOMAIN for domain in domains):
|
||||
base = full_data_domain_base(packet)
|
||||
for domain in domains:
|
||||
if domain not in PACKET_DOMAIN:
|
||||
continue
|
||||
rows.append(build_domain_facts(packet, domain, minute_confirmed=confirmed))
|
||||
rows.append(build_domain_facts(packet, domain, minute_confirmed=confirmed, base=base))
|
||||
return {
|
||||
"schema": "consult-card-facts-v1",
|
||||
"minute_confirmed": confirmed,
|
||||
@@ -848,9 +861,9 @@ def build_consult_card_facts(
|
||||
}
|
||||
|
||||
|
||||
def assert_fact_leaves(packet: dict, facts: dict[str, Any]) -> None:
|
||||
def assert_fact_leaves(packet: dict, facts: dict[str, Any], *, base: Mapping | None = None) -> None:
|
||||
"""Every copied leaf equals the packet. Derived selections are not leaves."""
|
||||
view = read_full_data_domain(packet, facts["packet_domain"])
|
||||
view = read_full_data_domain(packet, facts["packet_domain"], base=base)
|
||||
|
||||
def check(item: Any) -> None:
|
||||
if isinstance(item, list):
|
||||
|
||||
@@ -2472,19 +2472,18 @@ def _append_annual_method_reference(markdown: str, packet: dict, language: str)
|
||||
return markdown.rstrip() + "\n\n" + extra + "\n"
|
||||
|
||||
|
||||
def read_full_data_domain(packet: Mapping, domain: str) -> dict:
|
||||
"""Deep-copied packet view for one consult domain.
|
||||
def full_data_domain_base(packet: Mapping) -> dict:
|
||||
"""The detached, domain-independent part of every domain view.
|
||||
|
||||
Field selection belongs to the consult card. This entry only freezes the
|
||||
domain name and detaches the copy from the live packet.
|
||||
One deep copy of the packet's shared sections. ``read_full_data_domain``
|
||||
used to make this copy once per domain, and a long report reads five
|
||||
domains twice (facts, then the leaf check): ten copies of the whole
|
||||
``worksheets`` tree for read-only use (issue #122). Callers that read
|
||||
several domains of one packet build this once and pass it as ``base``.
|
||||
"""
|
||||
if domain not in FULL_DATA_DOMAINS:
|
||||
raise KeyError(domain)
|
||||
if not isinstance(packet, Mapping):
|
||||
raise TypeError("packet must be a mapping")
|
||||
copied = copy.deepcopy({
|
||||
"domain": domain,
|
||||
"label": FULL_DATA_DOMAINS[domain],
|
||||
"report_version": packet.get("report_version"),
|
||||
"birth_info": packet.get("birth_info"),
|
||||
"core_chart": packet.get("core_chart"),
|
||||
@@ -2496,3 +2495,19 @@ def read_full_data_domain(packet: Mapping, domain: str) -> dict:
|
||||
if isinstance(timing, dict):
|
||||
timing.pop("pyjhora_annual_method_reference", None)
|
||||
return copied
|
||||
|
||||
|
||||
def read_full_data_domain(packet: Mapping, domain: str, *, base: Mapping | None = None) -> dict:
|
||||
"""Detached packet view for one consult domain.
|
||||
|
||||
Field selection belongs to the consult card. This entry only freezes the
|
||||
domain name and detaches the view from the live packet. With ``base`` (from
|
||||
``full_data_domain_base`` of the same packet) the shared sections are that
|
||||
one copy; views of different domains then share it and must be read only.
|
||||
"""
|
||||
if domain not in FULL_DATA_DOMAINS:
|
||||
raise KeyError(domain)
|
||||
if not isinstance(packet, Mapping):
|
||||
raise TypeError("packet must be a mapping")
|
||||
shared = full_data_domain_base(packet) if base is None else base
|
||||
return {"domain": domain, "label": FULL_DATA_DOMAINS[domain], **shared}
|
||||
|
||||
@@ -14,9 +14,11 @@ from typing import Any
|
||||
try:
|
||||
from scripts.consult_card_domain_facts import assert_fact_leaves, build_consult_card_facts, dig
|
||||
from scripts.consult_full_data_cache import _attach_graha_drishti
|
||||
from scripts.pl9_full_data_export import full_data_domain_base
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path
|
||||
from consult_card_domain_facts import assert_fact_leaves, build_consult_card_facts, dig
|
||||
from consult_full_data_cache import _attach_graha_drishti
|
||||
from pl9_full_data_export import full_data_domain_base
|
||||
|
||||
READING_FACTS_SCHEMA = "report-reading-facts-v1"
|
||||
READING_DOMAINS = ("general", "career", "wealth", "marriage", "health")
|
||||
@@ -87,10 +89,12 @@ def build_report_reading_facts(
|
||||
) -> dict[str, Any]:
|
||||
"""Compact, packet-sourced facts for the reading. Raises on any non-packet leaf."""
|
||||
working = _attach_graha_drishti(copy.deepcopy(packet), full_reading)
|
||||
raw = build_consult_card_facts(working, list(READING_DOMAINS), minute_confirmed=minute_confirmed)
|
||||
# One detached copy of the packet serves every domain's facts and their leaf check (issue #122).
|
||||
base = full_data_domain_base(working)
|
||||
raw = build_consult_card_facts(working, list(READING_DOMAINS), minute_confirmed=minute_confirmed, base=base)
|
||||
rows = {row["domain"]: row for row in raw["domains"]}
|
||||
for row in rows.values():
|
||||
assert_fact_leaves(working, row)
|
||||
assert_fact_leaves(working, row, base=base)
|
||||
general = rows["general"]
|
||||
planets = {}
|
||||
for name, record in general["planets"].items():
|
||||
|
||||
@@ -425,3 +425,32 @@ def test_one_warm_at_a_time_and_same_key_user_waits_for_that_build(monkeypatch):
|
||||
warm_thread.join(timeout=5)
|
||||
user_thread.join(timeout=5)
|
||||
_drop_cache(same_identity)
|
||||
|
||||
|
||||
def test_all_domains_share_one_packet_copy_and_still_match_the_per_domain_path(monkeypatch):
|
||||
"""Issue #122: a report reads five domains twice; the packet is copied once, not ten times."""
|
||||
import consult_card_domain_facts as facts_module
|
||||
from consult_card_domain_facts import build_consult_card_facts
|
||||
|
||||
packet = _packet()
|
||||
expected = [build_domain_facts(packet, domain, minute_confirmed=True) for domain in CONSULTATION_DOMAINS]
|
||||
calls = []
|
||||
real_base = facts_module.full_data_domain_base
|
||||
|
||||
def counting_base(value):
|
||||
calls.append(1)
|
||||
return real_base(value)
|
||||
|
||||
monkeypatch.setattr(facts_module, "full_data_domain_base", counting_base)
|
||||
shared = build_consult_card_facts(packet, list(CONSULTATION_DOMAINS), minute_confirmed=True)
|
||||
assert len(calls) == 1
|
||||
assert json.dumps(shared["domains"], ensure_ascii=False, sort_keys=True, default=str) == json.dumps(
|
||||
expected, ensure_ascii=False, sort_keys=True, default=str,
|
||||
)
|
||||
# Each domain's facts stay their own objects, as when every domain copied the packet.
|
||||
first, second = shared["domains"][0], shared["domains"][1]
|
||||
assert first["planets"]["Sun"] is not second["planets"]["Sun"]
|
||||
assert first["timing"] is not second["timing"]
|
||||
base = real_base(packet)
|
||||
for row in shared["domains"]:
|
||||
assert_fact_leaves(packet, row, base=base)
|
||||
|
||||
Reference in New Issue
Block a user