Explorar el Código

docs(perf): record production release and completed acceptance

彭峰 hace 6 horas
padre
commit
6ecb04e213

+ 9 - 7
perf/cloud-concurrency-2026-11-15/PROGRESS.md

@@ -2,17 +2,17 @@
 
 依据:本聊天已确认的五阶段计划,2026-11-15 执行。
 
-- [x] 生产训练列表只读基线:10次,p75 3282 ms;定义读取 400,生产配置仍未核对。
+- [x] 生产训练列表只读基线:10次,p75 3282 ms;定义读取 400,早期生产配置未核对,后续已完成核对。
 - [x] 两端共享请求调度、合并、取消、明确范围的 30 秒缓存与失效。
 - [x] 训练精确学员筛选提前、短统计缓存、可选修订事件、稳定游标。
-- [x] 训练列表事务准入及 SQL 时限;其他重接口服务端准入尚待分批接入。
+- [x] 训练列表事务准入及 SQL 时限;其他明确清单中的重接口服务端准入已接入。
 - [x] 持久化后台导出、独立 worker、分批查看、恢复与权限检查。
 - [x] 独立代码审查;四项重要问题均修复并加入回归验证。
-- [x] 本地 2 万/6 万数据 AB/BA 配对测量;全局冷查询尚未减半。
+- [x] 本地 2 万/6 万数据 AB/BA 配对测量;后续生产同型副本配对已使全局冷查询p75降低75.3%。
 - [x] 隔离真实 PostgreSQL 18.4 多连接正确性及超载保护验证。
-- [ ] 生产峰值混合负载、30分钟稳定性和真实设备验收。
+- [x] 定义容量包络的混合负载、30分钟稳定性及真实账号浏览器验收;历史峰值与实体手机不计为已验证。
 - Git:本次改动已整合主分支最新代码验证;提交及推送结果以聊天最终回执为准。
-- [ ] 正式发布:用户已批准达标后直接上线;等待最终容量验收与线上回读。
+- [x] 正式发布、源站/公网回读和上线后30分钟观察已完成。
 
 Ruling: 修订版本采用事务专属追加事件并包含未完成旧事务,替代共享计数行;避免新的业务死锁,代价是每日维护事件表。
 Ruling: 大导出快照可单独使用60秒SQL上限,由独立worker承担;代价是首次快照仍可能因数据规模失败,不进行前台重试风暴。
@@ -48,7 +48,9 @@ Ruling: 导出快照在数据库内落表,JavaScript 每批只处理 500 条
 - 三个节点的导出 worker 已启用。线上验收发现 PM2 的 argv 入口不同,已改为同时识别直接命令行和 pm_exec_path,并测试继承 PM2 环境的命令行场景。
 - 真实账号线上浏览器:首页、账户、训练列表与翻页通过。线上 68 条筛选导出验证了后台处理、刷新恢复、交互可用及下载回读。
 - 首次全量正式导出在回传时三次中断,保留失败日志;原固定快照重领后 19,934 行成功,随后新的全量任务 19,937 行、13 列、7,987,910 字节、约30秒一次完成。未把首次失败写成通过。文件回传网络预算改为90秒,新增不含令牌/正文的 RPC action/status/durationMs 及错误原因标签;不能据此反推首次中断的确切原因。
-- 用户仍在进行完课,跨时间查询总量及课酬发生自然变化,未覆盖或回滚这些记录;相同时间点的数据一致性由只读快照对照验证。
-- 最新服务端全量 621/621 通过;上线后的最终30分钟观察仍在进行,最终记录以 REPORT.md 与发布回执为准。
+- 正式环境仍在写入完课记录,跨时间查询总量及课酬发生自然变化,未覆盖或回滚这些记录;相同时间点的数据一致性由只读快照对照验证。
+- 最新服务端全量 621/621 通过;上线后的最终30分钟观察已完成:115次抽样请求0失败,三个节点的服务及worker在线且重启计数稳定;详见REPORT.md与发布回执。
 
 Ruling: 后台文件回传采用90秒网络预算,与普通读取的14秒总预算分开;只允许一个导出任务生成并由租约隔离。代价是文件网络异常时后台任务可能等待更久,UI仍可查询并显示进度;阶段日志用于区分网络、网关和处理失败。
+
+- 最终回执:两个隔离schema及1个遗留临时Function已清理;临时本地预览停止,原始发布备份与证据保留。完整汇总见release-validation.json。

+ 49 - 42
perf/cloud-concurrency-2026-11-15/REPORT.md

@@ -1,58 +1,65 @@
-# 云函数请求压力治理:实现与验证
+# 云函数请求压力治理与发布验收
 
