序列化热路径去重:to_dict 里重复求值的属性改局部复用

为什么:snapshot() 同时压在 SSE 每 250ms 推送和 full=1 动作回执两条
高频路径上,而它的成本大头是重复求值——profile 显示单次快照
1,128,001 次函数调用里近半是属性链重复计算同一份配置数据。

- Plot.to_dict:grow_days 原来经 4 个属性各求值一遍(本体 + days_left
  + is_ripe + stage),16 块地就是 64 次配置表查询。改为算一次局部
  复用;stage 的实现体拆到 _stage(grow),规则本体仍只有一份。
- Animal.to_dict:info 配置表原来查 4 次、cycle int() 两遍,改取一次。
- GameState.to_dict:weather_info 3 键 2 次查询;granary_free 原来把
  granary_capacity 和 storage_used 整个重算,改算一轮复用。

行为零变化(规则本体全在原属性里,532 项测试全绿)。
基准(tools/bench.py --repeat 5,最坏形态 16 地 + 8 牲畜):

    场景                       优化前      优化后
    engine.snapshot()          50.1 µs     44.2 µs      -12%
    session.snapshot()         51.0 µs     41.9 µs      -18%
    build_state_message()      50.9 µs     43.3 µs      -15%
    engine.available_actions   52.7 µs     48.7 µs       -8%

单次 snapshot 函数调用数 1,128,001 → 588,001(-48%),本次数字已记入
bench.py docstring 作长期参考。重复计数器全 OK(to_dict/snapshot/
net_worth 每请求仍各 1 次,无回归)。
This commit is contained in:
snowy-yang committed 2026-09-26 11:08:14 +08:00
1 parent 4bc7729715
commit a9bc8238ab
3 files changed
+63 -17

No files matched your search

