#!/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}`);