关于 django-startup-time
面向 PostHog 后端开发的技能指南,帮助控制 Django 启动时的导入成本,并避免延迟导入引发的回归。提供导入预算检查、测量方法和常见陷阱清单。
主要功能
- 01Django 启动导入预算
- 02延迟重型依赖导入
- 03模型与接收器注册指引
- 04启动性能测量方法
- 05回归陷阱检查清单
适用场景
- 01新增后端应用或接收器
- 02排查启动导入预算失败
- 03评估延迟导入的影响
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ posthog/ posthog --skill django-startup-time安装后在 Claude Code 中输入/django-startup-time确认已加载。
详细介绍
Django 启动时间
完整背景 —— 四种机制(惰性路由器、模型注册、接收器接线、启动 GC 窗口)、回归防护、如何在不引发回归的情况下添加代码、如何测量,以及详细的陷阱记录:docs/internal/django-startup-time.md。该文档是细节的唯一来源;本 skill 是触发器和检查清单。
防护位于 posthog/test/repo_invariants/test_startup_import_budget.py。当某个防护失败时,延迟该导入 —— 不要为了绕过它而移除条目。 反之,当你刻意将某个重要重型库从 setup 中延迟时,将其加入 FORBIDDEN_AT_SETUP(在确认它不在裸 django.setup() 中出现之后)。绝不要在那里固定产品 facade 或其 contracts 模块:facade 必须可从任何地方导入,因此改为固定你延迟的那个重型模块。尚未被命名的新的导入会被 test_no_new_heavy_imports_at_setup 捕获:任何不在 posthog/test/repo_invariants/setup_import_baseline.txt 中、且在 setup 时耗时 ≥100ms 的包都会导致构建失败 —— 延迟它;只将每个进程在 setup 时真正需要的内容加入基线。
添加后端代码时的默认做法
- 新应用/产品:从
models/__init__.py注册模型,从AppConfig.ready()接线接收器,保持ready()轻量。 - 新接收器:在所属的
AppConfig.ready()中接线,来自一个专用的轻量模块(activity_logging.py、signals.py)—— 绝不要通过 API/viewset 模块接线,即使它今天看起来轻量;那些模块会累积重型导入,而ready()会静默继承它们。 - 新的重型依赖(vendor SDK、Temporal/AI/ClickHouse、pandas/pyarrow/scipy):在使用它的路径上以函数局部方式导入,并加上
# noqa: PLC0415,绝不要在模块作用域导入。 - setup 路径模块上的 schema 类型:枚举来自
posthog.schema_enums(开销低);pydantic 模型只在使用它们的方法内部从posthog.schema导入。setup 路径上不要有模块级的from posthog.tasks...——CeleryQueue位于posthog.celery_queues。 - 新 viewset/路由:它不再在 setup 时加载 —— 不要依赖导入副作用;路由放在
rest_router.py中,而不是__init__.py垫片。 - 在哪里削减:在 setup 路径入口(
ready()接线的模块、模型文件、apps.py),绝不要在消费产品 facade 的模块内部。当 facade 触及 setup 时,入口延迟通向它的实现模块;facade 导入保持在模块作用域。如果 setup 路径需要来自重型模块的一个符号,将其移到轻量模块并重新导出。 - 任何延迟都会转移开销 —— 问一问现在哪个进程在付费、在什么路径上、以及该路径是否对延迟敏感(后台 worker 惰性付费:可以;web worker 在首批请求时付费:通常不行)。
提交前要检查的陷阱
每条一句话——文档里有完整说明和每个条目的修复方法。
ready()会把一个沉重的依赖重新拖到启动阶段——导入接收方的模块也会运行其模块级导入;接线后重新测量。- 接收方静默丢失——丢失的接收方不会报错;在不构建路由器的进程中复现(
manage.py shell、celery、migrate)。 - 模型从注册表中消失——只能通过 viewset 导入才能访问的模型会破坏
makemigrations/admin/mypy;从models/__init__.py导入它,然后执行dmypy stop。 - 懒加载路由器的合并冲突——保留
__init__.py垫片,把 master 的聚合器增量移植到rest_router.py。 - 聚合器包
__init__——使用 PEP 562__getattr__做垫片,经由importlib.import_module和__all__白名单(宽泛匹配会在同级导入时递归或死锁)。 - 序列化器字段 kwargs 在导入时运行——
choices=...输入无法在函数局部延迟;把常量移到导入轻量的模块。 - 消失的再导出——重新生成/垫片化模块会丢掉其他代码顺带从它导入的名称(例如
schema.BaseModel);grep 查找旧命名空间的消费者,包括相对导入写法。 - 补丁目标在调用时导入下失效——一旦
mod在调用时导入helper,@patch("mod.helper")就不再拦截;改为对定义模块打补丁。 - 坏合并后的快照重新生成——针对合并后的分支重新生成
.ambr,然后确认它没有掩盖回归。 - 测量的是包装器而不是工作本身——在进程内计时;用
importtime+tuna,而不是 pyinstrument;用grimp检查循环和入口枚举。 - 猜该在哪里延迟——在
importtime日志上运行 hothog;它的1-cut@列会指出应在哪个支配者处延迟,--compare给出前后对比。在 PR 的 Agent 上下文中写明这个技能和 hothog,让审查者知道这些裁剪是如何选出的。 - GC 造成的虚假 importtime 成本——用
gc.disable()重新采集;如果成本消失,那发现的是 GC,而不是模块。让入口点的 GC 窗口在finally中关闭。
评论
支持 Markdown