analyze.mjs 4.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. import { readFileSync,readdirSync,writeFileSync } from 'node:fs';
  2. const directory=process.argv[2]??new URL('../results',import.meta.url).pathname;
  3. const network=readFileSync(directory+'/network.jsonl','utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
  4. const samples=[];
  5. const median=x=>{x=[...x].sort((a,b)=>a-b);return x[Math.floor(x.length/2)]};
  6. function measure(trace,run,mode){
  7. const started=trace.find(v=>v.name==='send_start');if(!started)return;
  8. const entries=trace.filter(v=>v.time>=started.time);
  9. const finished=Math.max(...trace.map(v=>v.time))+.8;
  10. const n=network.filter(v=>v.run===run&&v.time>=started.time-.01&&v.time<=finished);
  11. const ready=entries.find(v=>v.name==='send_ready');
  12. const ack=entries.find(v=>v.name==='post_ack');
  13. const views=entries.filter(v=>v.name==='view_text'&&v.detail.startsWith('assistant|')&&Number(v.detail.split('|').at(-1))>0);
  14. const first=views[0];
  15. const dom=entries.filter(v=>v.name==='dom_text'&&Number(v.detail.split('|').at(-1))>0);
  16. const domGaps=dom.slice(1).map((v,i)=>(v.time-dom[i].time)*1000).filter(v=>v>0&&v<2000);
  17. const posts=n.filter(v=>v.event==='request'&&v.method==='POST'&&v.path.endsWith('/messages'));
  18. const ackNetwork=n.find(v=>v.event==='response_end'&&v.id===posts[0]?.id);
  19. const delay=entries.map(v=>v.time);const gaps=views.slice(1).map((v,i)=>(v.time-views[i].time)*1000).filter(v=>v>0&&v<2000);
  20. const round=v=>v===undefined?null:Math.round(v*100)/100;
  21. const complete=entries.find(v=>v.name==='completed_text'&&v.detail.includes(' END-'));
  22. if(complete){
  23. const marker=complete.detail.split(' END-').at(-1);
  24. const body=marker.startsWith('long-')?Array.from({length:200},(_,i)=>`第${String(i+1).padStart(3,'0')}段。用于比较两个移动端的连续文本显示表现,全部内容来自隔离测试服务。\n\n`).join(''):Array.from({length:40},(_,i)=>`${String(i+1).padStart(2,'0')}测试文字 `).join('');
  25. if(complete.detail!==body+' END-'+marker)throw new Error('Exact text mismatch: '+run);
  26. }
  27. samples.push({run,mode,startedAt:started.time,finishedAt:Math.max(...trace.map(v=>v.time)),sendReadyMs:round(ready&&(ready.time-started.time)*1000),postAckMs:round(ack&&(ack.time-started.time)*1000),
  28. afterAckMs:round(ready&&(ack??ackNetwork)&& (ready.time-(ack??ackNetwork).time)*1000),firstViewMs:round(first&&(first.time-started.time)*1000),
  29. viewCount:views.length,domUpdateGapMs:round(domGaps.length?median(domGaps):undefined),medianViewGapMs:round(gaps.length?median(gaps):undefined),maxViewGapMs:round(gaps.length?Math.max(...gaps):undefined),
  30. sseBytes:n.filter(v=>v.event==='sse_bytes').reduce((a,b)=>a+b.bytes,0),postCount:posts.length,
  31. exactTextVerified:!!complete,requests:n.filter(v=>v.event==='request').map(v=>v.method+' '+v.path)});
  32. }
  33. for(const file of readdirSync(directory).filter(f=>/^latency-(baseline|optimized)-ios-.*\.jsonl$/.test(f))){
  34. const trace=readFileSync(directory+'/'+file,'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
  35. const starts=trace.map((v,i)=>v.name==='send_start'?i:-1).filter(i=>i>=0);
  36. for(let i=0;i<starts.length;i++){
  37. const group=file.slice(8,-6), part=trace.slice(starts[i],starts[i+1]??trace.length);
  38. measure(part,group+'-'+(i+1),group.startsWith('baseline')?'baseline':'optimized');
  39. }
  40. }
  41. for(const file of readdirSync(directory).filter(f=>/^(baseline|optimized)-android\.jsonl$/.test(f))){
  42. const trace=readFileSync(directory+'/'+file,'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
  43. for(const run of new Set(trace.map(v=>v.run).filter(v=>!v.endsWith('setup')))) measure(trace.filter(v=>v.run===run),run,file.startsWith('baseline')?'baseline':'optimized');
  44. }
  45. try{for(const sample of JSON.parse(readFileSync(directory+'/web-traces.json','utf8')))measure(sample.trace,sample.run,sample.mode)}catch(e){if(e.code!=='ENOENT')throw e}
  46. const summary=[];
  47. for(const platform of ['ios','android','web'])for(const mode of ['baseline','optimized']){
  48. const values=samples.filter(v=>v.run.startsWith(mode+'-'+platform+'-short-')&&!v.run.endsWith('-1')&&v.sendReadyMs!==null);
  49. if(!values.length)continue;
  50. const row={platform,mode,samples:values.length};
  51. for(const key of ['sendReadyMs','postAckMs','afterAckMs','firstViewMs','medianViewGapMs','domUpdateGapMs']){
  52. const data=values.map(v=>v[key]).filter(v=>v!==null);row[key]=data.length?Math.round(median(data)*100)/100:null;
  53. }
  54. summary.push(row);
  55. }
  56. writeFileSync(directory+'/measurements.json',JSON.stringify({samples,summary},null,2));console.log(JSON.stringify(summary,null,2));