v0.9.1 ece7682

web-latency.cjs RawHistoryFolder

1// Isolated gateway only; creates a retained ephemeral session per viewport.
2const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
3const { execFileSync } = require('node:child_process');
4(async () => {
5  if (!process.env.VEV_BINARY || !process.env.VEV_ENV) throw new Error('VEV_BINARY and isolated VEV_ENV are required');
6  const output = execFileSync(process.env.VEV_BINARY, ['--web-daemon'], { encoding: 'utf8' });
7  const token = output.match(/#token=([A-Za-z0-9_-]+)/)[1];
8  const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH });
9  try {
10    for (const viewport of [{width:800,height:500}, {width:1600,height:1000}, {width:2560,height:1440}, {width:3840,height:2160}]) {
11      const page = await browser.newPage({viewport});
12      if (process.env.WEB_APP_JS) await page.route('**/app.js', route => route.fulfill({path:process.env.WEB_APP_JS,contentType:'text/javascript'}));
13      if (process.env.WEB_RUNTIME_JS) await page.route('**/terminal.js', route => route.fulfill({path:process.env.WEB_RUNTIME_JS,contentType:'text/javascript'}));
14      await page.addInitScript(() => {
15        const NativeWebSocket = window.WebSocket;
16        window.wire = [];
17        window.WebSocket = class extends NativeWebSocket {
18          constructor(...args) {
19            super(...args);
20            this.addEventListener('message', event => {
21              const entry = {received:performance.now(), bytes:event.data.length};
22              window.wire.push(entry);
23              queueMicrotask(() => { entry.applied = performance.now(); });
24            });
25          }
26        };
27      });
28      await page.goto(`http://127.0.0.1:8778/#token=${token}`);
29      await page.waitForFunction(() => document.querySelector('#status').textContent === 'Connected');
30      await page.waitForTimeout(1000);
31      if (process.env.WEB_LAYER) await page.evaluate(() => {
32        for (const rule of ['#terminal .vev-terminal__cell { overflow: visible; }', '#terminal .vev-terminal__row { transform: translateZ(0); }']) document.styleSheets[0].insertRule(rule, document.styleSheets[0].cssRules.length);
33      });
34      const cdp = await page.context().newCDPSession(page);
35      await cdp.send('Performance.enable');
36      const trace = [];
37      if (process.env.WEB_PROFILE && viewport.width === 3840) {
38        cdp.on('Tracing.dataCollected', ({value}) => trace.push(...value));
39        await cdp.send('Tracing.start', {categories:'devtools.timeline', transferMode:'ReportEvents'});
40        await cdp.send('Profiler.enable');
41        await cdp.send('Profiler.start');
42      }
43      const metricsBefore = await cdp.send('Performance.getMetrics');
44      const samples = [];
45      let expected = '';
46      for (const key of 'abcdefghij') {
47        expected += key;
48        await page.evaluate(({key, expected}) => {
49          window.sample = new Promise(resolve => {
50            const output = document.querySelector('.vev-terminal__accessible-output');
51            const before = output.textContent;
52            const start = performance.now();
53            const observer = new MutationObserver(() => {
54              if (output.textContent !== before && output.textContent.includes(expected)) {
55                observer.disconnect();
56                const last = window.wire.at(-1);
57                resolve({total:performance.now()-start, receive:last.received-start, apply:performance.now()-last.received, bytes:last.bytes});
58              }
59            });
60            observer.observe(output, {childList:true,subtree:true,characterData:true});
61            document.querySelector('textarea').dispatchEvent(new InputEvent('beforeinput', {data:key,inputType:'insertText',bubbles:true,cancelable:true}));
62          });
63        }, {key, expected});
64        samples.push(await page.evaluate(() => Promise.race([window.sample, new Promise((_, reject) => setTimeout(() => reject(new Error('input echo timeout')), 5000))])));
65      }
66      if (process.env.WEB_PROFILE && viewport.width === 3840) {
67        const completed = new Promise(resolve => cdp.once('Tracing.tracingComplete', resolve));
68        await cdp.send('Tracing.end');
69        await completed;
70        const durations = {};
71        for (const event of trace) if (event.ph === 'X' && event.dur) durations[event.name] = (durations[event.name] || 0) + event.dur / 1000;
72        console.log('Trace ms', Object.entries(durations).sort((a,b)=>b[1]-a[1]).slice(0,20));
73        const {profile} = await cdp.send('Profiler.stop');
74        const counts = new Map();
75        for (const id of profile.samples || []) counts.set(id, (counts.get(id) || 0)+1);
76        console.log('CPU samples', profile.nodes.map(node => ({name:node.callFrame.functionName, url:node.callFrame.url, count:counts.get(node.id)||0})).sort((a,b)=>b.count-a.count).slice(0,15));
77      }
78      const metricsAfter = await cdp.send('Performance.getMetrics');
79      const costs = {};
80      for (const metric of metricsAfter.metrics) {
81        if (['LayoutDuration','RecalcStyleDuration','ScriptDuration','TaskDuration','LayoutCount','RecalcStyleCount'].includes(metric.name)) {
82          costs[metric.name] = metric.value - metricsBefore.metrics.find(before => before.name === metric.name).value;
83        }
84      }
85      console.log(JSON.stringify({viewport, samplesMs:samples, costs}));
86      await page.keyboard.press('Control+c');
87      const action = async (name, text) => {
88        await page.evaluate(({name, text}) => {
89          window.actionSample = new Promise(resolve => {
90            const output = document.querySelector('.vev-terminal__accessible-output');
91            const start = performance.now();
92            const observer = new MutationObserver(() => {
93              if (output.textContent.includes(text)) {
94                observer.disconnect();
95                const last = window.wire.at(-1);
96                resolve({total:performance.now()-start,receive:last.received-start,apply:performance.now()-last.received,bytes:last.bytes});
97              }
98            });
99            observer.observe(output, {childList:true,subtree:true});
100            if (name === 'palette') document.querySelector('#palette').click();
101            else document.querySelector('textarea').dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',code:'Enter',bubbles:true,cancelable:true}));
102          });
103        }, {name, text});
104        const ms = await page.evaluate(() => Promise.race([window.actionSample, new Promise((_, reject) => setTimeout(() => reject(new Error('action timeout')), 5000))]));
105        console.log(JSON.stringify({viewport, action:name, ms}));
106      };
107      await action('palette', 'Commands');
108      await page.keyboard.type('SPR');
109      await page.waitForTimeout(300);
110      await action('split', '│');
111      await page.close();
112    }
113  } finally { await browser.close(); }
114})().catch(error => { console.error(error); process.exitCode=1; });

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close