diff --git a/.github/workflows/webkit-stall-experiment.yml b/.github/workflows/webkit-stall-experiment.yml new file mode 100644 index 0000000000..7c0939efc2 --- /dev/null +++ b/.github/workflows/webkit-stall-experiment.yml @@ -0,0 +1,67 @@ +# TEMPORARY: experiment for a draft PR, never to be merged. +# Does a WebKit page on Linux stall after rendering a million rows, or under the suite's load, +# and do the stalls line up with drops in the memory of WebKit's web processes? +name: WebKit stall experiment + +on: pull_request + +permissions: {} + +jobs: + experiment: + runs-on: ubuntu-latest + timeout-minutes: 30 + + permissions: + contents: read # check out the repository + + strategy: + fail-fast: false + matrix: + run: [1, 2, 3] + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: voidzero-dev/setup-vp@68f39c78aaf39be468cabd2afb09c162574c84af # v1.21.2 + with: + cache: true + cache-save: false + run-install: false + + - run: vp install --frozen-lockfile + + - run: vp run install-browsers + timeout-minutes: 8 + + - name: Idle after rendering a million rows, then 100k rows + env: + WEBKIT_STALL_SAMPLER: 1 + WEBKIT_STALL_IDLE: 1 + DEBUG: pw:browser* + run: | + (while sleep 0.2; do echo "$(date +%s%3N) $(ps -eo pid=,rss=,comm= | grep -E 'WebProces' | tr -s ' ' | tr '\n' ';')"; done) > memory-idle.log & + poller=$! + vp test test/browser/zzStall.experiment.tsx --project "browser (webkit)" --sequence.shuffle.tests=false > idle.log 2>&1 || true + kill "$poller" + grep -E 'STALLS|Tests |Duration' idle.log | cut -c1-2000 + + - name: WebKit suite with the stall sampler + env: + WEBKIT_STALL_SAMPLER: 1 + DEBUG: pw:browser* + run: | + (while sleep 0.2; do echo "$(date +%s%3N) $(ps -eo pid=,rss=,comm= | grep -E 'WebProces' | tr -s ' ' | tr '\n' ';')"; done) > memory-suite.log & + poller=$! + vp test --project "browser (webkit)" > suite.log 2>&1 || true + kill "$poller" + grep -E 'FAIL|Tests |Duration' suite.log | head -50 + + - name: Upload logs + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: webkit-stall-${{ matrix.run }} + path: '*.log' diff --git a/test/browser/zzStall.experiment.tsx b/test/browser/zzStall.experiment.tsx new file mode 100644 index 0000000000..60fac16a24 --- /dev/null +++ b/test/browser/zzStall.experiment.tsx @@ -0,0 +1,51 @@ +// TEMPORARY: WebKit stall experiment, does the page stall while idle after rendering a million rows? +import { page } from 'vite-plus/test/browser'; + +import { TreeDataGrid } from '../../src'; + +interface Row { + id: number; + country: string; + year: number; +} + +const columns = [ + { key: 'country', name: 'Country' }, + { key: 'id', name: 'Id' } +]; + +async function renderRows(count: number) { + const rows = Array.from({ length: count }, (_, id): Row => ({ id, country: 'USA', year: 2020 })); + + await page.render( + {}} + /> + ); + + await expect.element(page.getTreeGrid()).toHaveAttribute('aria-rowcount', String(count + 2)); + await expect.element(page.getCell({ name: '0', exact: true })).toBeVisible(); +} + +// vitest-browser-react unmounts the grid before the idle test starts +async function idle(seconds: number) { + for (let i = 0; i < seconds; i++) { + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + } +} + +describe.each([1_000_000, 100_000])('%i rows', (count) => { + it('render', async () => { + await renderRows(count); + }); + + it('idle for 65s', { timeout: 90_000 }, async () => { + await idle(65); + }); +}); diff --git a/test/zzStallSampler.ts b/test/zzStallSampler.ts new file mode 100644 index 0000000000..36597868fd --- /dev/null +++ b/test/zzStallSampler.ts @@ -0,0 +1,42 @@ +// TEMPORARY: WebKit stall experiment, records the page's main-thread stalls and test timings +// with wall-clock times, so stalls can be compared across pages and with memory samples +import { server } from 'vite-plus/test/browser'; + +const INTERVAL = 5; +const THRESHOLD = 100; + +let file = ''; +let currentTest = '(before the first test)'; +let testStart = 0; +let tests: { name: string; start: number; end: number }[] = []; +let stalls: { start: number; end: number; test: string }[] = []; +let last = performance.now(); + +setInterval(() => { + const now = performance.now(); + if (now - last - INTERVAL > THRESHOLD) { + const end = Date.now(); + stalls.push({ start: Math.round(end - (now - last)), end, test: currentTest }); + } + last = now; +}, INTERVAL); + +// registered before the other setup files' hooks, so it runs first +beforeEach(({ task }) => { + file = task.file.name; + currentTest = task.name; + testStart = Date.now(); +}); + +afterEach(({ task }) => { + tests.push({ name: task.name, start: testStart, end: Date.now() }); + currentTest = `(after ${task.name})`; +}); + +// one log per file, so the experiment doesn't flood the console +afterAll(() => { + // oxlint-disable-next-line eslint/no-console + console.info(`STALLS ${JSON.stringify({ browser: server.browser, file, tests, stalls })}`); + tests = []; + stalls = []; +}); diff --git a/vite.config.ts b/vite.config.ts index 4fe6cec15b..c95dd0a3b7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -311,9 +311,20 @@ export default defineConfig({ { test: { name: 'browser', - include: ['browser/**/*.test.*', ...(isCI ? ['visual/*.test.*'] : [])], + include: [ + 'browser/**/*.test.*', + ...(isCI ? ['visual/*.test.*'] : []), + // TEMPORARY: WebKit stall experiment + ...(process.env.WEBKIT_STALL_IDLE === '1' ? ['browser/zzStall.experiment.tsx'] : []) + ], browser: { enabled: true }, - setupFiles: ['test/browser/styles.css', 'test/setupBrowser.ts', 'test/failOnConsole.ts'] + setupFiles: [ + // TEMPORARY: WebKit stall experiment + ...(process.env.WEBKIT_STALL_SAMPLER === '1' ? ['test/zzStallSampler.ts'] : []), + 'test/browser/styles.css', + 'test/setupBrowser.ts', + 'test/failOnConsole.ts' + ] } }, {