diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 757705c23c63..645a2e7b64ac 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -26,7 +26,7 @@ jobs: run: pnpm build:e2e - name: Run Playwright Tests - run: pnpm exec playwright test --output=playwright-assets + run: pnpm exec playwright test - name: 🚀 Deploy to Vercel uses: amondnet/vercel-action@v25 @@ -49,7 +49,13 @@ jobs: if: ${{ !cancelled() }} with: name: playwright-report - path: | - playwright-report/ - test-results/ + path: playwright-report/ retention-days: 30 + + - name: Upload HeapSnapshots + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: snapshots + path: test-results/*.heapsnapshot + retention-days: 3 diff --git a/.gitignore b/.gitignore index 741532cd6ec9..7e4a19a7628e 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,7 @@ typings/ # examples screenshot +*.heapsnapshot univer-custom-build universheet-custom-build diff --git a/e2e/memory/memory.spec.ts b/e2e/memory/memory.spec.ts index 56dd5b8d3c39..36358ae06397 100644 --- a/e2e/memory/memory.spec.ts +++ b/e2e/memory/memory.spec.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import type { CDPSession } from '@playwright/test'; +import { createWriteStream } from 'node:fs'; import { expect, test } from '@playwright/test'; import { getMetrics } from './util'; @@ -27,11 +29,101 @@ const MAX_UNIVER_MEMORY_OVERFLOW = 6_000_000; // TODO@wzhudev: temporarily added const MAX_SECOND_INSTANCE_OVERFLOW = 100_000; // Only 100 KB +interface HeapSnapshotChunk { + chunk: string; +} + +interface HeapSnapshotProgress { + done: number; + total: number; + finished?: boolean; +} +async function takeHeapSnapshot(client: CDPSession, filename: string) { + return new Promise((resolve, reject) => { + const file = createWriteStream(`./test-results/${filename}`); + let isFinished = false; + let error = null; + let noChunkTimeout = null; + let chunkHandler = (_: HeapSnapshotChunk) => {}; + let progressHandler = (_: HeapSnapshotProgress) => {}; + + // Cleanup function to remove listeners + const cleanup = () => { + client.off('HeapProfiler.addHeapSnapshotChunk', chunkHandler); + client.off('HeapProfiler.reportHeapSnapshotProgress', progressHandler); + }; + + // Handle file stream errors + file.on('error', (err) => { + error = err; + reject(err); + }); + + // Handle successful completion + file.on('finish', () => { + if (!error && isFinished) { + resolve(0); + } + }); + + const scheduleEnd = () => { + if (noChunkTimeout) { + clearTimeout(noChunkTimeout); + } + + // Set new timeout + noChunkTimeout = setTimeout(() => { + cleanup(); + file.end(); + }, 1000); // Wait 1 second after last chunk + }; + + // Set up the chunk handler + chunkHandler = (payload: HeapSnapshotChunk) => { + try { + if (payload.chunk) { + file.write(payload.chunk); + scheduleEnd(); + } + } catch (err) { + error = err; + console.error('chunkHandler error', err); + cleanup(); + reject(err); + } + }; + + // Set up the progress handler + progressHandler = (params: HeapSnapshotProgress) => { + if (params.finished) { + isFinished = true; + } + }; + + // Add event listeners + client.on('HeapProfiler.addHeapSnapshotChunk', chunkHandler); + client.on('HeapProfiler.reportHeapSnapshotProgress', progressHandler); + + // Start the heap snapshot process + client.send('HeapProfiler.enable') + .then(() => client.send('HeapProfiler.takeHeapSnapshot', { reportProgress: true })) + .catch((err) => { + console.error('HeapProfiler.enable error', err); + error = err; + file.end(); + cleanup(); + reject(err); + }); + }); +} + +// const isLocal = !process.env.CI; test('memory', async ({ page }) => { test.setTimeout(60_000); + const client = await page.context().newCDPSession(page); await page.goto('http://localhost:3000/sheets/'); - await page.waitForTimeout(5000); + await page.waitForTimeout(2000); const memoryAfterFirstInstance = (await getMetrics(page)).JSHeapUsedSize; @@ -47,12 +139,18 @@ test('memory', async ({ page }) => { await page.evaluate(() => window.univer.dispose()); await page.waitForTimeout(2000); + + await takeHeapSnapshot(client, 'memory-first.heapsnapshot'); + const memoryAfterDisposingFirstInstance = (await getMetrics(page)).JSHeapUsedSize; await page.evaluate(() => window.createNewInstance()); await page.waitForTimeout(2000); await page.evaluate(() => window.univer.dispose()); await page.waitForTimeout(2000); + + await takeHeapSnapshot(client, 'memory-second.heapsnapshot'); + const memoryAfterDisposingSecondUniver = (await getMetrics(page)).JSHeapUsedSize; expect(memoryAfterDisposingSecondUniver - memoryAfterDisposingFirstInstance) .toBeLessThanOrEqual(MAX_SECOND_INSTANCE_OVERFLOW); diff --git a/e2e/perf/scroll.spec.ts b/e2e/perf/scroll.spec.ts index 8f22fb2fdb3f..c60923020418 100644 --- a/e2e/perf/scroll.spec.ts +++ b/e2e/perf/scroll.spec.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import type { Page } from '@playwright/test'; /* eslint-disable no-console */ +import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; import { sheetData as emptySheetData } from '../__testing__/emptysheet'; import { sheetData as freezeData } from '../__testing__/freezesheet'; @@ -135,14 +135,6 @@ async function measureFPS(page: Page, testDuration = 5, deltaX: number, deltaY: const createTest = (title: string, sheetData: IJsonObject, minFpsValue: number, deltaX = 0, deltaY = 0) => { // Default Size Of browser: 1280x720 pixels. And default DPR is 1. test(title, async ({ page }) => { - // dev:e2e open localhost:3000, not 3002 - // let port = 3000; - // if (!isCI) { - // const browser = await chromium.launch({ headless: false }); // launch browser - // page = await browser.newPage(); - // port = 3002; - // } - await page.goto('http://localhost:3000/sheets/'); await page.waitForTimeout(2000); diff --git a/playwright.config.ts b/playwright.config.ts index d40698aa5246..094abe67db5c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,6 +18,7 @@ const HEADLESS = !!process.env.HEADLESS; */ export default defineConfig({ testDir: './e2e', + outputDir: 'test-results/', // Make sure this is set /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */