benchmark-teacher-review.mjs 3.6 KB

1234567891011121314151617181920212223242526272829
  1. #!/usr/bin/env node
  2. // Local SQL benchmark only; never connects to production or models artificial network delay.
  3. import {readFileSync,writeFileSync,mkdirSync} from 'node:fs';
  4. import {PGlite} from '@electric-sql/pglite';
  5. import vm from 'node:vm';
  6. const baseline=process.argv.find(a=>a.startsWith('--baseline='))?.slice(11);
  7. if(!baseline)throw Error('需要 --baseline=/path/to/original/mobile-teaching.js');
  8. const db=new PGlite();
  9. try{
  10. await db.exec(readFileSync(new URL('./tests/fixtures/teacher-review-schema.sql',import.meta.url),'utf8'));
  11. await db.exec(`TRUNCATE "MemoryPracticeRecord","CommonModel";
  12. INSERT INTO "MemoryPracticeRecord" SELECT 'r'||i,'co',i,30,20+i%100,100+i%100,1,CASE WHEN i%4=0 THEN '1' ELSE '0' END,CASE WHEN i<=40 THEN '2026-10-14' WHEN i<=80 THEN '2026-10-13' ELSE to_char(date '2026-10-20'+(i%365),'YYYY-MM-DD') END,to_char(date '2026-10-20'+(i%365),'YYYY-MM-DD')||' 10:00:00' FROM generate_series(1,10000) i;
  13. INSERT INTO "CommonModel" SELECT company,60,id+1000,id FROM "MemoryPracticeRecord";
  14. CREATE VIEW "ReviewReward" AS SELECT * FROM "ReviewRewardIncome";
  15. CREATE INDEX ON "CommonModel" (company,"modelId","itemId");
  16. CREATE INDEX ON "CommonModel" (company,"modelId","generalId");
  17. CREATE INDEX ON "DailyStudyRecord" (company,id);
  18. CREATE INDEX ON "MemoryPracticeRecord" (company,yhid,kywsj);
  19. CREATE INDEX ON "MemoryPracticeRecord" (company,plid,fxzt);
  20. CREATE INDEX ON "MemoryPracticeRecord" (company,id); ANALYZE;`);
  21. const make=code=>{const queries=[];const query=async(sql,args)=>{const t=performance.now();const rows=(await db.query(sql,args)).rows;queries.push({ms:performance.now()-t,sql});return rows;};const c=vm.createContext({Date,Promise,DEFAULT_COMPANY_ID:'co',number:(v,f=0)=>Number(v)||f,authorizeRequestedUser:async()=>30,appOverviewToday:()=> '2026-10-14',legacyAliases:r=>r,appLiveVersionSnapshot:async()=>({}),reviewIncomeTotal:async()=>Number((await query('SELECT COALESCE(sum(amount),0) AS n FROM "ReviewRewardIncome" WHERE company=$1',[ 'co']))[0].n)});c.Psql={query,one:async(s,a)=>(await query(s,a))[0]};vm.runInContext(code,c);return{c,queries};};
  22. const variants={before:make(readFileSync(baseline,'utf8')),after:make(readFileSync(new URL('./cloud/mobile-teaching.js',import.meta.url),'utf8'))};
  23. const samples={before:[],after:[]};
  24. for(let i=0;i<24;i++)for(const key of i%2?['after','before']:['before','after']){const v=variants[key];v.queries.length=0;const t=performance.now();const r=await v.c.appReviewPage(key==='before'?{status:'pending',pageSize:20}:{scope:'today',pageSize:4,includeMetadata:false,includeIncome:false},{get:()=>3});const sample={ms:performance.now()-t,queries:v.queries.length,bytes:Buffer.byteLength(JSON.stringify(r)),items:r.items.length,todayItems:r.items.filter(i=>i.kywrq==='2026-10-14').length,metadataDates:r.dates.length};if(i>=4)samples[key].push(sample);}
  25. const quantile=(a,p)=>[...a].sort((x,y)=>x-y)[Math.ceil(a.length*p)-1];
  26. const summary=Object.fromEntries(Object.entries(samples).map(([k,rows])=>[k,{runs:rows.length,p50:quantile(rows.map(r=>r.ms),.5),p75:quantile(rows.map(r=>r.ms),.75),p95:quantile(rows.map(r=>r.ms),.95),queries:rows[0].queries,bytes:rows[0].bytes,items:rows[0].items,todayItems:rows[0].todayItems,errors:0}]));
  27. const result={scope:'local PGlite appReviewPage SQL + serialization, 10000 tasks; excludes real network, live-version queries and browser rendering',generatedAt:new Date().toISOString(),baseline,alternating:true,warmupPerVersion:4,summary,samples};
  28. mkdirSync('tmp/teacher-earnings-qa',{recursive:true});writeFileSync('tmp/teacher-earnings-qa/sql-benchmark.json',JSON.stringify(result,null,2));console.log(JSON.stringify(summary,null,2));
  29. }finally{await db.close();}