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

[Screenshotting] Fix potential race condition when screenshotting #123820

Conversation

jloleysens
Copy link
Contributor

@jloleysens jloleysens commented Jan 26, 2022

Summary

Closes #122391

We reduce unnecessary reflow by:

  • Approximating screenshot view port size at page creation time. In all cases width will be set correctly, for print-optimized the viewport will be height adjusted, causing a vertical-only reflow. This improves the need to adjust charts horizontally which has been observed to be an issue.
  • We naively chain setting viewport size before looking for "render complete" visualisations which should create a slight buffer for the browser to complete reflow and redraw.

How to test

Manually test creating a visual report for:

  • Canvas (regular + full page)
  • Dashboard (regular + print-optimized)
  • Visualize

In all cases check that the resulting report looks as expected.

Notes

@jloleysens jloleysens added discuss (Deprecated) Feature:Reporting Use Reporting:Screenshot, Reporting:CSV, or Reporting:Framework instead release_note:skip Skip the PR/issue when compiling release notes Team:Reporting Services v8.1.0 v8.2.0 v8.0.1 labels Jan 26, 2022
@jloleysens jloleysens requested a review from tsullivan January 26, 2022 13:38
Comment on lines 187 to 191
return driver
.setViewport(viewport, this.logger)
.then(() =>
waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout)
);
Copy link
Member

Choose a reason for hiding this comment

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

Hi, I've just a side note, not related to this PR but something that can be considered in the future to improve the rendering time: what about considering setting the viewport before opening the page? this will skip at least 1 rendering cycle to resize all the visualizations. We can probably get this information from the URL and/or the saved object related to that dashboard/viz.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey @markov00 ! That's a great idea. I was thinking we might have to do something a bit more complicated (like inject a resize observer) to really know when the reflow is done 😅 . Your suggestion sounds a lot simpler and definitely worth looking into.

Copy link
Member

Choose a reason for hiding this comment

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

It seems the reason we do this here is because this is the point where we know how many items there are to render. For print layout, we have to know the number of items to resize the viewport: https://github.com/elastic/kibana/blob/main/x-pack/plugins/screenshotting/server/layouts/print_layout.ts#L47

Copy link
Member

Choose a reason for hiding this comment

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

