mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-18 23:09:29 -05:00
Measured first: one libmicrohttpd thread with 64 KiB a connection held uploads to
13 MB/s, and four connections to 15.3, on a link where FTP reached 25 / 30.5 MB/s.
PS5-Suite's design was reviewed (persistent connection pool, small files per
connection, large files as pwrite ranges, big socket buffers) and an engine built
from scratch on the WebUI's HTTP (src/webui_transfer.{h,cpp}, docs/WEBUI.md):
- a pool of four server threads, 512 KiB a connection, 4 MiB socket buffers, writes
in 1 MiB blocks; non-transfer routes under one lock as on the single thread;
- PUT /api/upload/batch: many small files in one request (a framed stream), each
renamed into place when whole, existing names skipped and named unless replace;
- upload sessions (/api/upload/session, /part, /commit): a large file's ranges in
any order on any number of connections, committed once covered;
- Range downloads (206); every upload still atomic, never over an existing name;
- the page plans batches, sessions and single uploads onto six lanes.
Evidence: evidence/webui-transfer-engine (base PS5 13.40, PC on Wi-Fi 6). Uploads
26.0 MB/s on one connection and 29.9 MB/s for one file in six parts (FTP: 25 /
30.5); 400 x 32 KiB at 215 files/s (FTP 105). The page against the console: 605
files in 20.1 s, all equal by FTP readback, duplicates refused, a cancelled 200 MiB
file left nothing. ThreadSanitizer under a mixed parallel load: no report.
Command: build/tmp/transfer/bench.py; node tests/webui_transfer_browser.cjs;
tools/verify.sh: all five PASS.
60 lines
4.4 KiB
JavaScript
60 lines
4.4 KiB
JavaScript
/* The page's transfer engine in Chromium against a native webui_server_main fixture
|
|
* (tests/test_webui_transfer_browser.py starts it): many small files go as batches,
|
|
* large ones as parallel parts, the rest one to a request; all arrive whole, names
|
|
* that exist are refused, and a cancelled large file leaves nothing behind. */
|
|
const assert = require('node:assert/strict');
|
|
const { chromium } = require(process.env.PLAYWRIGHT_PATH || 'playwright');
|
|
const base = process.env.WEBUI_TEST_URL;
|
|
(async () => {
|
|
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium' });
|
|
try {
|
|
const page = await browser.newPage();
|
|
const errors = []; page.on('pageerror', e => errors.push(e.message));
|
|
await page.route('https://api.github.com/**', route => route.fulfill({ json: [] }));
|
|
const requests = []; page.on('request', r => { if (r.url().includes('/api/upload')) requests.push(new URL(r.url()).pathname); });
|
|
await page.goto(base);
|
|
await page.waitForFunction(() => !document.querySelector('#browse-files').disabled);
|
|
// 600 small files, 3 medium, 2 large: made in the page, queued as a person drops them.
|
|
const summary = await page.evaluate(async () => {
|
|
const make = (name, size, seed) => { const b = new Uint8Array(size); for (let i = 0; i < size; i += 4096) b[i] = (seed + i) & 255; return new File([b], name); };
|
|
const files = [];
|
|
for (let i = 0; i < 600; i++) files.push(make(`shot-${String(i).padStart(4, '0')}.png`, 1000 + (i * 997) % 60000, i));
|
|
for (let i = 0; i < 3; i++) files.push(make(`medium-${i}.bin`, 9 * 1024 * 1024 + i, 7 + i));
|
|
for (let i = 0; i < 2; i++) files.push(make(`large-${i}.iso`, 150 * 1024 * 1024 + 4097 * i, 31 + i));
|
|
const started = performance.now();
|
|
queueFiles(files, 'engine');
|
|
await new Promise(resolve => { const t = setInterval(() => { if (!transfers.some(x => ['queued', 'uploading'].includes(x.state))) { clearInterval(t); resolve(); } }, 50); });
|
|
const states = {}; for (const x of transfers) states[x.state] = (states[x.state] || 0) + 1;
|
|
return { states, seconds: (performance.now() - started) / 1000, failed: transfers.filter(x => x.state === 'failed').map(x => x.name + ': ' + x.message).slice(0, 5) };
|
|
});
|
|
assert.deepEqual(summary.states, { complete: 605 }, JSON.stringify(summary));
|
|
const count = name => requests.filter(p => p === name).length;
|
|
assert.ok(count('/api/upload/batch') >= 3 && count('/api/upload/batch') <= 10, `batches: ${count('/api/upload/batch')}`);
|
|
assert.equal(count('/api/upload'), 3);
|
|
assert.equal(count('/api/upload/session'), 2);
|
|
assert.equal(count('/api/upload/commit'), 2);
|
|
assert.ok(count('/api/upload/part') >= 12, `parts: ${count('/api/upload/part')}`);
|
|
// The same names again: every one is refused, none replaced.
|
|
const again = await page.evaluate(async () => {
|
|
const files = [new File([new Uint8Array(10)], 'shot-0001.png'), new File([new Uint8Array(9 * 1024 * 1024)], 'medium-0.bin')];
|
|
const before = transfers.length; queueFiles(files, 'engine');
|
|
await new Promise(resolve => { const t = setInterval(() => { if (!transfers.some(x => ['queued', 'uploading'].includes(x.state))) { clearInterval(t); resolve(); } }, 50); });
|
|
return transfers.slice(before).map(x => x.state + ': ' + x.message);
|
|
});
|
|
assert.deepEqual(again.map(x => x.split(':')[0]), ['failed', 'failed']);
|
|
assert.match(again[0], /already exists/); assert.match(again[1], /already exists/);
|
|
// A large file cancelled mid-way: the session is dropped.
|
|
const cancelled = await page.evaluate(async () => {
|
|
const before = transfers.length; queueFiles([new File([new Uint8Array(200 * 1024 * 1024)], 'cancel.iso')], 'engine');
|
|
const transfer = transfers[before];
|
|
await new Promise(resolve => { const t = setInterval(() => { if ((transfer.sent || 0) > 20 * 1024 * 1024) { clearInterval(t); resolve(); } }, 10); });
|
|
cancelTransfer(transfer);
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
return transfer.state;
|
|
});
|
|
assert.equal(cancelled, 'cancelled');
|
|
assert.deepEqual(errors, []);
|
|
console.log(`PASS: 605 files (600 small in ${count('/api/upload/batch')} batches, 3 single, 2 large in ${count('/api/upload/part')} parts) in ${summary.seconds.toFixed(1)} s; duplicates refused; cancel drops the session.`);
|
|
} finally { await browser.close(); }
|
|
})().catch(error => { console.error(error); process.exit(1); });
|