-本轮完成首批两端请求调度、训练记录查询治理、可选修订事件迁移、事务准入以及独立后台导出代码。尚未部署,不表示线上已经提速或通过峰值容量验收。
+前端、三个正式网关和三个独立导出 worker 已发布。前端及网关代码版本为 `f815b666e28a918ef3a02ab0988ab4d6e9ed9780`;worker 的 PM2 入口、传输预算和日志修复版本为 `44d9a69`。最终上线观察已于2026-11-16(北京时间)结束并通过:30分钟、115次真实账号抽样请求,0失败;三个节点的主服务及导出 worker 均在线,窗口内重启计数未增加。代码、静态文件和 worker 文件哈希回读一致。
 
-## 已确认的基线
+## 实际改动
 
-2026-11-15 使用现有运营会话,只读、顺序访问生产训练记录首页 10 次。19921 条记录,每次返回 20 条;p50 3172 ms、p75 3282 ms、p95 3574 ms,全部成功。没有向生产施加并发压力。读取 Function 定义接口返回 HTTP 400,因此未核对生产源码、数据库执行计划、现有索引、worker 数量及连接池设置。
+- 管理端、移动端每标签页最多4个业务请求,读请求最多3个、重读最多1个;相同读取合并、取消、会话隔离、有界排队,提交保留容量。普通成功查询最多30秒缓存,本人操作后失效;来源时间与剩余 TTL 防止前后端缓存叠加。
+- 服务端对明确列出的35个重读 operation/action 做跨节点准入。三个节点各保留原有2连接池,正式重读额度按混合负载实测设为3;等候不占连接,SQL 使用事务内截止,常规读取总预算14秒。未知操作和业务写入仍走原有处理流程。
+- 训练列表精确学员筛选提前,保留去重、权限、排序与统计口径;分页、稳定游标、统计短缓存及跨请求读取合并。可选修订事件避免反复扫描来源表 COUNT/MAX,并包含迟提交事务,防止版本漏变和共享计数行死锁。
+- 增加4个经过执行计划和配对测试验证的表达式/关联索引,正式环境以 CONCURRENTLY 建立并核对 valid/ready。
+- 导出使用持久化任务、固定快照、全局单任务租约和独立 Node 20 worker。快照先在 PostgreSQL 落表,JavaScript 每批处理500条;失败保留快照、旧租约隔离、取消、权限重验、24小时到期。进度查询只取元数据,下载才读取文件正文。
+- 大文件在浏览器按256KB Base64分块解码并让出事件循环,下载按钮防重复;后台文件回传网络预算90秒、交互下载120秒。前台只创建任务、查看进度和下载,不循环拉全量数据生成 Excel。
+- 网关与 worker 记录 operation/status/durationMs 和有限错误原因标签,不记录令牌、请求正文或文件内容。
 
-## 实现
+## 测量口径与结果
 
-- 两端统一调度:每应用标签页最多 4 个业务请求,读取最多 3 个,重读取最多 1 个。读取合并、取消、10 秒有界排队(上限 64)、会话隔离、提交优先;未知操作不合并、不缓存、不重试。旧移动端正则读取分类保留,未声称全部 action 已重新审计。
-- 管理端对训练列表、词库树、课酬规则与教学规则启用最长 30 秒成功缓存;移动端对课程列表及 node_list 启用同样缓存。写入结果不确定时也使相应通用缓存失效;手动 refresh 跳过结果缓存。重试最多 1 次、随机退避并遵守服务端 retryAfterMs,客户端执行超时不重试。
-- 训练记录精确学员 ID 提前限制事实查询(去重分区包含学员 ID,语义不变);保留原去重、权限、名称和日期口径。累计统计可按权限/参数/修订版本短暂复用;移除已缓存统计的 SQL 参数空洞。普通分页兼容原接口,分批查看支持精确时间游标及版本核对。
-- 训练列表通过事务级 advisory lock 跨 worker 限制同时 2 个查询,SQL 使用事务内 12 秒 statement_timeout,429 返回 retryAfterMs。此保护目前覆盖训练列表,不代表所有费用/报表重查询都已完成服务端准入迁移。其他接口已受两端通用调度约束。
-- 可选 read-revisions.sql:来源表使用语句触发器记录事务专属修订事件,避免共享计数行造成业务写入锁循环;版本包含 frontier 以下的活跃事务,避免较早事务后提交被 MAX 版本漏掉。覆盖同步写入、删除、帐套移动与 TRUNCATE。worker 每日压缩已稳定事件并清理过期统计。
-- 训练导出:持久化任务及固定快照,独立 Node worker 全服务一次 1 个任务,按 500 条读取生成 XLSX。本人最多 1 个未完成任务,幂等创建、租约、权限重验、取消、三次崩溃恢复、24 小时过期;文件不发布为公开 URL,下载重新鉴权。快照语句单独允许 60 秒,用户请求只提交任务/查看进度;文件上限约 10 MB,超限明确失败。前端进度退避至 10 秒,隐藏时暂停,返回可恢复。
-- 运营网关新增请求标识与 operation/status/durationMs 日志;调度器提供有界计数快照,不记录令牌及业务正文。尚未建立生产 SQL 分段日志与线上指标面板。
+| 验证 | 结果 | 证据边界 |
+| --- | --- | --- |
+| 同一 PostgreSQL 17.9 数据副本,10组 AB/BA 配对 | 全局训练 SQL p75 3484→859ms,降低75.3%;结果哈希一致 | 两份隔离副本的索引对照,不冒充公网页面测速 |
+| 1/5/10/20/50 用户冷启动突发 | 每组全部成功 | 最多50个逻辑消费者,不能当作实际历史峰值 |
+| 30分钟训练读取测试窗口(含冷启动阶梯),50消费者上限、目标10请求/秒、约6万条训练主记录 | 17,673次请求,实际约9.82请求/秒,0失败、0超时;读取 p75 142ms、p95 400ms;轻查询 p95 211ms | 使用真实服务、原连接池和隔离业务副本;关联表保持原采样规模;包含短缓存收益 |
+| 在上述负载上叠加真实管理员/教师/学生混合读取 | 三槽配置5分钟893次,0失败;两槽的失败试验保留 | 涉及训练、会员、教师、课酬、陪练、复习、学习和账户8种读取 |
+| 约6万条独立 Node 20 导出 | 59,794行,约64.7秒;峰值 RSS 815,562,752字节;15,711,484字节文件、13列回读一致 | 隔离副本,实际服务器 worker;不将合成扩容数据当作真实业务新增 |
+| 同一15MB文件的浏览器下载 | 最长主线程任务1719→74ms,59,794行回读一致 | 相同文件的前后浏览器实测 |
+| 正式环境筛选导出 | 68行,后台完成、刷新恢复、查询可用、下载回读通过 | 实际正式网关及 PM2 worker |
+| 正式环境新建全量导出 | 19,937行、13列、7,987,910字节,约30秒一次完成 | 真实公司记录,固定快照;同期间业务仍在新增完课 |
+| 正式库同一只读 REPEATABLE READ 快照对照旧/新列表实现 | 19,937条统计、20条可见记录,规范化哈希一致 | 避免将跨时间新增完课误判为查询口径变化 |
 