For a later PR perhaps: let's consider making this whole mergeMap conditional, and used only if the report job is the "optimized for print" layout mode.

        if (this.layout.id === LayoutTypes.PRINT) {
          // set the viewport to the dimentions from the job, to allow elements to flow into print-optimized layout
          const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort();
          await driver.setViewport(viewport, this.logger);
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good point @tsullivan , it looks like the resize is for height in that case so I think we should at least be able to address a "width" reflow which seems could be more problematic.

I'll open up an issue to capture your suggested change!

@jloleysens
Copy link
Contributor Author

@elasticmachine merge upstream

@kibanamachine
Copy link
Contributor

merge conflict between base and head

jloleysens and others added 6 commits January 28, 2022 12:32
…fix-potential-race-condition-when-screenshotting

* 'main' of github.com:elastic/kibana: (75 commits)
  [Reporting] Logging improvements while generating reports (elastic#123802)
  [Uptime] Default alert connectors email settings (elastic#123244)
  Update comparison series styles to match the main series (elastic#123858)
  [RAC][Uptime] remove extra dot from the uptime alert connector message (elastic#124000)
  [Exploratory view] Allow ability add extra actions in lens embeddable (elastic#123713)
  [SecuritySolution][Investigations] Add message about missing index in data view in analyzer (elastic#122859)
  [TSVB] Formatting in the left axis is not respected when I have two separate axis (elastic#123903)
  [Discover] Remove services from component dependencies (elastic#121691)
  Stop IM rule execution if there are no events (elastic#123811)
  [Security Solution][Endpoint] Update Fleet Trusted Apps and Host Isolation Exception cards to use exception list summary API (elastic#123900)
  [Security Solution][Exceptions] Switches modal to flyout component (elastic#123408)
  [Workplace Search] Fix bug where modal visible after deleting a group (elastic#123976)
  [Alerting] Remove state variables from action variable menu (elastic#123702)
  replace deprecated api usage (elastic#123970)
  Fix package policy merge logic for boolean values (elastic#123974)
  [Security Solution][Endpoint][Policy] Remove GET policy list api route (elastic#123873)
  Reenable alert_add test suite (elastic#123862)
  [Fleet] Remove usage of IFieldType in Fleet (elastic#123960)
  [Lists] Add an instance of `ExceptionListClient` with server extension points turned off to context object provided to callbacks (elastic#123885)
  [Maps] Add execution context (elastic#123651)
  ...

# Conflicts:
#	x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts
@jloleysens
Copy link
Contributor Author

@elasticmachine merge upstream

@jloleysens jloleysens marked this pull request as ready for review January 31, 2022 10:43
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-reporting-services (Team:Reporting Services)

@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-app-services (Team:AppServicesUx)

@jloleysens
Copy link
Contributor Author

@elasticmachine merge upstream

@jloleysens
Copy link
Contributor Author

@elasticmachine merge upstream

@jloleysens
Copy link
Contributor Author

@elasticmachine merge upstream

@jloleysens
Copy link
Contributor Author

@elasticmachine merge upstream

// browser reflow. In most cases only the height needs to be adjusted
// before taking a screenshot.
// NOTE: _.defaults assigns to the target object, so we copy it.
defaultViewport: _.defaults({ ...defaultViewport }, DEFAULT_VIEWPORT),
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason why this can't be just:

Suggested change
defaultViewport: _.defaults({ ...defaultViewport }, DEFAULT_VIEWPORT),
defaultViewport: { ...DEFAULT_VIEWPORT, ...defaultViewport },

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason I chose _.defaults is because the override behaviour is different with own props (it ignores overrides that are undefined, so we won't end up with width: undefined for ex.).

Do you think we should change the type of defaultViewport making both width and height required values? I see they are both required in the Size object. WDYT?

Copy link
Contributor Author

@jloleysens jloleysens Feb 7, 2022

Choose a reason for hiding this comment

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

I tested this locally, and it looks like height can currently be undefined when passing it directly from layout.height to the createPage call resulting in this error:

[2022-02-07T10:36:53.499+01:00][INFO ][plugins.reporting.runTask.printablePdfV2.printable_pdf_v2.execute-job.kzci20um17zm9d0062eb528s] Compiling PDF using "preserve_layout" layout...
[2022-02-07T10:36:55.542+01:00][INFO ][plugins.reporting.runTask] Saved printable_pdf_v2 job /.reporting-2022-02-06/_doc/kzci20um17zm9d0062eb528s
[2022-02-07T10:36:59.710+01:00][INFO ][plugins.screenshotting.screenshot.browser-driver] Creating browser page driver
Unhandled Promise rejection detected:

Error: Protocol error (Emulation.setDeviceMetricsOverride): Invalid parameters Failed to deserialize params.height - BINDINGS: mandatory field missing at position 88
    at /Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Connection.ts:291:57
    at new Promise (<anonymous>)
    at CDPSession.send (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Connection.ts:290:12)
    at EmulationManager.emulateViewport (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/EmulationManager.ts:41:20)
    at Page.setViewport (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Page.ts:2383:54)
    at Function.create (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Page.ts:444:37)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at Browser._createPageInContext (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Browser.ts:468:18)
    at Observable._subscribe (/Users/jeanlouisleysens/repos/work/kibana/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts:125:20)

I think, for now, we can leave this default using the lodash algo. WDYT?

/**
* We naively round all numeric values in the object, this will break screenshotting
* if ever a have a non-number set as a value, but this points to an issue
* in the code responsible for creating the dimensions object.
Copy link
Member

Choose a reason for hiding this comment

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

would it be possible to have a unit test that explores this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, I think we could, but it may need to be a funcitonal test because the "break" occurs when we pass any non integer value to Chromium when launching (so floats or NaN would break it).

Is that the kind of test you had in mind?

@@ -36,6 +37,12 @@ import { getMetrics, PerformanceMetrics } from './metrics';

interface CreatePageOptions {
browserTimezone?: string;
defaultViewport?: {
Copy link
Member

Choose a reason for hiding this comment

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

Should this be a required property instead of optional? It looks like the only area it's not given is in tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, let's do that! I'll also make both height and width required values!

Copy link
Member

Choose a reason for hiding this comment

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

Great! If it doesn't impact anything (existing POST URLs?), it would make things more clear.

@jloleysens
Copy link
Contributor Author

@elasticmachine merge upstream

Comment on lines 183 to 187
return driver
.setViewport(viewport, this.logger)
.then(() =>
waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout)
);
Copy link
Contributor

@dokmic dokmic Feb 8, 2022

Choose a reason for hiding this comment

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

Suggested change
return driver
.setViewport(viewport, this.logger)
.then(() =>
waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout)
);
await driver.setViewport(viewport, this.logger)
await waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout);

@jloleysens
Copy link
Contributor Author

@elasticmachine merge upstream

@jloleysens
Copy link
Contributor Author

Hey @dokmic and @tsullivan I think I've addressed all your feedback. Do you think you could take another look?

@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

✅ unchanged

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

Copy link
Contributor

@dokmic dokmic left a comment

Choose a reason for hiding this comment

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

LGTM. Great job 👍

@jloleysens jloleysens merged commit b2b60ff into elastic:main Feb 9, 2022
@jloleysens jloleysens deleted the screenshotting/fix-potential-race-condition-when-screenshotting branch February 9, 2022 10:42
@jloleysens
Copy link
Contributor Author

@tsullivan happy to return to this in a follow up PR #123820 (comment)!

Let me know what you think, I decided to merge this in the meantime.

jloleysens added a commit to jloleysens/kibana that referenced this pull request Feb 9, 2022
…astic#123820)

* extract message from error objects

* only warn for 400 and up status codes

* naively wait for vis ready after resizing the browser viewport

* use a single default viewport size, enable layout to set default page viewport for every page that is created

* refactor viewport -> windowSize in chromium args

* allow overriding defaults and use new windowSize arg for chromium args

* always round page dimension numbers. note: this will break if we ever have a "undefined" set as a key value

* added comment

* update snapshot to new width value

* make defaultViewport a required field on createPage

* added comment

* style: use async-await rather than .then chaining. also added a comment

Co-authored-by: Kibana Machine <[email protected]>
(cherry picked from commit b2b60ff)
jloleysens added a commit to jloleysens/kibana that referenced this pull request Feb 9, 2022
…astic#123820)

* extract message from error objects

* only warn for 400 and up status codes

* naively wait for vis ready after resizing the browser viewport

* use a single default viewport size, enable layout to set default page viewport for every page that is created

* refactor viewport -> windowSize in chromium args

* allow overriding defaults and use new windowSize arg for chromium args

* always round page dimension numbers. note: this will break if we ever have a "undefined" set as a key value

* added comment

* update snapshot to new width value

* make defaultViewport a required field on createPage

* added comment

* style: use async-await rather than .then chaining. also added a comment

Co-authored-by: Kibana Machine <[email protected]>
(cherry picked from commit b2b60ff)
jloleysens added a commit that referenced this pull request Feb 9, 2022
…23820) (#125055)

* extract message from error objects

* only warn for 400 and up status codes

* naively wait for vis ready after resizing the browser viewport

* use a single default viewport size, enable layout to set default page viewport for every page that is created

* refactor viewport -> windowSize in chromium args

* allow overriding defaults and use new windowSize arg for chromium args

* always round page dimension numbers. note: this will break if we ever have a "undefined" set as a key value

* added comment

* update snapshot to new width value

* make defaultViewport a required field on createPage

* added comment

* style: use async-await rather than .then chaining. also added a comment

Co-authored-by: Kibana Machine <[email protected]>
(cherry picked from commit b2b60ff)
jloleysens added a commit that referenced this pull request Feb 9, 2022
…23820) (#125054)

* extract message from error objects

* only warn for 400 and up status codes

* naively wait for vis ready after resizing the browser viewport

* use a single default viewport size, enable layout to set default page viewport for every page that is created

* refactor viewport -> windowSize in chromium args

* allow overriding defaults and use new windowSize arg for chromium args

* always round page dimension numbers. note: this will break if we ever have a "undefined" set as a key value

* added comment

* update snapshot to new width value

* make defaultViewport a required field on createPage

* added comment

* style: use async-await rather than .then chaining. also added a comment

Co-authored-by: Kibana Machine <[email protected]>
(cherry picked from commit b2b60ff)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
(Deprecated) Feature:Reporting Use Reporting:Screenshot, Reporting:CSV, or Reporting:Framework instead discuss release_note:skip Skip the PR/issue when compiling release notes v8.0.1 v8.1.0 v8.2.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Screenshotting] Possible race condition when setting the viewport and capturing the screenshot
7 participants