buninu-browser 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/audio/playback-buffer.js +27 -0
- package/lib/audio/pulse-client.js +18 -8
- package/lib/cdp-server.js +53 -14
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +20 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +33 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +12 -6
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +289 -131
- package/lib/media/byte-pipe.js +2 -2
- package/lib/media/position-audio.js +10 -1
- package/lib/media/source-snapshots.js +126 -0
- package/lib/media/video-keyframe.js +42 -0
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +22 -27
- package/lib/paint/frame-cache.js +25 -0
- package/lib/paint/frame.js +42 -13
- package/lib/paint/paragraph-text.js +24 -0
- package/lib/paint/raster-readback.js +41 -0
- package/lib/paint/rounded-rect.js +82 -0
- package/lib/paint/shared-frame-pool.js +105 -0
- package/lib/render-tree/index.js +40 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +244 -82
- package/lib/renderer/broadcast-channel.js +76 -0
- package/lib/renderer/canvas-2d.js +257 -14
- package/lib/renderer/canvas-text.js +120 -0
- package/lib/renderer/compositor.js +27 -10
- package/lib/renderer/custom-element-reactions.js +42 -0
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/history.js +42 -0
- package/lib/renderer/host.js +36 -5
- package/lib/renderer/image-loading.js +27 -0
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/keyboard-input.js +12 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +33 -5
- package/lib/renderer/media-playback-events.js +13 -0
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +278 -147
- package/lib/renderer/page-renderer.js +840 -301
- package/lib/renderer/piano-input.js +22 -0
- package/lib/renderer/pointer-move.js +29 -0
- package/lib/renderer/prepared-script-cache.js +50 -0
- package/lib/renderer/rendered-text.js +108 -0
- package/lib/renderer/rewrite-worker.js +4 -3
- package/lib/renderer/script-rewrite.js +13 -2
- package/lib/renderer/scroll-into-view.js +28 -0
- package/lib/renderer/slot-assignment.js +60 -0
- package/lib/renderer/video-presentation.js +145 -0
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +632 -108
- package/lib/style/selector-traits.js +49 -5
- package/package.json +1 -1
- package/test/broadcast-channel.test.js +26 -0
- package/test/canvas-2d.test.js +83 -2
- package/test/canvas-paint.test.js +9 -0
- package/test/canvas-text.test.js +41 -0
- package/test/cdp-stream.test.js +56 -0
- package/test/compositor.test.js +38 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +279 -1
- package/test/display-list.test.js +26 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/download.test.js +8 -0
- package/test/fetch-lifecycle.test.js +161 -0
- package/test/fixtures/canvas-paint.js +117 -0
- package/test/fixtures/canvas-text.js +42 -0
- package/test/fixtures/image-lifetime.js +90 -0
- package/test/fixtures/media-seek-snapshot.js +77 -0
- package/test/fixtures/mse-audio-chirp.webm +0 -0
- package/test/fixtures/mse-switch-large.mp4 +0 -0
- package/test/fixtures/mse-switch-small.mp4 +0 -0
- package/test/fixtures/renderer-memory.js +110 -0
- package/test/fixtures/summary-style.js +28 -0
- package/test/font-loading.test.js +97 -0
- package/test/grid.test.js +58 -0
- package/test/headless-shell.test.js +22 -0
- package/test/hit-test.test.js +21 -1
- package/test/image-lifetime.test.js +12 -0
- package/test/image-resources.test.js +94 -0
- package/test/local-media.test.js +84 -0
- package/test/media-playback-events.test.js +22 -0
- package/test/media-seek-snapshot.test.js +11 -0
- package/test/media-source-config.test.js +98 -0
- package/test/media-source-webm-accounting.test.js +98 -0
- package/test/media-source-webm.test.js +108 -0
- package/test/navigation.test.js +1 -0
- package/test/network-service.test.js +388 -0
- package/test/page-scripts.test.js +160 -2
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/piano-input.test.js +44 -0
- package/test/playback-buffer.test.js +31 -0
- package/test/position-audio.test.js +24 -0
- package/test/positioning.test.js +22 -0
- package/test/prepared-script-cache.test.js +45 -0
- package/test/pulse-client.test.js +27 -5
- package/test/raster-readback.test.js +40 -0
- package/test/render-tree.test.js +28 -0
- package/test/renderer-memory.test.js +11 -0
- package/test/selector-traits.test.js +24 -0
- package/test/shared-frame-pool.test.js +63 -0
- package/test/style-sheets.test.js +51 -0
- package/test/svg.test.js +29 -0
- package/test/text-layout.test.js +168 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +136 -0
- package/tools/browser-lab.js +78 -0
- package/tools/browser-soak.js +75 -0
- package/tools/network-reference.js +49 -0
- package/tools/process-memory.js +38 -0
- package/tools/script-benchmark.js +25 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Development-only CDP comparison runner. No package dependencies or page patches.
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export class CDP {
|
|
7
|
+
constructor(url) {
|
|
8
|
+
this.ws = new WebSocket(url); this.next = 1; this.pending = new Map(); this.events = [];
|
|
9
|
+
this.ready = new Promise((yes, no) => { this.ws.addEventListener('open', yes, {once:true}); this.ws.addEventListener('error', no, {once:true}); });
|
|
10
|
+
this.ws.addEventListener('message', ({data}) => {
|
|
11
|
+
const m = JSON.parse(data);
|
|
12
|
+
if (m.id) { const p = this.pending.get(m.id); if (!p) return; this.pending.delete(m.id); clearTimeout(p.timer); m.error ? p.no(Error(JSON.stringify(m.error))) : p.yes(m.result); }
|
|
13
|
+
else { this.events.push({time:performance.now(), ...m}); if(this.events.length>3000)this.events.shift(); }
|
|
14
|
+
});
|
|
15
|
+
this.ws.addEventListener('close', () => { for(const p of this.pending.values()){clearTimeout(p.timer);p.no(Error('CDP closed'));} this.pending.clear(); });
|
|
16
|
+
}
|
|
17
|
+
async send(method, params={}, timeout=30000) {
|
|
18
|
+
await this.ready;
|
|
19
|
+
return new Promise((yes,no) => { const id=this.next++; const timer=setTimeout(()=>{this.pending.delete(id);no(Error(`${method} timed out`));},timeout);this.pending.set(id,{yes,no,timer});this.ws.send(JSON.stringify({id,method,params,...(this.sessionId?{sessionId:this.sessionId}:{})})); });
|
|
20
|
+
}
|
|
21
|
+
async evaluate(expression, timeout=10000) { const r=await this.send('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true},timeout);if(r.exceptionDetails)throw Error(r.exceptionDetails.text+' '+(r.exceptionDetails.exception?.description||''));return r.result?.value; }
|
|
22
|
+
close(){this.ws.close();}
|
|
23
|
+
}
|
|
24
|
+
export async function launch(engine, dir, extra=[]) {
|
|
25
|
+
mkdirSync(dir,{recursive:true});
|
|
26
|
+
const log=join(dir,engine+'.log');
|
|
27
|
+
const cmd=engine==='chromium' ? ['/usr/local/bin/chromium-headless-shell','--no-sandbox','--disable-dev-shm-usage','--remote-debugging-port=0','--window-size=1280,800',...extra,'about:blank'] : [process.execPath,resolve('buninu-browser.js'),'--remote-debugging-port=0',...extra];
|
|
28
|
+
const proc=Bun.spawn(cmd,{stdout:Bun.file(join(dir,engine+'.stdout')),stderr:'pipe',env:{...process.env,BUNINU_LOG:join(dir,'buninu-diagnostic.log')}});
|
|
29
|
+
let stderr='';const writer=Bun.file(log).writer();
|
|
30
|
+
(async()=>{for await(const chunk of proc.stderr){writer.write(chunk);stderr+=Buffer.from(chunk).toString();if(stderr.length>200000)stderr=stderr.slice(-100000);}await writer.end();})();
|
|
31
|
+
let cdp;
|
|
32
|
+
try {
|
|
33
|
+
let port;for(let i=0;i<200;i++){const m=stderr.match(/DevTools listening on ws:\/\/[^:]+:(\d+)/);if(m){port=Number(m[1]);break;}if(proc.exitCode!==null)throw Error(`engine exited ${proc.exitCode}: ${stderr.slice(-1000)}`);await Bun.sleep(50);}
|
|
34
|
+
if(!port){proc.kill();throw Error('CDP endpoint missing');}
|
|
35
|
+
const version=await (await fetch(`http://127.0.0.1:${port}/json/version`)).json();
|
|
36
|
+
cdp=new CDP(version.webSocketDebuggerUrl);await cdp.ready;
|
|
37
|
+
const {targetId}=await cdp.send('Target.createTarget',{url:'about:blank'});
|
|
38
|
+
const {sessionId}=await cdp.send('Target.attachToTarget',{targetId,flatten:true});
|
|
39
|
+
cdp.sessionId=sessionId;
|
|
40
|
+
await cdp.send('Page.enable');await cdp.send('Runtime.enable');
|
|
41
|
+
await cdp.send('Emulation.setDeviceMetricsOverride',{width:1280,height:800,deviceScaleFactor:1,mobile:false});
|
|
42
|
+
return {cdp,proc,stop:async()=>{cdp.close();proc.kill();await proc.exited;}};
|
|
43
|
+
} catch(error) { cdp?.close();proc.kill();await proc.exited;throw error; }
|
|
44
|
+
}
|
|
45
|
+
const describe = `(()=>({url:location.href,title:document.title,text:document.body?.innerText?.slice(0,12000),nodes:document.querySelectorAll('*').length,readyState:document.readyState,controls:[...document.querySelectorAll('a,button,input,select,summary,[role="button"]')].map(e=>{const r=e.getBoundingClientRect();return {tag:e.tagName,text:(e.innerText||e.getAttribute('aria-label')||e.getAttribute('placeholder')||'').trim().slice(0,100),href:e.getAttribute('href'),id:e.id,x:r.x,y:r.y,width:r.width,height:r.height};}).filter(e=>e.width>0&&e.height>0&&e.x<1280&&e.y<800&&e.x+e.width>0&&e.y+e.height>0).slice(0,100)}))()`;
|
|
46
|
+
export async function snapshot(cdp,dir,name) {
|
|
47
|
+
const start=performance.now(); const {data}=await cdp.send('Page.captureScreenshot',{format:'png'});const captureMs=performance.now()-start;
|
|
48
|
+
await Bun.write(join(dir,name+'.png'),Buffer.from(data,'base64'));
|
|
49
|
+
let dom;try{dom=await cdp.evaluate(describe);}catch(error){dom={error:error.message};}
|
|
50
|
+
await Bun.write(join(dir,name+'.json'),JSON.stringify({...dom,captureMs},null,2));return {captureMs,...dom};
|
|
51
|
+
}
|
|
52
|
+
export async function runCase({engine,url,dir,actions=[],settle=1500}) {
|
|
53
|
+
const started=performance.now();let browser;const report={engine,url,actions:[]};
|
|
54
|
+
try {
|
|
55
|
+
browser=await launch(engine,dir);const {cdp}=browser;report.startupMs=performance.now()-started;
|
|
56
|
+
const nav=performance.now();await cdp.send('Page.navigate',{url},45000);report.navigateReplyMs=performance.now()-nav;
|
|
57
|
+
const deadline=performance.now()+30000;
|
|
58
|
+
while(performance.now()<deadline&&!cdp.events.some(e=>e.method==='Page.loadEventFired'&&e.time>=nav))await Bun.sleep(100);
|
|
59
|
+
const loaded=cdp.events.find(e=>e.method==='Page.loadEventFired'&&e.time>=nav);report.loadMs=loaded?loaded.time-nav:null;
|
|
60
|
+
await Bun.sleep(settle);report.initial=await snapshot(cdp,dir,'initial');
|
|
61
|
+
for(let i=0;i<actions.length;i++){
|
|
62
|
+
const action=actions[i],at=performance.now();let error=null;
|
|
63
|
+
try {
|
|
64
|
+
if(action.click||action.clickText){const point=await cdp.evaluate(`(()=>{const candidates=[...document.querySelectorAll(${JSON.stringify(action.click||'button,a,summary,[role="button"],[role="menuitem"]')})];const e=candidates.find(e=>{const r=e.getBoundingClientRect(),s=getComputedStyle(e);return r.width>0&&r.height>0&&s.visibility!=='hidden'&&(!${JSON.stringify(action.clickText||'')}||(e.innerText||e.getAttribute('aria-label')||'').trim()===${JSON.stringify(action.clickText||'')});});if(!e)return null;e.scrollIntoView({block:'nearest',inline:'nearest'});const r=e.getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2};})()`);if(!point)throw Error('visible selector not found');for(const type of ['mousePressed','mouseReleased'])await cdp.send('Input.dispatchMouseEvent',{type,...point,button:'left',clickCount:1});}
|
|
65
|
+
else if(action.point){for(const type of ['mousePressed','mouseReleased'])await cdp.send('Input.dispatchMouseEvent',{type,x:action.point[0],y:action.point[1],button:'left',clickCount:1});}
|
|
66
|
+
else if(action.type)await cdp.send('Input.insertText',{text:action.type});
|
|
67
|
+
else if(action.key){for(const type of ['keyDown','keyUp'])await cdp.send('Input.dispatchKeyEvent',{type,key:action.key,code:action.key,windowsVirtualKeyCode:action.key==='Enter'?13:action.key==='Escape'?27:0});}
|
|
68
|
+
else if(action.scroll)await cdp.send('Input.dispatchMouseEvent',{type:'mouseWheel',x:640,y:500,deltaY:action.scroll,deltaX:0});
|
|
69
|
+
else if(action.evaluate)report.lastEvaluation=await cdp.evaluate(action.evaluate);
|
|
70
|
+
}catch(e){error=e.message;}
|
|
71
|
+
await Bun.sleep(action.wait??600);
|
|
72
|
+
const state=await snapshot(cdp,dir,`action-${i+1}`);report.actions.push({action,error,elapsedMs:performance.now()-at,state});
|
|
73
|
+
}
|
|
74
|
+
report.errors=cdp.events.filter(e=>e.method==='Runtime.exceptionThrown').slice(-20);
|
|
75
|
+
}catch(error){report.error=error.stack;}finally{await browser?.stop();report.totalMs=performance.now()-started;await Bun.write(join(dir,'report.json'),JSON.stringify(report,null,2));}
|
|
76
|
+
return report;
|
|
77
|
+
}
|
|
78
|
+
if(import.meta.main){const [engine,url,dir,...rest]=process.argv.slice(2);if(!engine||!url||!dir)throw Error('usage: bun tools/browser-lab.js ENGINE URL OUTPUT_DIR [ACTIONS_JSON]');const result=await runCase({engine,url,dir:resolve(dir),actions:rest[0]?JSON.parse(rest[0]):[]});console.log(JSON.stringify({engine,url,error:result.error,startupMs:result.startupMs,loadMs:result.loadMs,captureMs:result.initial?.captureMs,title:result.initial?.title,actions:result.actions.map(a=>({action:a.action,error:a.error,url:a.state?.url}))}));}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// End-to-end bounded DOM/canvas/image churn, navigation and CDP capture soak.
|
|
3
|
+
// Usage: bun tools/browser-soak.js buninu /tmp/browser-soak 900
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { launch } from './browser-lab.js';
|
|
6
|
+
import { encodeImage } from '../lib/paint/frame.js';
|
|
7
|
+
import { processMemory } from './process-memory.js';
|
|
8
|
+
|
|
9
|
+
const [engine = 'buninu', directory = '/tmp/browser-soak', seconds = '600'] = process.argv.slice(2);
|
|
10
|
+
const duration = Math.max(1, Number(seconds)) * 1000;
|
|
11
|
+
const dir = resolve(directory);
|
|
12
|
+
const rgba = new Uint8Array(32 * 32 * 4);
|
|
13
|
+
for (let i = 0; i < rgba.length; i += 4) rgba.set([30, 120, 210, 255], i);
|
|
14
|
+
const png = await encodeImage(rgba, 32, 32);
|
|
15
|
+
let requests = 0;
|
|
16
|
+
const server = Bun.serve({ port: 0, fetch(request) {
|
|
17
|
+
requests++;
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
if (url.pathname === '/pixel.png') return new Response(png, { headers: { 'content-type': 'image/png', 'cache-control': 'max-age=3600' } });
|
|
20
|
+
return new Response(`<!doctype html><meta charset="utf-8"><title>Browser soak ${url.search}</title>
|
|
21
|
+
<style>body{margin:0;font:16px sans-serif;background:#fafafa}header{position:sticky;top:0;background:#135;color:white;padding:12px;z-index:2}main{padding:12px}.card{display:flex;gap:12px;align-items:center;padding:8px;border-bottom:1px solid #ddd}canvas{display:block}button{font:inherit}</style>
|
|
22
|
+
<header>Navigation ${url.search} · <span id="counter">0</span> <button id="toggle">Toggle</button></header>
|
|
23
|
+
<main><canvas id="paint" width="800" height="120"></canvas><section id="cards">${Array.from({length:160}, (_,i)=>`<div class="card"><img src="/pixel.png" width="32" height="32"><span>Row ${i}: Browser rendering, memory and input exercise 中文 🙂</span></div>`).join('')}</section></main>
|
|
24
|
+
<script>
|
|
25
|
+
let tick=0; const paint=document.getElementById('paint'); const context=paint.getContext('2d');
|
|
26
|
+
const counter=document.getElementById('counter'); const cards=document.getElementById('cards');
|
|
27
|
+
document.getElementById('toggle').onclick=()=>document.body.style.background=tick%2?'#def':'#fed';
|
|
28
|
+
setInterval(()=>{tick++;counter.textContent=String(tick);context.fillStyle=tick%2?'#69c':'#c96';context.fillRect((tick*11)%760,20,40,80);
|
|
29
|
+
if(tick%10===0){const card=cards.firstElementChild;card.querySelector('span').textContent='Recycled row '+tick;cards.appendChild(card);}
|
|
30
|
+
if(tick%20===0){for(let i=0;i<8;i++){const c=document.createElement('canvas');c.width=800;c.height=600;c.getContext('2d').fillRect(0,0,800,600);}}
|
|
31
|
+
},100);
|
|
32
|
+
</script>`, { headers: { 'content-type': 'text/html' } });
|
|
33
|
+
} });
|
|
34
|
+
|
|
35
|
+
let browser;
|
|
36
|
+
const samples = [];
|
|
37
|
+
const errors = [];
|
|
38
|
+
const started = performance.now();
|
|
39
|
+
let navigations = 0;
|
|
40
|
+
try {
|
|
41
|
+
browser = await launch(engine, dir);
|
|
42
|
+
const writer = Bun.file(join(dir, 'samples.jsonl')).writer();
|
|
43
|
+
const { cdp } = browser;
|
|
44
|
+
let nextNavigation = 0;
|
|
45
|
+
while (performance.now() - started < duration) {
|
|
46
|
+
const elapsed = performance.now() - started;
|
|
47
|
+
if (elapsed >= nextNavigation) {
|
|
48
|
+
const host = Math.floor(navigations / 4) % 2 ? 'localhost' : '127.0.0.1';
|
|
49
|
+
await cdp.send('Page.navigate', { url: `http://${host}:${server.port}/page?n=${navigations++}` });
|
|
50
|
+
nextNavigation = elapsed + 15000;
|
|
51
|
+
await Bun.sleep(300);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: 600, y: 500, deltaY: samples.length % 20 < 10 ? 180 : -180, deltaX: 0 });
|
|
55
|
+
const before = performance.now();
|
|
56
|
+
const { data } = await cdp.send('Page.captureScreenshot', { format: 'png', optimizeForSpeed: true });
|
|
57
|
+
const captureMs = performance.now() - before;
|
|
58
|
+
const counter = await cdp.evaluate(`document.getElementById('counter')?.textContent`);
|
|
59
|
+
const processes = await processMemory(browser.proc.pid);
|
|
60
|
+
const sample = { elapsedMs: Math.round(elapsed), navigation: navigations, counter, captureMs, encodedLength: data.length, rssMiB: processes ? processes.reduce((sum,p)=>sum+p.rssKiB,0)/1024 : null, processes };
|
|
61
|
+
samples.push(sample);writer.write(JSON.stringify(sample)+'\n');writer.flush();
|
|
62
|
+
if (samples.length % 30 === 0) await Bun.write(join(dir, 'latest.png'), Buffer.from(data, 'base64'));
|
|
63
|
+
if (samples.length % 60 === 0) console.log(JSON.stringify(sample));
|
|
64
|
+
} catch (error) { errors.push({ elapsedMs: Math.round(elapsed), error: error.message }); }
|
|
65
|
+
await Bun.sleep(1000);
|
|
66
|
+
}
|
|
67
|
+
await writer.end();
|
|
68
|
+
} finally {
|
|
69
|
+
await browser?.stop();server.stop(true);
|
|
70
|
+
const mean = values => values.length ? values.reduce((a,b)=>a+b,0)/values.length : null;
|
|
71
|
+
const report = { engine, elapsedMs: performance.now()-started, navigations, requests, samples: samples.length, errors,
|
|
72
|
+
peakRssMiB: Math.max(0,...samples.map(s=>s.rssMiB)), firstMinuteRssMiB: mean(samples.filter(s=>s.elapsedMs>=60000&&s.elapsedMs<120000).map(s=>s.rssMiB)),
|
|
73
|
+
lastMinuteRssMiB: mean(samples.filter(s=>s.elapsedMs>duration-60000).map(s=>s.rssMiB)), captureMeanMs: mean(samples.map(s=>s.captureMs)) };
|
|
74
|
+
await Bun.write(join(dir, 'report.json'), JSON.stringify(report,null,2));console.log(JSON.stringify(report));
|
|
75
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Exercise redirects through real HTTP servers, using Chromium as a reference.
|
|
3
|
+
import { launch } from "./browser-lab.js";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
const [engine = "buninu", output = "/tmp/buninu-network-reference"] = process.argv.slice(2);
|
|
7
|
+
const calls = [];
|
|
8
|
+
let source, target;
|
|
9
|
+
const handler = async request => {
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
calls.push({ method: request.method, path: url.pathname, host: url.host, origin: request.headers.get("origin"),
|
|
12
|
+
cookie: request.headers.get("cookie"), authorization: request.headers.get("authorization"),
|
|
13
|
+
preflight: request.headers.get("access-control-request-headers") });
|
|
14
|
+
const cors = { "access-control-allow-origin": request.headers.get("origin") ?? source,
|
|
15
|
+
"access-control-allow-credentials": "true", "access-control-allow-methods": "PUT", "access-control-allow-headers": "x-custom" };
|
|
16
|
+
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: cors });
|
|
17
|
+
if (url.pathname === "/") return new Response("<!doctype html><title>Network reference</title><p>Redirect fixture</p>", { headers: { "content-type": "text/html" } });
|
|
18
|
+
if (url.pathname.startsWith("/redirect/")) return new Response(null, { status: 307, headers: { location: target + url.pathname.slice(9) } });
|
|
19
|
+
if (url.pathname === "/chain") return new Response(null, { status: 302, headers: { ...cors, location: source + "/finish" } });
|
|
20
|
+
return new Response("visible body", { headers: { ...(url.pathname === "/denied" ? {} : cors),
|
|
21
|
+
"x-private": "hidden", "x-public": "visible", "access-control-expose-headers": "x-public" } });
|
|
22
|
+
};
|
|
23
|
+
const first = Bun.serve({ port: 0, fetch: handler });
|
|
24
|
+
const second = Bun.serve({ port: 0, fetch: handler });
|
|
25
|
+
source = `http://localhost:${first.port}`; target = `http://localhost:${second.port}`;
|
|
26
|
+
let browser;
|
|
27
|
+
const report = { engine, cases: [] };
|
|
28
|
+
try {
|
|
29
|
+
browser = await launch(engine, resolve(output));
|
|
30
|
+
await browser.cdp.send("Page.navigate", { url: source + "/" });
|
|
31
|
+
await Bun.sleep(300);
|
|
32
|
+
await browser.cdp.evaluate('document.cookie="private=fixture; SameSite=Lax"; undefined;');
|
|
33
|
+
for (const [name, url, init] of [
|
|
34
|
+
["denied", source + "/redirect/denied", {}],
|
|
35
|
+
["allowed", source + "/redirect/allowed", {}],
|
|
36
|
+
["same-origin", source + "/redirect/allowed", { mode: "same-origin" }],
|
|
37
|
+
["opaque", source + "/redirect/allowed", { mode: "no-cors" }],
|
|
38
|
+
["preflight", source + "/redirect/allowed", { method: "PUT", headers: { authorization: "Bearer fixture", "x-custom": "yes" }, body: "body" }],
|
|
39
|
+
["chain", target + "/chain", {}],
|
|
40
|
+
]) {
|
|
41
|
+
calls.length = 0;
|
|
42
|
+
const result = await browser.cdp.evaluate(`(async()=>{try {const r=await fetch(${JSON.stringify(url)},${JSON.stringify(init)});
|
|
43
|
+
return {type:r.type,status:r.status,body:await r.text(),private:r.headers.get('x-private'),public:r.headers.get('x-public')};
|
|
44
|
+
} catch(e) {return {error:e.name};}})()`);
|
|
45
|
+
report.cases.push({ name, result, calls: calls.slice() });
|
|
46
|
+
}
|
|
47
|
+
await Bun.write(resolve(output, "network.json"), JSON.stringify(report, null, 2));
|
|
48
|
+
console.log(JSON.stringify(report));
|
|
49
|
+
} finally { await browser?.stop(); first.stop(true); second.stop(true); }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
// Android kernels may omit /proc/PID/task/PID/children. PPid in status works
|
|
4
|
+
// there too; return null if this platform does not expose process memory.
|
|
5
|
+
export async function processMemory(rootPid) {
|
|
6
|
+
let entries;
|
|
7
|
+
try { entries = (await readdir('/proc')).filter(name => /^\d+$/.test(name)); }
|
|
8
|
+
catch { return null; }
|
|
9
|
+
const records = (await Promise.all(entries.map(async name => {
|
|
10
|
+
try {
|
|
11
|
+
const status = await Bun.file(`/proc/${name}/status`).text();
|
|
12
|
+
return { pid: Number(name), parent: Number(status.match(/^PPid:\s+(\d+)/m)?.[1]), rssKiB: Number(status.match(/^VmRSS:\s+(\d+)/m)?.[1] ?? 0) };
|
|
13
|
+
} catch { return null; }
|
|
14
|
+
}))).filter(Boolean);
|
|
15
|
+
const children = new Map();
|
|
16
|
+
for (const record of records) {
|
|
17
|
+
if (!children.has(record.parent)) children.set(record.parent, []);
|
|
18
|
+
children.get(record.parent).push(record);
|
|
19
|
+
}
|
|
20
|
+
const root = records.find(record => record.pid === rootPid);
|
|
21
|
+
if (!root) return null;
|
|
22
|
+
const found = [root];
|
|
23
|
+
for (let index = 0; index < found.length; index++) found.push(...(children.get(found[index].pid) ?? []));
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (import.meta.main) {
|
|
28
|
+
const [pid, output, seconds = '600'] = process.argv.slice(2);
|
|
29
|
+
const writer = Bun.file(output).writer();
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
while (Date.now() - start < Number(seconds) * 1000) {
|
|
32
|
+
const processes = await processMemory(Number(pid));
|
|
33
|
+
if (!processes) break;
|
|
34
|
+
writer.write(JSON.stringify({elapsedMs:Date.now()-start,rssMiB:processes.reduce((sum,p)=>sum+p.rssKiB,0)/1024,processes})+'\n');
|
|
35
|
+
writer.flush();await Bun.sleep(1000);
|
|
36
|
+
}
|
|
37
|
+
await writer.end();
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Compare deterministic script preparation, independent of network/site timing.
|
|
3
|
+
import { resolve, basename } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
const [modulePath = "lib/renderer/script-rewrite.js", ...files] = process.argv.slice(2);
|
|
7
|
+
const { rewriteClassicScriptSource } = await import(pathToFileURL(resolve(modulePath)).href);
|
|
8
|
+
const cases = [
|
|
9
|
+
["top-level-functions", Array.from({ length: 3000 }, (_, i) =>
|
|
10
|
+
`function function${i}(x) { return callback(x) + this.offset + value${i}; }`).join("\n")],
|
|
11
|
+
["bundled-functions", `(function(){${Array.from({ length: 3000 }, (_, i) =>
|
|
12
|
+
`function function${i}(x) { return callback(x) + this.offset + value${i}; }`).join("\n")}result = function2999(1);})();`],
|
|
13
|
+
];
|
|
14
|
+
for (const file of files) cases.push([basename(file), await Bun.file(file).text()]);
|
|
15
|
+
for (const [name, source] of cases) {
|
|
16
|
+
const samples = []; let result;
|
|
17
|
+
for (let i = 0; i < 7; i++) {
|
|
18
|
+
Bun.gc(true);
|
|
19
|
+
const start = performance.now(); result = rewriteClassicScriptSource(source);
|
|
20
|
+
samples.push(performance.now() - start);
|
|
21
|
+
}
|
|
22
|
+
const sorted = samples.slice(2).sort((a, b) => a - b);
|
|
23
|
+
console.log(JSON.stringify({ name, inputLength: source.length, outputLength: result.code.length,
|
|
24
|
+
outputHash: Bun.hash(result.code).toString(16), parsed: result.parsed, medianMs: sorted[2], samples }));
|
|
25
|
+
}
|