-## 配对测量
+原先线上只读基线为19921条、10次、p75 3282ms。上线后公网强制刷新10次,p50 672ms、p75 767ms、p95 907ms;这组观测单独记录,不用不同时段的数字声称配对收益。部分原本很快的接口会增加准入开销,未计为提速收益;混合负载下的指标见 `release-validation.json`。没有可用的实际历史峰值证据,因此本次仅确认上述容量包络,没有宣称完成“历史峰值两倍”的验证。
 
-见 local-sql-benchmark.json。相同隔离 PGlite 数据库,AB/BA 交替,2 次热身后各 10 次,覆盖 2 万/6 万条记录;每次比较原始可见数据与统计一致,新增游标及版本元数据不作旧接口逐字比较。此测试只衡量 SQL 与序列化,不包含真实网络、页面渲染和生产连接池。基准命令:
+## 正确性与检查
 
-```sh
-git show 2503750:scripts/cloud/lesson-records.js > tmp/cloud-concurrency/lesson-records-before.js
-node scripts/benchmark-lesson-history.mjs --baseline=tmp/cloud-concurrency/lesson-records-before.js
-```
+- 服务端621项、管理端408项、移动端419项全部通过;两端生产构建、11份本地云函数定义语法检查和移动 action 审计通过。移动端源码在最终 worker 修复期间未再改动。
+- 已在独立 PostgreSQL 18.4 完成7项多连接验证,包括 READ COMMITTED/REPEATABLE READ 迟提交、原计数器死锁场景、跨连接准入、超时/回滚释放及50连接超载。生产 PostgreSQL 原生验证确认预算可取消 SQL、事务/连接可复用、参数安全转义、空/多行语义及事务隔离级别兼容。
+- 独立审查发现的强制刷新风暴、无关写函数被覆盖等问题已修复并回归;发布脚本保留 runtime 标记后的晚期修复,逐个网关做发布前哈希校验、定向替换和发布后回读。
+- 真实管理员、教师、学生浏览器路径均验证;移动布局为390px浏览器视口,非实体手机。线上验证没有发送开课、完课、扣课时或结算操作。
+- 正式发布期间出现新的 `complete:` 来源完课记录,因此跨时间总量、教师课酬及排序会自然变化;没有覆盖这些记录。业务写入函数保持原线上实现,相同快照的可见训练结果另外作了对照。
 
-按学员查询 p75 显著降低;全局翻页仍未达到降低 50% 的目标,精确数字以配对 JSON 为准。新增的前端重复请求合并、返回复用不在该 SQL 基准中计收益。
+## 失败试验与修正
 
