diff --git a/.github/workflows/apk.yml b/.github/workflows/apk.yml index 9e6119b7..0e1a188f 100644 --- a/.github/workflows/apk.yml +++ b/.github/workflows/apk.yml @@ -537,11 +537,6 @@ jobs: pattern: MobileGL-android-retrace-result-* path: retrace-artifacts - - name: Install PDF renderer - run: | - npm install --no-save --no-package-lock playwright - npx playwright install --with-deps chromium - - name: Render retrace summary run: | node tools/trace_replay/render_retrace_summary.mjs \ @@ -549,21 +544,11 @@ jobs: --output-dir android-retrace-summary \ --title "MobileGL Android retrace overview" \ --group-label "Android Emulator" \ - --html mobilegl-android-retrace-overview.html \ - --pdf mobilegl-android-retrace-overview.pdf + --html mobilegl-android-retrace-overview.html - name: Upload Android retrace summary - uses: actions/upload-artifact@v4 - with: - name: MobileGL-android-retrace-summary-${{ env.date_today }}-${{ env.short_sha }} - path: | - android-retrace-summary/** - !android-retrace-summary/mobilegl-android-retrace-overview.pdf - if-no-files-found: error - - - name: Upload Android retrace summary PDF uses: actions/upload-artifact@v7 with: - path: android-retrace-summary/mobilegl-android-retrace-overview.pdf + path: android-retrace-summary/mobilegl-android-retrace-overview.html archive: false if-no-files-found: error diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b1ccd901..488f2695 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -448,11 +448,6 @@ jobs: pattern: retrace-result-* path: retrace-artifacts - - name: Install PDF renderer - run: | - npm install --no-save --no-package-lock playwright - npx playwright install --with-deps chromium - - name: Render retrace summary run: | node tools/trace_replay/render_retrace_summary.mjs \ @@ -460,22 +455,12 @@ jobs: --output-dir retrace-summary \ --title "MobileGL Linux retrace overview" \ --group-label "Linux" \ - --html mobilegl-linux-retrace-overview.html \ - --pdf mobilegl-linux-retrace-overview.pdf + --html mobilegl-linux-retrace-overview.html - name: Upload retrace summary - uses: actions/upload-artifact@v4 - with: - name: retrace-summary-${{ env.date_today }}-${{ env.short_sha }} - path: | - retrace-summary/** - !retrace-summary/mobilegl-linux-retrace-overview.pdf - if-no-files-found: error - - - name: Upload retrace summary PDF uses: actions/upload-artifact@v7 with: - path: retrace-summary/mobilegl-linux-retrace-overview.pdf + path: retrace-summary/mobilegl-linux-retrace-overview.html archive: false if-no-files-found: error diff --git a/tools/trace_replay/render_retrace_summary.mjs b/tools/trace_replay/render_retrace_summary.mjs index 2acb3724..94fa5003 100644 --- a/tools/trace_replay/render_retrace_summary.mjs +++ b/tools/trace_replay/render_retrace_summary.mjs @@ -1,8 +1,7 @@ #!/usr/bin/env node -import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; +import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; import path from "node:path"; -import { pathToFileURL } from "node:url"; const BACKENDS = ["DirectGLES", "DirectVulkan"]; const CASES = [ @@ -33,10 +32,7 @@ function usage() { node tools/trace_replay/render_retrace_summary.mjs \\ --input DIR [--input DIR ...] \\ --output-dir DIR \\ - [--title TITLE] [--group-label LABEL] [--html NAME] [--pdf NAME] - -When --pdf is provided, the script requires Playwright to be installed in the -current Node environment.`); + [--title TITLE] [--group-label LABEL] [--html NAME]`); } function parseArgs(argv) { @@ -46,7 +42,6 @@ function parseArgs(argv) { title: "MobileGL retrace overview", groupLabel: "retrace", htmlName: "mobilegl-retrace-overview.html", - pdfName: "", }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; @@ -66,9 +61,6 @@ function parseArgs(argv) { } else if (arg === "--html" && value) { args.htmlName = value; i += 1; - } else if (arg === "--pdf" && value) { - args.pdfName = value; - i += 1; } else if (arg === "--help" || arg === "-h") { usage(); process.exit(0); @@ -274,14 +266,11 @@ function htmlEscape(value) { .replaceAll('"', """); } -async function copyAsset(source, outputDir, assetName) { +async function embedPng(source) { if (!source) return ""; if (!(await isUsablePng(source))) return ""; - const assetsDir = path.join(outputDir, "assets"); - await mkdir(assetsDir, { recursive: true }); - const destination = path.join(assetsDir, assetName); - await copyFile(source, destination); - return normalizeSlashes(path.relative(outputDir, destination)); + const data = await readFile(source); + return `data:image/png;base64,${data.toString("base64")}`; } function statusText(row) { @@ -304,27 +293,26 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) { const cells = new Map(); for (const row of rows) { - const base = `${safeCase(row.group)}-${row.backend}-${safeCase(row.caseName)}`; cells.set(`${row.group}\0${row.backend}\0${row.caseName}`, { ...row, - actualRel: await copyAsset(row.actual, outputDir, `${base}-actual.png`), - goldenRel: await copyAsset(row.golden, outputDir, `${base}-golden.png`), - diffRel: await copyAsset(row.diff, outputDir, `${base}-diff.png`), + actualSrc: await embedPng(row.actual), + goldenSrc: await embedPng(row.golden), + diffSrc: await embedPng(row.diff), }); } const thumb = (src, label) => `
-
${label}
- ${src ? `${label}` : `
NO IMAGE
`} +
${htmlEscape(label)}
+ ${src ? `` : `
NO IMAGE
`}
`; - const missingImages = (row) => !row.actualRel || !row.goldenRel || !row.diffRel; + const missingImages = (row) => !row.actualSrc || !row.goldenSrc || !row.diffSrc; const cellFor = (group, backend, caseName) => cells.get(`${group}\0${backend}\0${caseName}`) ?? { status: "NO_RESULT", - actualRel: "", - goldenRel: "", - diffRel: "", + actualSrc: "", + goldenSrc: "", + diffSrc: "", }; const backendCell = (group, backend, caseName) => { @@ -335,9 +323,9 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) {
${backend} ${statusText(row)}
- ${thumb(row.actualRel, "actual")} - ${thumb(row.goldenRel, "golden")} - ${thumb(row.diffRel, "diff")} + ${thumb(row.actualSrc, "actual")} + ${thumb(row.goldenSrc, "golden")} + ${thumb(row.diffSrc, "diff")}
`; }; @@ -347,10 +335,12 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) { const passed = groupCells.filter((row) => row.status === "PASS").length; const failed = groupCells.filter((row) => row.status === "FAIL").length; const noResult = groupCells.filter((row) => row.status !== "PASS" && row.status !== "FAIL").length; + const passRateDenominator = passed + failed; + const passRate = passRateDenominator === 0 ? "0.0%" : `${(passed * 100 / passRateDenominator).toFixed(1)}%`; return `

${htmlEscape(title)} - ${htmlEscape(group)}

-

Each backend cell shows actual / golden / diff. Failed cases are red; absent results are orange; incomplete image sets get an orange notch.
PASS ${passed}, FAIL ${failed}, NO RESULT ${noResult}.

+

Each backend cell shows actual / golden / diff. Failed cases are red; absent results are orange; incomplete image sets get an orange notch.
PASS ${passed}, FAIL ${failed}, NO RESULT ${noResult}, PASS RATE ${passRate}.

case
DirectGLES
@@ -478,6 +468,16 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) { font-size: 9px; font-weight: 700; } + .image-button { + width: 100%; + height: 100%; + display: block; + margin: 0; + padding: 0; + border: 0; + background: transparent; + cursor: zoom-in; + } .thumb img { width: 100%; height: 100%; @@ -493,10 +493,30 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) { font-weight: 700; color: var(--text); } + .lightbox { + position: fixed; + inset: 0; + z-index: 20; + display: grid; + place-items: center; + padding: 28px; + background: rgb(0 0 0 / 86%); + cursor: zoom-out; + } + .lightbox[hidden] { + display: none; + } + .lightbox img { + max-width: min(96vw, 1600px); + max-height: 94vh; + object-fit: contain; + box-shadow: 0 18px 70px rgb(0 0 0 / 70%); + } @page { size: 1170px 2380px; margin: 0; } @media print { main { padding: 10px; } .group { margin-bottom: 0; } + .lightbox { display: none; } } @@ -504,6 +524,35 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) {
${groupSections}
+ + `; @@ -512,66 +561,12 @@ ${groupSections} return output; } -function csvEscape(value) { - const text = String(value ?? ""); - return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text; -} - -async function writeCsv(rows, outputDir) { - const header = ["Group", "Backend", "Case", "Status", "SSIM", "Message", "Actual", "Golden", "Diff"]; - const sorted = [...rows].sort((a, b) => - a.group.localeCompare(b.group) || - BACKENDS.indexOf(a.backend) - BACKENDS.indexOf(b.backend) || - CASES.indexOf(a.caseName) - CASES.indexOf(b.caseName) - ); - const serialize = (row) => [ - row.group, - row.backend, - row.caseName, - row.status, - row.ssim, - row.message, - row.actual, - row.golden, - row.diff, - ].map(csvEscape).join(","); - await writeFile(path.join(outputDir, "summary.csv"), [header.join(","), ...sorted.map(serialize)].join("\n"), "utf8"); - await writeFile(path.join(outputDir, "failures.csv"), [header.join(","), ...sorted.filter((row) => row.status !== "PASS").map(serialize)].join("\n"), "utf8"); -} - -async function renderPdf(htmlPath, pdfPath) { - let chromium; - try { - ({ chromium } = await import("playwright")); - } catch (error) { - throw new Error("Playwright is required for --pdf. Install it with: npm install playwright && npx playwright install chromium"); - } - const browser = await chromium.launch(); - try { - const page = await browser.newPage({ viewport: { width: 1170, height: 2380 }, deviceScaleFactor: 1 }); - await page.goto(pathToFileURL(htmlPath).href, { waitUntil: "networkidle" }); - await page.pdf({ - path: pdfPath, - printBackground: true, - preferCSSPageSize: true, - }); - } finally { - await browser.close(); - } -} - async function main() { const args = parseArgs(process.argv.slice(2)); await mkdir(args.outputDir, { recursive: true }); const rows = await collectRows(args.inputs, args.groupLabel); const htmlPath = await renderHtml(rows, args.outputDir, args.title, args.groupLabel, args.htmlName); - await writeCsv(rows, args.outputDir); console.log(htmlPath); - if (args.pdfName) { - const pdfPath = path.join(args.outputDir, args.pdfName); - await renderPdf(htmlPath, pdfPath); - console.log(pdfPath); - } } main().catch((error) => {