| 1234567891011121314151617181920212223242526272829303132 |
- #!/usr/bin/env node
- // Isolated local SQL benchmark; no production writes or network calls.
- import {PGlite} from '@electric-sql/pglite';import {readFileSync,writeFileSync} from 'node:fs';import vm from 'node:vm';import assert from 'node:assert/strict';
- const baseline=process.argv.find(a=>a.startsWith('--baseline='))?.slice(11);if(!baseline)throw Error('--baseline required');
- const report={scope:'isolated PGlite SQL and serialization only, not browser or production latency',alternating:true,results:[]};
- for(const size of [20_000,60_000]){
- const db=new PGlite();try{
- await db.exec(readFileSync(new URL('./tests/fixtures/lesson-records-schema.sql',import.meta.url),'utf8'));
- await db.exec(readFileSync(new URL('./sql/review-rewards.sql',import.meta.url),'utf8'));await db.exec(readFileSync(new URL('./sql/lesson-records.sql',import.meta.url),'utf8'));
- await db.exec(`INSERT INTO "_User"("objectId",company,"legacyUserId","realName") SELECT 'u'||i,'c',i,'姓名'||i FROM generate_series(1,202) i;
- INSERT INTO "LessonRecord"("objectId",company,id,xymz,jsmz,kcid,yyds,"lessonAt","updatedAt") SELECT 'l'||i,'c',i,(3+i%200)::text,'1','30',i::text,'2026-11-01'::timestamptz+(i||' seconds')::interval,now() FROM generate_series(1,${size}) i;
- CREATE INDEX ON "LessonRecord"(company,xymz);CREATE INDEX ON "CommonModel"(company,"modelId","itemId");CREATE INDEX ON "LessonCompletionReport"(company,"lessonId");CREATE INDEX ON "LessonCompletionReport"(company,"appointmentGeneralId");ANALYZE;`);
- const adapt=tx=>({query:async(s,p=[])=>(await tx.query(s,p)).rows,one:async(s,p=[])=>(await tx.query(s,p)).rows[0],none:async(s,p=[])=>{await tx.query(s,p);}});
- const make=source=>{const ctx=vm.createContext({Psql:{...adapt(db),tx:work=>db.transaction(t=>work(adapt(t)))},text:v=>String(v??''),number:(v,d=0)=>Number(v)||d,dateIso:v=>v?new Date(v).toISOString():'',classType:v=>v,TEACHING_CATEGORY_LABELS:new Map(),companyIdOf:c=>c.company,roleOf:c=>c.role,requireRole:()=>{},fail:(_,m)=>{throw Error(m);},pageInput:i=>({page:i.page||1,pageSize:i.pageSize||20})});vm.runInContext(source,ctx);return ctx;};
- const before=make(readFileSync(baseline,'utf8')),after=make(readFileSync(new URL('./cloud/lesson-records.js',import.meta.url),'utf8'));
- await db.exec(readFileSync(new URL('./sql/read-revisions.sql',import.meta.url),'utf8'));
- for(const [scenario,input] of [['student',{studentId:20}],['global-page-2',{page:2}]]){
- const samples={before:[],after:[]};
- for(let round=0;round<12;round++){
- const results={};for(const variant of round%2?['after','before']:['before','after']){
- const start=performance.now(),result=await({before,after}[variant]).lessonHistoryPage({company:'c',current:{id:'manager'},role:'ops-manager'},input);const ms=performance.now()-start;
- results[variant]=JSON.parse(JSON.stringify(result));delete results[variant].nextCursor;delete results[variant].version;delete results[variant].refreshedAt;if(round>=2)samples[variant].push(ms);
- }assert.deepEqual(results.before,results.after);
- }
- const quantile=(a,p)=>[...a].sort((a,b)=>a-b)[Math.ceil(a.length*p)-1];
- const summary=Object.fromEntries(Object.entries(samples).map(([key,values])=>[key,{p50:quantile(values,.5),p75:quantile(values,.75),p95:quantile(values,.95)}]));
- report.results.push({size,scenario,samples,summary,p75Reduction:1-summary.after.p75/summary.before.p75});console.log(JSON.stringify({size,scenario,summary}));
- }
- }finally{await db.close();}
- }
- writeFileSync('perf/cloud-concurrency-2026-11-15/local-sql-benchmark.json',JSON.stringify(report,null,2));
- for(const result of report.results)assert.ok(result.summary.after.p75<=result.summary.before.p75*1.1,`p75 regression: ${result.size}/${result.scenario}`);
|