Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tracing): clip canvas contents from screenshots #33119

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

Skn0tt
Copy link
Member

@Skn0tt Skn0tt commented Oct 15, 2024

Closes #33115.

If we're happy with it, this can supercedes the "show screenshot instead of snapshot" implementation. Will remove that in a separate PR.

This PR adds logic to snapshot rendering that fetches the closest screenshot for the rendered snapshot to clip the canvas contents from it. This is best-effort, and the time alignment won't always be perfect. This is fine if there's no scrolling and the canvas has static contents. We show a warning icon to let the user know about that. Because it's based on viewport screenshots, we don't have data outside of it. Parts of the canvas that aren't available will be shown with a checkered background. Here's a demo:

Screen.Recording.2024-10-17.at.12.44.42.mov

@Skn0tt Skn0tt requested a review from dgozman October 15, 2024 13:03
@Skn0tt Skn0tt self-assigned this Oct 15, 2024
await page.waitForTimeout(1000); // ensure we could take a screenshot
});

await traceViewer.page.pause();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test isn't gonna stay, obviously. but you can use it to play around with the feature: npm run ctest -- --grep 'clipping' --headed

@dgozman what's the best way of testing this - should we trace a page with a canvas and then take a screenshot of the trace, to ensure the screenshot isn't blank? Or somehow compute if it's blank without storing a golden image?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be a flaky test, unfortunately. Perhaps just check some test-specific logging, like we do in recorder tests:

if (isUnderTest) console.error(`drawn 15;23-184;566 screenshot rect on the canvas`);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, good idea! implemented

This comment has been minimized.

This comment has been minimized.

const { wallTime, timestamp, pageId } = snapshot.snapshot();
const page = this._pages.get(pageId);
if (page) {
const closestFrame = (wallTime && page.screencastFrames[0]?.frameSwapWallTime) ? findClosest(page.screencastFrames, frame => frame.frameSwapWallTime!, wallTime) : findClosest(page.screencastFrames, frame => frame.timestamp, timestamp);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer this code to live in SnapshotRenderer instead.

Copy link
Member Author

@Skn0tt Skn0tt Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would SnapshotRenderer get access to screencastFrames? Would we pass them into .render(), or do we pass them into the constructor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've since moved the code a little, but it's still inside the server. Let me know how to proceed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we discussed this offline and ended up with 45d1b54.

if (page) {
const closestFrame = (wallTime && page.screencastFrames[0]?.frameSwapWallTime) ? findClosest(page.screencastFrames, frame => frame.frameSwapWallTime!, wallTime) : findClosest(page.screencastFrames, frame => frame.timestamp, timestamp);
if (closestFrame)
screenshotUrl = new URL(`./sha1/${closestFrame.sha1}`, swScope);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Injecting swScope here is unfortunate. Perhaps we should have a relative url for the screenshot, e.g. /screenshot/<pageOrFrameId>, and route it through sw main?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had that in a first iteration, but that requires making sw main listen to not just swScope, but also to requests from the iframe origin. Would that be fine? Alternatively, we could also use a fake origin like playwright.sw.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made it work with a new route in the service worker 👍

const xEnd = boundingRect.right / window.innerWidth;
const yEnd = boundingRect.bottom / window.innerHeight;

context.drawImage(img, xStart * img.width, yStart * img.height, (xEnd - xStart) * img.width, (yEnd - yStart) * img.height, 0, 0, canvas.width, canvas.height);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Is this going to work nicely for the out-of-viewport canvas? Perhaps we should say "not captured" there instead?
  • This code must run after scrollLeft/scrollTop are restored in onLoad().
  • We should still have a warning somewhere that would explain that image might be wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented all of it

await page.waitForTimeout(1000); // ensure we could take a screenshot
});

await traceViewer.page.pause();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be a flaky test, unfortunately. Perhaps just check some test-specific logging, like we do in recorder tests:

if (isUnderTest) console.error(`drawn 15;23-184;566 screenshot rect on the canvas`);

const canvases = root.querySelectorAll('canvas');
if (canvases.length > 0 && screenshotURL) {
const img = new Image();
img.onload = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please handle img.onerror as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

This comment has been minimized.

This comment has been minimized.

@Skn0tt
Copy link
Member Author

Skn0tt commented Oct 16, 2024

feedback from today:

  • make the pattern a grey & white checkerboard
  • put warning item in the corner

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

Copy link
Member Author

@Skn0tt Skn0tt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feedback addressed :)