+19
View File
@@ -59,4 +59,23 @@
- 测试 -2(AP 专属用例没了)改 5,532 项全绿;提交 15d290a 已推送。 - 测试 -2(AP 专属用例没了)改 5,532 项全绿;提交 15d290a 已推送。
真机:老库迁移 ✅、无行动点卡 ✅、倒计时 39:04→39:02 走秒 ✅。 真机:老库迁移 ✅、无行动点卡 ✅、倒计时 39:04→39:02 走秒 ✅。
## 2026-09-25 地图手感打磨(4bc7729)
- **动作粒子**:MAP.fx 数组,act() 成功后 mapJuice(action, params)——
地块号/牲畜 id 反查画布坐标喷粒子(重力+寿命衰减)+ 上浮表情浮字
(浇水💧播种🌱收获✨喂食🍽)。api() 失败返回 null 天然不触发。
- **昼夜色温**:dayTint(prog) 按一天进度叠半透明层(清晨淡金→正午无→
黄昏橙→入夜靛紫)。tick_seconds 只在完整快照里、brief 没有,
靠 refreshFromBrief 的 Object.assign 合并语义存活,兜底 2400。
- 成熟金光改呼吸式(sin 起伏 shadowBlur);悬停可交互对象指针变手型。
- 验收注意:粒子视觉链路可用 mapJuice 直接注入测(真动作要看地块状态
凑巧——全满时浇水会走失败路径,正好验证"失败不触发")。
- **沙箱进程回收边界(重要)**:WorkBuddy 会话里启动的进程都会被环境
回收——后台任务(跑 15m~1h 后静默死、日志无错误)、甚至
`--daemon` 的 DETACHED_PROCESS 守护进程(沙箱 Job 对象 kill-on-close
连 breakaway 进程一起杀)都活不长。**长期挂机必须让用户自己在系统
终端跑** `uv run main.py serve`(或 `--daemon`),那才是真正独立的进程。
会话内只做临时拉起 + 自动存档(60s 周期)兜底损失。
+34 -17
View File
@@ -80,9 +80,13 @@ class Plot:
@property @property
def stage(self) -> str: def stage(self) -> str:
"""人类可读的生长阶段。""" """人类可读的生长阶段。"""
return self._stage(self.grow_days)
def _stage(self, grow: int) -> str:
"""stage 的实现体。to_dict 已算好 grow_days,走这里避免重复求值。"""
if self.crop is None: if self.crop is None:
return "空地" return "空地"
progress = self.growth / max(1, self.grow_days) progress = self.growth / max(1, grow)
name = "已播种" name = "已播种"
for threshold, label in config.GROWTH_STAGES: for threshold, label in config.GROWTH_STAGES:
if progress >= threshold: if progress >= threshold:
@@ -91,20 +95,24 @@ class Plot:
# ---------------------------------------------------------------- 序列化 # ---------------------------------------------------------------- 序列化
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
# 热路径(每次快照对每块地调一次):grow_days 原来经 4 个属性各求值
# 一遍(本处 + days_left + is_ripe + stage),这里算一次局部复用。
# 规则本体仍以同名属性为唯一来源,此处只是等价的直接计算。
info = config.CROPS.get(self.crop) if self.crop else None info = config.CROPS.get(self.crop) if self.crop else None
grow = int(info["grow_days"]) if info else 0
return { return {
"index": self.index, "index": self.index,
"position": self.index + 1, "position": self.index + 1,
"crop": self.crop, "crop": self.crop,
"crop_name": info["name"] if info else None, "crop_name": info["name"] if info else None,
"growth": round(self.growth, 2), "growth": round(self.growth, 2),
"grow_days": self.grow_days, "grow_days": grow,
"days_left": self.days_left, "days_left": max(0, grow - int(self.growth)) if info else 0,
"is_ripe": self.is_ripe, "is_ripe": info is not None and self.growth >= grow,
"is_empty": self.is_empty, "is_empty": self.crop is None,
"moisture": self.moisture, "moisture": self.moisture,
"water_capacity": config.WATER_CAPACITY, "water_capacity": config.WATER_CAPACITY,
"stage": self.stage, "stage": self._stage(grow),
} }
@classmethod @classmethod
@@ -162,18 +170,22 @@ class Animal:
# ---------------------------------------------------------------- 序列化 # ---------------------------------------------------------------- 序列化
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
# 热路径:info 原来经 name/product/cycle/days_to_product 查 4 次配置表,
# 这里取一次局部复用;is_sick 与 days_to_product 的规则本体仍在属性里。
info = config.ANIMALS[self.kind]
cycle = int(info["cycle"])
return { return {
"id": self.aid, "id": self.aid,
"kind": self.kind, "kind": self.kind,
"name": self.name, "name": info["name"],
"product": self.product, "product": info["product"],
"fed": self.fed, "fed": self.fed,
"progress": self.progress, "progress": self.progress,
"cycle": self.cycle, "cycle": cycle,
"days_to_product": self.days_to_product, "days_to_product": max(0, cycle - self.progress),
"pending": self.pending, "pending": self.pending,
"starve": self.starve, "starve": self.starve,
"is_sick": self.is_sick, "is_sick": self.starve >= 2,
} }
@classmethod @classmethod
@@ -311,22 +323,27 @@ class GameState:
# 所以只算一次再分发给 net_worth / goal_index / next_goal。 # 所以只算一次再分发给 net_worth / goal_index / next_goal。
worth = self.net_worth worth = self.net_worth
goal = config.goal_index(worth) goal = config.goal_index(worth)
winfo = config.weather_info(self.weather)
finfo = config.weather_info(self.forecast)
# granary_free 原来会把 granary_capacity 和 storage_used 各重算一遍
cap = self.granary_capacity
used = self.storage_used
return { return {
"day": self.day, "day": self.day,
"weather": self.weather, "weather": self.weather,
"weather_name": config.weather_info(self.weather)["name"], "weather_name": winfo["name"],
"weather_desc": config.weather_info(self.weather)["desc"], "weather_desc": winfo["desc"],
"forecast": self.forecast, "forecast": self.forecast,
"forecast_name": config.weather_info(self.forecast)["name"], "forecast_name": finfo["name"],
"gold": self.gold, "gold": self.gold,
"level": self.level, "level": self.level,
"exp": self.exp, "exp": self.exp,
"exp_needed": self.exp_needed, "exp_needed": self.exp_needed,
"feed": self.feed, "feed": self.feed,
"inventory": dict(sorted(self.inventory.items())), "inventory": dict(sorted(self.inventory.items())),
"storage_used": self.storage_used, "storage_used": used,
"granary_capacity": self.granary_capacity, "granary_capacity": cap,
"granary_free": self.granary_free, "granary_free": max(0, cap - used),
"buildings": dict(self.buildings), "buildings": dict(self.buildings),
"plots": [p.to_dict() for p in self.plots], "plots": [p.to_dict() for p in self.plots],
"plot_count": len(self.plots), "plot_count": len(self.plots),
+10
View File
@@ -36,6 +36,16 @@
一次 ``POST /api/actions?full=1`` 的重复工作量:``to_dict`` 2 → 1 次、 一次 ``POST /api/actions?full=1`` 的重复工作量:``to_dict`` 2 → 1 次、
``snapshot`` 2 → 1 次、``net_worth`` 6 → 2 次。 ``snapshot`` 2 → 1 次、``net_worth`` 6 → 2 次。
2026-09-26 那次优化(to_dict 里重复求值的属性改局部复用:Plot 的
grow_days 4→1 次、Animal 的 info 查表 4→1 次、GameState 的仓储三兄弟
只算一轮)前后,单次 snapshot 的函数调用数 1,128,001 → 588,001(-48%)::
场景 优化前 优化后
engine.snapshot() 50.1 µs 44.2 µs -12%
session.snapshot() 51.0 µs 41.9 µs -18%
build_state_message() 50.9 µs 43.3 µs -15%
engine.available_actions 52.7 µs 48.7 µs -8%
""" """
from __future__ import annotations from __future__ import annotations