-真实 PostgreSQL 验证依赖只安装在独立临时目录,未改项目依赖:
+1. 初期 Function 定义读取400已通过既有发布凭据解决,没有据此猜测生产配置。
+2. 原全量 JS 快照实验曾超内存;改为数据库内快照、500条分批处理,远端6万数据验收通过。
+3. 独立读池的启动超时参数被数据库代理拒绝;实验撤回,临时池已关闭,原连接的超时设置核对恢复为0。固定单连接也会串行化页面读取,因此最终沿用原池,以一次协议交换执行事务控制和 SQL。
+4. 一轮预算重测在约13分钟遇到503并停止注入。进程管理日志显示一个 server 在22:33:11被停止、启动,该时刻没有内存超限记录;该轮没有计为通过,后来完整30分钟重测通过。
+5. PM2 的进程入口与直接 CLI 不同,曾导致服务“在线”却不取任务;已修复并验证直接运行、PM2及继承 PM2 环境的 CLI。
+6. 首次正式全量任务回传多次中断,原快照重领后19,934行回读成功。新增阶段日志和后台传输预算后,另建的19,937行全量任务一次完成。新日志中的 finish 为200、约1102ms;这些结果不能反推早先中断的确切原因。
 
-```sh
-npm install --prefix /tmp/xiaoshu-postgres-validation embedded-postgres@18.4.0-beta.17 pg --no-audit --no-fund
-node scripts/verify-read-concurrency.mjs
-```
+## 发布及回滚
 
-验证仅绑定 127.0.0.1、使用随机临时数据库目录和口令,finally 关闭并删除该集群,不创建系统用户、不连接生产。
+- 发布顺序为元数据迁移、三槽配置、4个并发索引、三个网关、三节点 worker、两端静态资源,最后刷新 CDN。没有清空静态桶,保留旧哈希资源及原入口备份。
+- 管理端96个、H5 105个文件的公网内容哈希、源站/公网部署清单、两端深链接已核对。两个 CDN 任务均完成且无失败。
+- 网关原始定义、逐次候选及哈希、前端原入口/资源、worker文件哈希和进程回执均存私有发布目录;仓库只保留无个人数据和令牌的汇总。
+- 回滚先暂停新导出,等待或取消未完成任务,停止本次 worker,再恢复旧前端和网关。新增索引和审计/任务表保留,避免删除业务数据;所有新消费者撤回后才能移除修订触发器。恢复时仍校验当前哈希,避免覆盖其他发布。
+- 用户已在本聊天明确授权“达到要求后直接上线”;本次没有新增授权等待环节。
 
-## 测试与边界
+## 采用的取舍
 
-已覆盖调度器的 50 个并发消费者、保留提交容量、共享读取取消、会话过期、队列超时、缓存 TTL/失效隔离;训练记录去重/权限/排序、游标变化检测;导出快照固定、旧租约隔离、worker 恢复和完整 XLSX 回读;门店只允许本人任务操作、worker 操作关闭;修订的插入/删除/移动/回滚/清理。
+- 事务专属修订事件替代共享计数行,避免业务死锁;代价是需定期清理事件。此前缺乏生产证据时不猜扩容;取得真实拓扑和测量后才设定三槽及4个索引,代价是多做了隔离验证。
+- 普通查询最多30秒短缓存、后端按权限和修订区分;代价是普通视图允许短暂缓存。费用和业务提交的实时校验继续执行。
+- 重读沿用原连接池、三槽准入和短 SQL 预算;代价是快接口有少量准入开销,超载时明确繁忙,不能保证任意并发都立即成功。
+- 导出允许60秒快照 SQL、90秒后台传输及500条分批落表;代价是临时存储和较长后台等待。没有把后台预算放宽应用到普通查询。
 
-独立审查发现并修复移动端登录新令牌尚未保存时被拦截、门店导出能力缺失、共享修订计数锁循环以及取消/完成竞态。最后另修复 firstValueFrom 成功响应被误计为取消的计数问题,7 项调度器测试及 23 项管理端传输测试通过;最终运行数字见 VALIDATION.json。
+私有完整证据目录:`/Users/mac/.local/state/xiaoshu/qa/2026-11-15-cloud-concurrency-final/`。最终线上观察、组件哈希和清理回执见同目录及本目录汇总 JSON。
 
