From a9bc8238ab01de902f94cd283a2a538eabd3108b Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:08:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=E7=83=AD=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=8E=BB=E9=87=8D=EF=BC=9Ato=5Fdict=20=E9=87=8C?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E6=B1=82=E5=80=BC=E7=9A=84=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E6=94=B9=E5=B1=80=E9=83=A8=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为什么: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 次,无回归)。 --- .workbuddy/memory/2026-09-25.md | 19 ++++++++++++ farm/models.py | 51 ++++++++++++++++++++++----------- tools/bench.py | 10 +++++++ 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/.workbuddy/memory/2026-09-25.md b/.workbuddy/memory/2026-09-25.md index 8217078..d00f942 100644 --- a/.workbuddy/memory/2026-09-25.md +++ b/.workbuddy/memory/2026-09-25.md @@ -59,4 +59,23 @@ - 测试 -2(AP 专属用例没了)改 5,532 项全绿;提交 15d290a 已推送。 真机:老库迁移 ✅、无行动点卡 ✅、倒计时 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 周期)兜底损失。 + + diff --git a/farm/models.py b/farm/models.py index 7420832..93990b2 100644 --- a/farm/models.py +++ b/farm/models.py @@ -80,9 +80,13 @@ class Plot: @property 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: return "空地" - progress = self.growth / max(1, self.grow_days) + progress = self.growth / max(1, grow) name = "已播种" for threshold, label in config.GROWTH_STAGES: if progress >= threshold: @@ -91,20 +95,24 @@ class Plot: # ---------------------------------------------------------------- 序列化 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 + grow = int(info["grow_days"]) if info else 0 return { "index": self.index, "position": self.index + 1, "crop": self.crop, "crop_name": info["name"] if info else None, "growth": round(self.growth, 2), - "grow_days": self.grow_days, - "days_left": self.days_left, - "is_ripe": self.is_ripe, - "is_empty": self.is_empty, + "grow_days": grow, + "days_left": max(0, grow - int(self.growth)) if info else 0, + "is_ripe": info is not None and self.growth >= grow, + "is_empty": self.crop is None, "moisture": self.moisture, "water_capacity": config.WATER_CAPACITY, - "stage": self.stage, + "stage": self._stage(grow), } @classmethod @@ -162,18 +170,22 @@ class Animal: # ---------------------------------------------------------------- 序列化 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 { "id": self.aid, "kind": self.kind, - "name": self.name, - "product": self.product, + "name": info["name"], + "product": info["product"], "fed": self.fed, "progress": self.progress, - "cycle": self.cycle, - "days_to_product": self.days_to_product, + "cycle": cycle, + "days_to_product": max(0, cycle - self.progress), "pending": self.pending, "starve": self.starve, - "is_sick": self.is_sick, + "is_sick": self.starve >= 2, } @classmethod @@ -311,22 +323,27 @@ class GameState: # 所以只算一次再分发给 net_worth / goal_index / next_goal。 worth = self.net_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 { "day": self.day, "weather": self.weather, - "weather_name": config.weather_info(self.weather)["name"], - "weather_desc": config.weather_info(self.weather)["desc"], + "weather_name": winfo["name"], + "weather_desc": winfo["desc"], "forecast": self.forecast, - "forecast_name": config.weather_info(self.forecast)["name"], + "forecast_name": finfo["name"], "gold": self.gold, "level": self.level, "exp": self.exp, "exp_needed": self.exp_needed, "feed": self.feed, "inventory": dict(sorted(self.inventory.items())), - "storage_used": self.storage_used, - "granary_capacity": self.granary_capacity, - "granary_free": self.granary_free, + "storage_used": used, + "granary_capacity": cap, + "granary_free": max(0, cap - used), "buildings": dict(self.buildings), "plots": [p.to_dict() for p in self.plots], "plot_count": len(self.plots), diff --git a/tools/bench.py b/tools/bench.py index 74311cc..81ee54e 100644 --- a/tools/bench.py +++ b/tools/bench.py @@ -36,6 +36,16 @@ 一次 ``POST /api/actions?full=1`` 的重复工作量:``to_dict`` 2 → 1 次、 ``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