#!/usr/bin/env node // Local SQL benchmark only; never connects to production or models artificial network delay. import {readFileSync,writeFileSync,mkdirSync} from 'node:fs'; import {PGlite} from '@electric-sql/pglite'; import vm from 'node:vm'; const baseline=process.argv.find(a=>a.startsWith('--baseline='))?.slice(11); if(!baseline)throw Error('需要 --baseline=/path/to/original/mobile-teaching.js'); const db=new PGlite(); try{ await db.exec(readFileSync(new URL('./tests/fixtures/teacher-review-schema.sql',import.meta.url),'utf8')); await db.exec(`TRUNCATE "MemoryPracticeRecord","CommonModel"; 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; INSERT INTO "CommonModel" SELECT company,60,id+1000,id FROM "MemoryPracticeRecord"; CREATE VIEW "ReviewReward" AS SELECT * FROM "ReviewRewardIncome"; CREATE INDEX ON "CommonModel" (company,"modelId","itemId"); CREATE INDEX ON "CommonModel" (company,"modelId","generalId"); CREATE INDEX ON "DailyStudyRecord" (company,id); CREATE INDEX ON "MemoryPracticeRecord" (company,yhid,kywsj); CREATE INDEX ON "MemoryPracticeRecord" (company,plid,fxzt); CREATE INDEX ON "MemoryPracticeRecord" (company,id); ANALYZE;`); 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};}; const variants={before:make(readFileSync(baseline,'utf8')),after:make(readFileSync(new URL('./cloud/mobile-teaching.js',import.meta.url),'utf8'))}; const samples={before:[],after:[]}; 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);} const quantile=(a,p)=>[...a].sort((x,y)=>x-y)[Math.ceil(a.length*p)-1]; 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}])); 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}; 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)); }finally{await db.close();}