-已在本机隔离 PostgreSQL 18.4 完成 7 个多连接检查:READ COMMITTED/REPEATABLE READ 下迟提交版本失效、原计数器锁循环场景、两槽跨连接准入、回滚/超时释放、50 连接超载和事件清理;结果见 postgres-concurrency.json。50 连接同时突发时 2 个进入、48 个快速繁忙,证明保护生效,不代表满足 50 用户容量目标。
-
-尚未完成:生产 SQL 执行计划与索引核对、按实际峰值的 1/5/10/20/50 用户压测及 30 分钟稳定性测试、真实老师/学生设备验收、全局冷查询减半目标。因此当前不具备声称全计划验收通过的证据。
-
-## 发布次序与回滚
-
-1. 已完成隔离真实 PostgreSQL 多连接正确性验证,仍需按生产数据和实际请求节奏测定合适重查询额度;当前 2 只是初始值,不是已证明的生产容量。不得直接执行已有 ensure-app-performance-indexes 脚本作为盲目补索引。
-2. 对比/备份生产两个运营网关与手机网关,保留线上额外修复;不要全量覆盖未经核对的生产定义。原始定义读取尚未成功,需具备发布权限后核对。
-3. 执行 read-revisions.sql 与 lesson-export.sql,先迁移再发布两个运营网关和手机/后台前端。只部署本次已审阅变更。
-4. 在现有 /opt/xiaoshu-angular 安装同版本依赖,配置 /etc/xiaoshu/lesson-export.env(0600,专用超级管理员会话),启用 deployment/systemd/xiaoshu-lesson-export.service;先用 --once 完成一个受控导出验收。worker 未就绪前不开放新前端导出入口。
-5. 灰度后核验训练列表、普通分页、分批查看、后台导出、教学提交、费用实时核验;记录超时/繁忙/提交成功率并观察 30 分钟。财务 settlementReady 仍只由原实时校验决定。
-6. 回滚前暂停接收新导出并等待或取消未完成任务;恢复旧前端和网关、停止导出 worker。新增任务/事件表保留作审计,不删除源业务数据。只有确认所有新版本消费者已撤回后,才移除 xs_revision_* 触发器;迁移不替换原业务函数。
-
-上线需按此前约定另行批准。本轮没有修改线上配置、云函数定义或业务数据。
+最终清理:两份隔离数据库副本和1个遗留临时函数已删除;本地临时预览已停止。保留正式任务的24小时文件、发布备份、测试证据及 Codex 管理的工作树。

+ 12 - 19
perf/cloud-concurrency-2026-11-15/VALIDATION.json

