browser-memory.mjs (2755B)
1 // Linux process-tree RSS sampling. Shared pages are double-counted; not PSS, 2 // private memory or a hard peak. No forced GC and no evaluator instrumentation. 3 import {readdir,readFile} from 'node:fs/promises'; 4 export function sampleBrowser(root,marks=[]){ 5 const samples=[];let active=true,task; 6 async function sample(){ 7 const rows=[]; 8 const fields=text=>Object.fromEntries(text.split('\n').filter(x=>x.includes(':')).map(x=>{const at=x.indexOf(':');return [x.slice(0,at),x.slice(at+1).trim()];})); 9 const own=fields(await readFile('/proc/self/status','utf8')); 10 for(const pid of (await readdir('/proc')).filter(x=>/^\d+$/.test(x))){ 11 try{const f=fields(await readFile('/proc/'+pid+'/status','utf8'));rows.push({pid:Number(f.Pid),parent:Number(f.PPid),local:Number(f.NSpid?.split(/\s+/).at(-1)??f.Pid),rss:Number(f.VmRSS?.split(/\s+/)[0]??0)*1024});}catch{} 12 } 13 // /proc may be mounted from an outer PID namespace. Locate our direct child 14 // using both the host parent PID and the child's namespace-local PID. 15 const hostRoot=rows.find(r=>r.parent===Number(own.Pid)&&r.local===root)?.pid; 16 const family=new Set(hostRoot===undefined?[]:[hostRoot]);let changed=true; 17 while(changed){changed=false;for(const r of rows)if(family.has(r.parent)&&!family.has(r.pid)){family.add(r.pid);changed=true;}} 18 const found=rows.filter(r=>family.has(r.pid));const current=marks.at(-1); 19 let pssBytes=null,privateBytes=null; 20 if(process.env.BENCHMARK_PSS&¤t&&(current.name.startsWith('live-')||['baseline','final-idle'].includes(current.name))){ 21 let pss=0,priv=0,complete=found.length>0; 22 for(const r of found){try{const f=fields(await readFile('/proc/'+r.pid+'/smaps_rollup','utf8'));pss+=Number(f.Pss.split(/\s+/)[0])*1024;priv+=(Number(f.Private_Clean.split(/\s+/)[0])+Number(f.Private_Dirty.split(/\s+/)[0]))*1024;}catch{complete=false;}} 23 if(complete){pssBytes=pss;privateBytes=priv;} 24 } 25 samples.push({at:performance.now(),phase:current?.name,rssBytes:found.length?found.reduce((s,r)=>s+r.rss,0):null,processes:found.length,pssBytes,privateBytes}); 26 } 27 task=(async()=>{while(active){await sample();await new Promise(r=>setTimeout(r,100));}})(); 28 return async marks=>{active=false;await task;if(!samples.some(x=>x.rssBytes>0))throw Error('Browser RSS unavailable');return {method:'Linux /proc descendant RSS sum, ~100ms sampling (shared pages double-counted). Optional PSS/private pages sampled only during explicit idle/live memory holds, not timing samples.',samples,phases:marks.map((m,i)=>{const values=samples.filter(x=>x.at>=m.at&&x.at<(marks[i+1]?.at??Infinity)).map(x=>x.rssBytes).filter(x=>x!==null);return {...m,maxRssBytes:values.length?Math.max(...values):null,minRssBytes:values.length?Math.min(...values):null};})};}; 29 }