summarize.mjs 6.3 KB

123456789101112
  1. import fs from 'node:fs';import assert from 'node:assert/strict';
  2. const input='tmp/admin-speed-round2-2026-11-10',output='perf/admin-speed-round2-2026-11-10';
  3. const percentile=(values,p)=>[...values].sort((a,b)=>a-b)[Math.ceil(values.length*p)-1];
  4. function summarize(file,metric){const rows=JSON.parse(fs.readFileSync(input+'/'+file)),pages=[...new Set(rows.map(r=>r.page))];return pages.map(page=>{const group=rows.filter(r=>r.page===page),before=group.filter(r=>r.variant==='before'),after=group.filter(r=>r.variant==='after');assert.equal(before.length,10);assert.equal(after.length,10);for(const old of before)assert.equal(old.hash,after.find(r=>r.round===old.round).hash,'Business output mismatch '+page+' '+old.round);const stats=list=>Object.fromEntries([['p50',.5],['p75',.75],['p95',.95]].map(([key,p])=>[key,percentile(list.map(r=>r[metric]),p)]));const a=stats(before),b=stats(after);return{page,samplesPerVariant:10,allBusinessPairsEqual:true,beforeMs:a,afterMs:b,reductionPercent:Math.round((1-b.p75/a.p75)*1000)/10,...(metric==='readyMs'?{coachDetailP75:page==='payroll'?percentile(after.map(r=>r.coachDetailMs),.75):undefined,primaryRequests:{before:[...new Set(before.map(r=>r.primaryRequests))],after:[...new Set(after.map(r=>r.primaryRequests))]},resourceRequestsP75:{before:percentile(before.map(r=>r.resources.requests),.75),after:percentile(after.map(r=>r.resources.requests),.75)},resourceBytesP75:{before:percentile(before.map(r=>r.resources.bytes),.75),after:percentile(after.map(r=>r.resources.bytes),.75)}}:{afterCacheHitCount:after.filter(r=>r.cacheHit).length})};});}
  5. const result={at:new Date().toISOString(),protocol:'10 alternating paired samples; p75 nearest rank; browser new contexts and identical production assets/gzip/20 ms/4 Mbps down/3 Mbps up; full verified summary plus separately opened exact coach detail',browser:summarize('browser-final-paired.json','readyMs'),apiFullRebuild:summarize('api-cold-paired.json','ms')};
  6. fs.writeFileSync(output+'/summary.json',JSON.stringify(result,null,2)+'\n');
  7. const rows=JSON.parse(fs.readFileSync(input+'/browser-final-paired.json'));for(const r of rows.filter(r=>r.variant==='after')){assert.equal(r.primaryRequests,1);assert.deepEqual(r.errors,[]);}
  8. fs.writeFileSync(output+'/ratchet.json',JSON.stringify({businessEquality:true,primaryRequests:{payroll:1,dashboard:1,coaches:1},payrollSummaryLines:0,exactCoachDetailChecked:true,fullRebuildMustNotHitCache:true,thresholds:result.browser.map(r=>({page:r.page,p75Ms:r.afterMs.p75})),note:'Counts and business equality are gates. Repeated paired measurements are required for timing regression; a single noisy sample is not a CI failure.'},null,2)+'\n');
  9. console.log(JSON.stringify(result,null,2));
  10. const b=result.browser,c=result.apiFullRebuild[0],sec=n=>(n/1000).toFixed(2),labels={payroll:'费用页完整核验',dashboard:'工作台',coaches:'陪练老师'};
  11. const lines=['# 管理后台第二轮提速验收','','同条件生产构建、每组 10 轮 AB/BA 交替;费用终点为真实汇总和完整来源核验均可用,老师明细另行实际打开。不是金额占位、跳过核验或旧值秒开。','','| 操作 | 优化前 p75 | 优化后 p75 | 降幅 |','|---|---:|---:|---:|',...b.map(r=>`| ${labels[r.page]} | ${sec(r.beforeMs.p75)} 秒 | ${sec(r.afterMs.p75)} 秒 | ${r.reductionPercent}% |`),'',`费用页按需打开老师明细 p75:${sec(b.find(r=>r.page==='payroll').coachDetailP75)} 秒。所有 30 组浏览器业务比较一致;费用比较保留全部汇总/核验字段,并将真正点击取得的老师明细与旧版对应原始行逐条比较。API 全量响应另有 10 组完全重建比较,全部一致。`,'','## 速度边界','',`上述为来源版本未变、可复用已核验结果的情况;每次都检查当前事务变更和依赖。清空本帐套所有结果缓存后,完整重算接口 p75 为 ${sec(c.beforeMs.p75)} → ${sec(c.afterMs.p75)} 秒,10 次均未命中缓存。原始来源/政策变动导致失效后的首次查询仍可能需要数秒,不能声称所有情况都已达到 1 秒。`,'','浏览器为新上下文、相同 gzip、20 ms 延迟、4 Mbps 下行/3 Mbps 上行。原始响应及截图只保留在被 Git 忽略的 tmp 目录。没有公开 CrUX/真实用户分位统计;线上逐页检查属于生产冒烟,不能与受控 p75 混为一谈。','','## 实现与准确性','','- 事务内维护准确行摘要,将约 51 万条来源记录的重复全文哈希移出读路径;内容、排序和原校验值保持一致。','- 用数据库事务快照验证派生结果,检测新增、修改、删除、作废、回滚、跨租户移动和乱序提交;不是仅靠有效期或最大更新时间。','- 费率、规则、人工调整、冲突、核验凭据、用户与门店等依赖变化即失效。触发器停用、结构/函数/环境变化会回退原始计算;修复维护时更换代次避免旧缓存复活。','- 首屏传完整已核验的 28 位老师汇总;1,484 条明细改为点击后按老师读取。明细请求成功时同时更新对应的当前汇总,避免不同版本混用。导出强制读取完整数据,不受展示裁剪参数影响。','- 生成、重算、审核、发放仍采用原始财务检查。未确认的差异仍会阻止结算,本次不修改财务业务记录。','','## 验证','','- 后端定向测试 46 项、管理端 361 项通过;11 个云函数定义校验、管理端生产构建通过。构建仅有已有样式预算和 html2pdf 提示。','- 真实数据库隔离 schema 验证乱序提交、提交可见性、拒绝过期发布、回滚和来源范围失效;结束后已删除隔离 schema。','- 原始 sources/source_version/cutoff_snapshot/cutoff_conflicts 定义未变。派生设施卸载已实际事务回滚演练。','- 两次独立代码审查无剩余 Critical/Important 项;页面两种宽度截图与基线布局一致。窄桌面下既有筛选栏溢出未在本轮调整。','- 最终发布版本及公网校验见 release-verification.json;完整回滚方式见 README.md。','','性能回归门禁见 ratchet.json:真实业务等价、每页首屏主要读取仅 1 次、首屏无明细传输、明细准确性以及完整重算不得伪命中缓存。时间回归使用重复配对测量,不用一次随机样本直接判错。',''];
  12. fs.writeFileSync(output+'/验收报告.md',lines.join('\n'));