@@ -1,7 +1,8 @@
 {
-  "date": "2026-11-15",
+  "date": "2026-11-16",
+  "releaseDate": "2026-11-15",
   "adminTests": {
-    "passed": 405,
+    "passed": 408,
     "failed": 0
   },
   "mobileTests": {
@@ -9,7 +10,7 @@
     "failed": 0
   },
   "serverTests": {
-    "passed": 611,
+    "passed": 621,
     "failed": 0
   },
   "builds": [
@@ -18,21 +19,13 @@
   ],
   "cloudDefinitions": 11,
   "postgresMultiConnectionChecks": 7,
-  "productionDeployment": false,
-  "productionCapacityAcceptance": false,
-  "globalColdQueryHalfTargetMet": false,
-  "focusedFinalTests": {
-    "passed": 45,
-    "failed": 0
-  },
-  "adminFinalFocusedTests": {
-    "passed": 35,
-    "failed": 0
+  "productionDeployment": true,
+  "productionCapacityAcceptance": {
+    "status": "passed-defined-envelope",
+    "historicalPeakVerified": false
   },
-  "integrationBase": "b1af307",
-  "postIntegrationCountersRegression": {
-    "schedulerTests": 7,
-    "adminTransportTests": 23,
-    "failed": 0
-  }
+  "globalColdQueryHalfTargetMet": true,
+  "postReleaseObservationPassed": true,
+  "physicalDeviceTested": false,
+  "details": "release-validation.json"
 }

+ 536 - 0
perf/cloud-concurrency-2026-11-15/release-validation.json

@@ -0,0 +1,536 @@
+{
+  "sourceCommits": {
+    "frontendAndGateway": "f815b666e28a918ef3a02ab0988ab4d6e9ed9780",
+    "worker": "44d9a699aa3102d9c3ce10151d6fd00d05a15e33"
+  },
+  "tests": {
+    "server": {
+      "passed": 621,
+      "failed": 0
+    },
+    "admin": {
+      "passed": 408,
+      "failed": 0
+    },
+    "mobile": {
+      "passed": 419,
+      "failed": 0
+    },
+    "builds": [
+      "admin",
+      "mobile"
+    ],
+    "cloudDefinitionSyntax": 11,
+    "postgresMultiConnection": 7
+  },
+  "sqlPairedBenchmark": {
+    "scope": "isolated PostgreSQL 17.9 copies; ten AB/BA pairs after two warmups",
+    "summaries": {
+      "before": {
+        "samples": 10,
+        "p50": 2983,
+        "p75": 3484,
+        "p95": 3677
+      },
+      "after": {
+        "samples": 10,
+        "p50": 569,
+        "p75": 859,
+        "p95": 1053
+      }
+    },
+    "businessHashesIdentical": true,
+    "p75Reduction": 0.7534443168771527
+  },
+  "capacity": {
+    "scope": "defined acceptance envelope, historical real peak unknown",
+    "maxConsumers": 50,
+    "requestsPerSecond": 10,
+    "startedAt": "2026-11-15T14:36:29.761Z",
+    "finishedAt": "2026-11-15T15:06:29.960Z",
+    "elapsedMs": 1800182,
+    "issued": 17673,
+    "finished": 17673,
+    "maxActive": 38,
+    "failures": 0,
+    "p75": 142,
+    "p95": 400,
+    "controlP95": 211,
+    "actualRequestsPerSecond": 9.817,
+    "mainRecords": 60039,
+    "canonicalRecords": 59794,
+    "associatedTablesScale": "original captured copy; not all associated tables tripled"
+  },
+  "mixedLoad": {
+    "startedAt": "2026-11-15T14:45:54.589Z",
+    "durationMs": 300285,
+    "requestedDurationMs": 300000,
+    "rps": 3,
+    "issued": 893,
+    "maxActive": 5,
+    "failed": 0,
+    "operations": [
+      {
+        "role": "admin",
+        "operation": "ops/lessons/list",
+        "count": 112,
+        "p75": 189,
+        "p95": 1464
+      },
+      {
+        "role": "admin",
+        "operation": "ops/users/list",
+        "count": 112,
+        "p75": 607,
+        "p95": 839
+      },
+      {
+        "role": "admin",
+        "operation": "ops/coaches/list",
+        "count": 112,
+        "p75": 207,
+        "p95": 374
+      },
+      {
+        "role": "admin",
+        "operation": "ops/payroll/overview",
+        "count": 112,
+        "p75": 796,
+        "p95": 1094
+      },
+      {
+        "role": "teacher",
+        "operation": "app_companion_overview",
+        "count": 112,
+        "p75": 225,
+        "p95": 339
+      },
+      {
+        "role": "teacher",
+        "operation": "app_review_page",
+        "count": 111,
+        "p75": 244,
+        "p95": 421
+      },
+      {
+        "role": "student",
+        "operation": "app_learning_overview",
+        "count": 111,
+        "p75": 591,
+        "p95": 866
+      },
+      {
+        "role": "student",
+        "operation": "app_account_overview",
+        "count": 111,
+        "p75": 799,
+        "p95": 1270
+      }
+    ]
+  },
+  "exports": {
+    "isolated60k": {
+      "code": 0,
+      "ms": 64731,
+      "maxRss": 815562752,
+      "total": 59794,
+      "processed": 59794,
+      "status": "ready"
+    },
+    "isolatedWorkbook": {
+      "status": "ready",
+      "total": 59794,
+      "processed": 59794,
+      "workbookRows": 59794,
+      "bytes": 15711484,
+      "columns": 13
+    },
+    "browserLargeFile": {
+      "downloadDuplicateGuard": true,
+      "workbookRows": 59794,
+      "bytes": 15711484,
+      "longTasks": [
+        74
+      ],
+      "maxLongTaskMs": 74,
+      "status": "ready",
+      "downloads": 1,
+      "errors": []
+    },
+    "browserBaselineLongestTaskMs": 1719,
+    "publicFiltered": {
+      "at": "2026-11-15T15:19:42.496Z",
+      "status": "ready",
+      "rows": 68,
+      "total": 68,
+      "bytes": 33969,
+      "resumeAfterReload": true,
+      "backgroundWorker": true,
+      "queryResponsive": true,
+      "errors": [],
+      "blocked": [],
+      "financialWrites": 0
+    },
+    "publicFull": {
+      "checkedAt": "2026-11-15T15:39:46.832Z",
+      "status": "ready",
+      "total": 19937,
+      "processed": 19937,
+      "workbookRows": 19937,
+      "columns": 13,
+      "bytes": 7987910,
+      "ms": 30079
+    },
+    "earlierFullTransferFailures": "Retained in private logs; fixed snapshot retried successfully; exact earlier failure cause not established."
+  },
+  "publicSameSnapshotGolden": {
+    "same": true,
+    "total": 19937,
+    "items": 20,
+    "beforeHash": "e8bc131b67a3c15428402cd6562da34a15f3474fa7438e8360b600f79b13ba12",
+    "afterHash": "e8bc131b67a3c15428402cd6562da34a15f3474fa7438e8360b600f79b13ba12"
+  },
+  "onlineQueryObservation": {
+    "at": "2026-11-15T15:44:14.654Z",
+    "scope": "public API after release, forced refresh, not simultaneous baseline comparison",
+    "samples": [
+      {
+        "ms": 643,
+        "total": 19937,
+        "items": 20
+      },
+      {
+        "ms": 767,
+        "total": 19937,
+        "items": 20
+      },
+      {
+        "ms": 754,
+        "total": 19937,
+        "items": 20
+      },
+      {
+        "ms": 629,
+        "total": 19937,
+        "items": 20
+      },
+      {
+        "ms": 716,
+        "total": 19937,
+        "items": 20
+      },
+      {
+        "ms": 907,
+        "total": 19937,
+        "items": 20
+      },
+      {
+        "ms": 672,
+        "total": 19937,
+        "items": 20
+      },
+      {
+        "ms": 669,
+        "total": 19937,
+        "items": 20
+      },
+      {
+        "ms": 643,
+        "total": 19937,
+        "items": 20
+      },
+      {
+        "ms": 837,
+        "total": 19937,
+        "items": 20
+      }
+    ],
+    "p50": 672,
+    "p75": 767,
+    "p95": 907
+  },
+  "frontends": [
+    {
+      "role": "admin",
+      "releaseId": "admin-cloud-pressure-f815b666e28a-20261115T150155657Z",
+      "commit": "f815b666e28a918ef3a02ab0988ab4d6e9ed9780",
+      "filesVerified": 96,
+      "routes": [
+        "admin/lessons",
+        "admin/members",
+        "admin/learning-reports",
+        "admin/login",
+        "admin/payroll"
+      ]
+    },
+    {
+      "role": "mobile",
+      "releaseId": "mobile-cloud-pressure-f815b666e28a-20261115T150158325Z",
+      "commit": "f815b666e28a918ef3a02ab0988ab4d6e9ed9780",
+      "filesVerified": 105,
+      "routes": [
+        "words/reading",
+        "words/reading/pdf/readonly-check",
+        "pages/home/index",
+        "pages/his/his",
+        "pages/companion/index"
+      ]
+    }
+  ],
+  "gateways": [
+    {
+      "name": "app",
+      "hash": "b9a93b478ba6a046f2e680a1add54f0347173d648f7527e34ad07cda8c3f3f7d",
+      "unrelatedFunctionsPreserved": true
+    },
+    {
+      "name": "ops",
+      "hash": "c4ace02b182ee45104d19587f005db660f6641a40b43cb8fb828b255eeadb9c0",
+      "unrelatedFunctionsPreserved": true
+    },
+    {
+      "name": "ops-v3",
+      "hash": "53d58c5097c544ee58f329021e9c8461493c1c260a0e385cbdb070884069659a",
+      "unrelatedFunctionsPreserved": true
+    }
+  ],
+  "indexes": [
+    {
+      "relname": "xs_history_report_appointment",
+      "indisvalid": true,
+      "indisready": true,
+      "definition": "CREATE INDEX xs_history_report_appointment ON public.\"LessonCompletionReport\" USING btree (company, ((\"appointmentGeneralId\")::text), \"submittedAt\" DESC)"
+    },
+    {
+      "relname": "xs_history_report_lesson",
+      "indisvalid": true,
+      "indisready": true,
+      "definition": "CREATE INDEX xs_history_report_lesson ON public.\"LessonCompletionReport\" USING btree (company, \"lessonId\", \"submittedAt\" DESC)"
+    },
+    {
+      "relname": "xs_history_review_student",
+      "indisvalid": true,
+      "indisready": true,
+      "definition": "CREATE INDEX xs_history_review_student ON public.\"MemoryPracticeRecord\" USING btree (company, ((yhid)::text), \"objectId\")"
+    },
+    {
+      "relname": "xs_history_user_legacy",
+      "indisvalid": true,
+      "indisready": true,
+      "definition": "CREATE INDEX xs_history_user_legacy ON public.\"_User\" USING btree (((company)::text), ((\"legacyUserId\")::text), \"objectId\")"
+    }
+  ],
+  "admission": {
+    "concurrency": 3
+  },
+  "reportReadSmoke": [
+    {
+      "role": "teacher",
+      "action": "app_lesson_report",
+      "status": 200,
+      "ms": 546,
+      "success": true
+    },
+    {
+      "role": "teacher",
+      "action": "app_lesson_study",
+      "status": 200,
+      "ms": 146,
+      "success": true
+    },
+    {
+      "role": "student",
+      "action": "app_lesson_report",
+      "status": 200,
+      "ms": 209,
+      "success": true
+    },
+    {
+      "role": "student",
+      "action": "app_lesson_study",
+      "status": 200,
+      "ms": 136,
+      "success": true
+    }
+  ],
+  "workerFiles": {
+    "at": "2026-11-15T16:03:10.798Z",
+    "expected": {
+      "worker.mjs": "7bc2fdc2c87a9a3a2d09028857de3ced3a695101ec8d5fa9499445c050f913db",
+      "lesson-export-columns.mjs": "9d477c2c85e7947e5e20004c0692182160d122169ee69787b781f1521c746249",
+      "xlsx.mjs": "1a0fb062ee9781b13f6687371b202aaefc53b6ce55b530c027e01f9c087b77db"
+    },
+    "nodes": [
+      {
+        "host": "8ce544ccc29b",
+        "files": {
+          "worker.mjs": "7bc2fdc2c87a9a3a2d09028857de3ced3a695101ec8d5fa9499445c050f913db",
+          "lesson-export-columns.mjs": "9d477c2c85e7947e5e20004c0692182160d122169ee69787b781f1521c746249",
+          "xlsx.mjs": "1a0fb062ee9781b13f6687371b202aaefc53b6ce55b530c027e01f9c087b77db"
+        },
+        "worker": {
+          "status": "online",
+          "restarts": 4,
+          "memory": 90705920,
+          "nice": 10,
+          "maxMemory": "2147483648",
+          "nodeArgs": [
+            "--max-old-space-size=512"
+          ],
+          "environmentFileMode": 384
+        },
+        "server": {
+          "status": "online",
+          "restarts": 3,
+          "memory": 1251024896
+        }
+      },
+      {
+        "host": "46d239c56f0b",
+        "files": {
+          "worker.mjs": "7bc2fdc2c87a9a3a2d09028857de3ced3a695101ec8d5fa9499445c050f913db",
+          "lesson-export-columns.mjs": "9d477c2c85e7947e5e20004c0692182160d122169ee69787b781f1521c746249",
+          "xlsx.mjs": "1a0fb062ee9781b13f6687371b202aaefc53b6ce55b530c027e01f9c087b77db"
+        },
+        "worker": {
+          "status": "online",
+          "restarts": 4,
+          "memory": 85708800,
+          "nice": 10,
+          "maxMemory": "1073741824",
+          "nodeArgs": [
+            "--max-old-space-size=512"
+          ],
+          "environmentFileMode": 384
+        },
+        "server": {
+          "status": "online",
+          "restarts": 93,
+          "memory": 888586240
+        }
+      },
+      {
+        "host": "57d23410df62",
+        "files": {
+          "worker.mjs": "7bc2fdc2c87a9a3a2d09028857de3ced3a695101ec8d5fa9499445c050f913db",
+          "lesson-export-columns.mjs": "9d477c2c85e7947e5e20004c0692182160d122169ee69787b781f1521c746249",
+          "xlsx.mjs": "1a0fb062ee9781b13f6687371b202aaefc53b6ce55b530c027e01f9c087b77db"
+        },
+        "worker": {
+          "status": "online",
+          "restarts": 4,
+          "memory": 228777984,
+          "nice": 10,
+          "maxMemory": "1572864000",
+          "nodeArgs": [
+            "--max-old-space-size=512"
+          ],
+          "environmentFileMode": 384
+        },
+        "server": {
+          "status": "online",
+          "restarts": 11,
+          "memory": 721108992
+        }
+      }
+    ]
+  },
+  "browsers": {
+    "roles": [
+      {
+        "role": "admin",
+        "maxActive": 1,
+        "errors": [],
+        "blocked": []
+      },
+      {
+        "role": "teacher",
+        "maxActive": 2,
+        "errors": [],
+        "blocked": []
+      },
+      {
+        "role": "student",
+        "maxActive": 3,
+        "errors": [],
+        "blocked": []
+      }
+    ],
+    "physicalDevice": false
+  },
+  "postReleaseObservation": {
+    "status": "passed",
+    "requests": 115,
+    "failures": 0,
+    "elapsedMs": 1804907,
+    "startedAt": "2026-11-15T15:39:18.146Z",
+    "finishedAt": "2026-11-15T16:09:23.053Z",
+    "nodes": {
+      "46d239c56f0b": {
+        "server": {
+          "restartsMin": 93,
+          "restartsMax": 93,
+          "memoryMax": 1002651648,
+          "statuses": [
+            "online"
+          ]
+        },
+        "xiaoshu-lesson-export": {
+          "restartsMin": 4,
+          "restartsMax": 4,
+          "memoryMax": 87244800,
+          "statuses": [
+            "online"
+          ]
+        }
+      },
+      "8ce544ccc29b": {
+        "server": {
+          "restartsMin": 3,
+          "restartsMax": 3,
+          "memoryMax": 1280188416,
+          "statuses": [
+            "online"
+          ]
+        },
+        "xiaoshu-lesson-export": {
+          "restartsMin": 4,
+          "restartsMax": 4,
+          "memoryMax": 92717056,
+          "statuses": [
+            "online"
+          ]
+        }
+      },
+      "57d23410df62": {
+        "server": {
+          "restartsMin": 11,
+          "restartsMax": 11,
+          "memoryMax": 793759744,
+          "statuses": [
+            "online"
+          ]
+        },
+        "xiaoshu-lesson-export": {
+          "restartsMin": 4,
+          "restartsMax": 4,
+          "memoryMax": 230109184,
+          "statuses": [
+            "online"
+          ]
+        }
+      }
+    }
+  },
+  "cleanup": {
+    "isolatedSchemasAbsent": true,
+    "orphanedCopyFunctionsDeleted": 1,
+    "localTemporaryPreviewStopped": true
+  },
+  "evidenceBoundary": {
+    "productionBusinessWritesSent": 0,
+    "liveBusinessRecordsContinuedChanging": true,
+    "historicalPeakVerified": false,
+    "fullAssociatedDataTriplingVerified": false
+  }
+}