const canvases = root.querySelectorAll('canvas');
if (canvases.length > 0 && screenshotURL) {
const img = new Image();
img.onload = () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const xEnd = boundingRect.right / window.innerWidth;
const yEnd = boundingRect.bottom / window.innerHeight;

context.drawImage(img, xStart * img.width, yStart * img.height, (xEnd - xStart) * img.width, (yEnd - yStart) * img.height, 0, 0, canvas.width, canvas.height);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented all of it

const { wallTime, timestamp, pageId } = snapshot.snapshot();
const page = this._pages.get(pageId);
if (page) {
const closestFrame = (wallTime && page.screencastFrames[0]?.frameSwapWallTime) ? findClosest(page.screencastFrames, frame => frame.frameSwapWallTime!, wallTime) : findClosest(page.screencastFrames, frame => frame.timestamp, timestamp);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've since moved the code a little, but it's still inside the server. Let me know how to proceed.

if (page) {
const closestFrame = (wallTime && page.screencastFrames[0]?.frameSwapWallTime) ? findClosest(page.screencastFrames, frame => frame.frameSwapWallTime!, wallTime) : findClosest(page.screencastFrames, frame => frame.timestamp, timestamp);
if (closestFrame)
screenshotUrl = new URL(`./sha1/${closestFrame.sha1}`, swScope);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made it work with a new route in the service worker 👍

await page.waitForTimeout(1000); // ensure we could take a screenshot
});

await traceViewer.page.pause();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, good idea! implemented

@Skn0tt
Copy link
Member Author

Skn0tt commented Oct 17, 2024

I've seen that this doesn't work perfectly on Firefox - gonna investigate more.

@Skn0tt
Copy link
Member Author

Skn0tt commented Oct 17, 2024

I've seen that this doesn't work perfectly on Firefox - gonna investigate more.

Seems to be a known issue with screencast frames in headed Firefox.

This comment has been minimized.

@Skn0tt
Copy link
Member Author

Skn0tt commented Oct 18, 2024

Alright, this is ready for one last review!

This comment has been minimized.

test('canvas clipping', async ({ runAndTrace, page, server }) => {
const traceViewer = await runAndTrace(async () => {
await page.goto(server.PREFIX + '/screenshots/canvas.html#canvas-on-edge');
await page.waitForTimeout(1000); // ensure we could take a screenshot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I predict this will be flaky 😄 We usually do a few rafs in the hopes it will redraw, but even that does not always help.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a raf?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages/trace-viewer/src/sw/snapshotRenderer.ts Outdated Show resolved Hide resolved
packages/trace-viewer/src/sw/snapshotRenderer.ts Outdated Show resolved Hide resolved

if (xEnd > 1 || yEnd > 1) {
if (xStart > 1 || yStart > 1)
canvas.title = `Playwright couldn't capture canvas contents because it's located outside the viewport.`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's skip both drawWarningBackground and drawImage calls in this case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made it skip drawImage, but I don't think we should skip drawCheckerboard - having that around is still valuable to show that there's something missing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also made it skip drawCheckerboard to align with what we discussed in the meeting. Gonna potentially change that in a follow-up. ea4116e

This comment has been minimized.

This comment has been minimized.

Copy link
Contributor

Test results for "tests 1"

12 failed
❌ [chromium-library] › library/trace-viewer.spec.ts:1443:1 › canvas clipping @chromium-ubuntu-22.04-node18
❌ [chromium-library] › library/trace-viewer.spec.ts:1456:1 › canvas clipping in iframe @chromium-ubuntu-22.04-node18
❌ [chromium-library] › library/trace-viewer.spec.ts:1443:1 › canvas clipping @chromium-ubuntu-22.04-node20
❌ [chromium-library] › library/trace-viewer.spec.ts:1456:1 › canvas clipping in iframe @chromium-ubuntu-22.04-node20
❌ [chromium-library] › library/trace-viewer.spec.ts:1443:1 › canvas clipping @chromium-ubuntu-22.04-node22
❌ [chromium-library] › library/trace-viewer.spec.ts:1456:1 › canvas clipping in iframe @chromium-ubuntu-22.04-node22
❌ [firefox-library] › library/trace-viewer.spec.ts:1443:1 › canvas clipping @firefox-ubuntu-22.04-node18
❌ [firefox-library] › library/trace-viewer.spec.ts:1456:1 › canvas clipping in iframe @firefox-ubuntu-22.04-node18
❌ [chromium-library] › library/trace-viewer.spec.ts:1443:1 › canvas clipping @ubuntu-20.04-chromium-tip-of-tree
❌ [chromium-library] › library/trace-viewer.spec.ts:1456:1 › canvas clipping in iframe @ubuntu-20.04-chromium-tip-of-tree
❌ [webkit-library] › library/trace-viewer.spec.ts:1443:1 › canvas clipping @webkit-ubuntu-22.04-node18
❌ [webkit-library] › library/trace-viewer.spec.ts:1456:1 › canvas clipping in iframe @webkit-ubuntu-22.04-node18

4 flaky ⚠️ [firefox-page] › page/page-set-input-files.spec.ts:39:3 › should upload a folder @firefox-ubuntu-22.04-node18
⚠️ [playwright-test] › ui-mode-test-setup.spec.ts:98:5 › should show errors in config @macos-latest-node18-1
⚠️ [playwright-test] › ui-mode-test-ct.spec.ts:59:5 › should run component tests after editing test @ubuntu-latest-node20-1
⚠️ [playwright-test] › ui-mode-test-watch.spec.ts:145:5 › should watch all @windows-latest-node18-1

36395 passed, 639 skipped
✔️✔️✔️

Merge workflow run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[internal] try to clip canvas from the image and stick on top of the canvas in DOM
2 participants