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

Eager DOM creation for module filter triggers delay in test execution #1664

Closed
mixonic opened this issue Nov 4, 2021 · 4 comments
Closed

Comments

@mixonic
Copy link
Contributor

mixonic commented Nov 4, 2021

At the codepath toolbarModuleFilter where moduleListHtml is called the list of all modules in a test suite is iterated and a set of sibling <li> elements is created. For a given test suite, the number of nodes created may be fairly high (for example about 800 in a large codebase I maintain). When these are appended to the DOM, Chrome seems to trigger a slow frame after the append where some kind of internal bookkeeping for the DOM is handled.

This may manifest as a 5-15 second delay before tests run (in some cases worse). On the devtools timeline, it looks like this:

Screen Shot 2021-11-04 at 2 22 27 PM

In Firefox, this does not seem be reliably be an issue. I suspect there is a Chrome issue when there are a large number of sibling nodes in the DOM, but so far haven't been able to run that down.

Regardless, removing moduleListHtml causes the entire long task to go away. Re-writing the moduleListHtml function to manipulate DOM instead of generating HTML did not improve the issue.

In toolbarModuleFilter, the call to moduleListHtml is being run eagerly. It runs regardless of if the module dropdown is going to be opened. There are several paths forward to improving QUnit's load time (even absent the pathological issue in Chrome) that could be considered:

  • The module filter should only ever show the top 20 modules. If the user needs a specific module, they can search.
  • The module filter should not populate the contents of the dropdown until the box is interacted with.
  • The module filter should never populate modules unless a search is happening.

I'm happy to land an upstream change. What approach, or what other approach, seems viable?

Tell us about your runtime:

  • QUnit version: 2.17.2
  • Which environment are you using? (e.g., browser, Node): Chrome Version 95.0.4638.69 (Official Build) (x86_64)
  • How are you running QUnit? (e.g., QUnit CLI, Grunt, Karma, manually in browser): Manual page load via Ember CLI.
@Krinkle
Copy link
Member

Krinkle commented Nov 16, 2021

Thanks for the report. I think within the QUnit 2.x series, in the interest of compatibility, I'd recommend the second approach:

The module filter should not populate the contents of the dropdown until the box is interacted with.

I'll also take a look this weekend to reproduce the issue in Chrome. There's a very small feeling that Chrome may be misattributing this, which I'd like to rule out, but the improvement would be welcome either way. If confirmed, I'd also like to report it to upstream Chromium if you haven't already.

Thanks again!

@Krinkle Krinkle self-assigned this Apr 11, 2022
Krinkle added a commit that referenced this issue Apr 11, 2022
@Krinkle
Copy link
Member

Krinkle commented Apr 11, 2022

OK. This is definitely slow to load in Firefox as well, and there isn't anything else going on in this example test suite, so it's gotta be the module count.

I made a few related changes to the performance of this at #1685.

@mixonic I propose we do both of the following:

  1. Yes, let's not bother generating it onload. We can defer moduleListHtml to the (first) searchFocus call.
  2. Yes, let's limit the absolute count.

I'd aim for 50 rather than 20, and to keep it populated on open (similarly, upto 50 instead of unlimited). This keeps the free navigation we have today that I think is valuable for beginners, to scroll and explore when uncertain. For the simple cases when one has under 50 modules in total, I think it's benefitial to the experience to have the list there right away and select what you need. (I don't expect to make use of that myself.)

We can ignore the compat concern from my previous comment. This would apply to themes, but that's for styling. I'm not aware of an (undocumented) expectation in plugins or themes relying on suggestions being in the DOM by default. And in terms of the results being complete, prior to #1685 we showed relatively few suggestions, and I actually increased it by removing the scoring threshold. To avoid making large test suites feel even slower, I think we should bring back some limitation to the results prior to releasing that change, but this time by absolute count rather than by fuzzy scoring threshold.

@Krinkle Krinkle removed their assignment Apr 11, 2022
@mixonic
Copy link
Contributor Author

mixonic commented Apr 11, 2022

I did take a stab at this while waiting for triage here: mixonic@9590890 it seems pretty simple. Via the branch at https://github.com/mixonic/qunit/commits/mixonic/lazy-dropdown-built we've been using that successfully.

Funny enough, we always found Firefox fast. It was only for a subset of our Chrome users that it was extremely slow (never for all).

Krinkle added a commit that referenced this issue Apr 17, 2022
== Background ==

As part of fixing #1664, I
found that the minimal patch of simply appending the module HTML
on focus, no longer worked correctly in certain edge cases due to
various refactors that took place since.

This commit is the prep work to make the code simpler again, which
then makes fixing #1664 really simple. I also fixed numerous bugs
I found along the way, and improved the usability in a couple of
ways.

== Fix bugs ==

Most of these are specific to a narrow viewport, such as the iframe
on the https://qunitjs.com homepage!

* Dropdown menu was forced to 400px min-width, causing overflow
  on narrow viewports.

  Fixed for modern browsers by limiting min-width to 80vw.

* The dropdown menu was positioned too high in narrow viewports. The
  position was based on `top: 50%; margin-top: 0.8em;`, which
  approximated "1 line-height from the top".
  This worked in most viewports, where the dropdown label and input
  render next to each other. However, in narrow viewports, they wrap
  are render below each other, and "1 line from the top" is then
  overlapping half-way through the input field.

  Fixed by positioning the menu at `top: 100%` instead, which is
  always below the input field.

* The dropdown "arrow" icon was dislodged from the input field in
  narrow viewports due to being positioned absolutely on the right
  edge of the parent `<form>` and `<label>`. These are inline-block,
  which automatically follows the needed width, except when children
  wrap over multiple lines, then it reports its width as 100% even
  if there isn't anything in that space.

  Fixed by giving the parent element `text-align: right;` so that
  the dropdown menu and input field start from the right even in
  narrow viewports. On wider viewports this was already accomodated
  by `float: right;` for the then-single-line element.

* Dropdown items had `text-overflow: ellipsis;` but this did not
  work in practice because it was applied to the `<li>` element
  instead of the `<label>` where the text resides. The second reason
  is that the dropdown menu did not have a maximum width, which meant
  longer module names resulted in the menu getting wider instead of
  the options being trimmed with ellipsis. This is good and desirable
  when there is sufficient space.

  Fixed by seting a liberal max-width, and moving overflow to the
  `<label>` element, which the ellipsis will appear as-needed.

* There was an uncoloured padding between the line that divided
  the dropdown actions from the dropdown list, and the first selected
  item's background color.

  Fixed by removing padding from `#qunit-modulefilter-dropdown-list`.

== Usability improvements ==

* When searching for a module, selecting it, and then searching
  for something else; previously the only indication of what you
  selected so far was the "placeholder" of the input field.
  Apart from being cropped and thus not showing most of the info,
  this was by definition not visible when actually using the filter
  to search. It was only shown when not using the filter.

  Get rid of this placeholder, and instead hoist the selected modules
  to the top of the results, shown always, even if they don't match.

  To ensure a retention of vision and ofer an easy way to undo
  accidental clicks, we only hoist options when re-rendering the
  dropdown (e.g. after deciding to refine or change your search).
  When the results are idle, clicks simply toggle the options
  in their current position.

* Remove display toggling of the Reset/Apply buttons. Let these
  always be visible, with "Apply" always available, and the "Reset"
  button disabled when the selection is identical to the last-used
  (same as before).

* Replace the "All modules" checkbox with a "Select none" button,
  and for the common case where this is functionally identical
  to the "Reset" button, hide it to reduce clutter.

* Add tooltips to buttons to clarify their meaning.

* Add proper button styles, including high-contrast border
  and hover/active/disabled styles.

* For most viewports (wider than 350px) remove the reserved space for
  dropdown action buttons, and instead float them to the side and
  overlap the right side of the first dropdown result.

== Remove ==

Unused code:

```
 #qunit-modulefilter-dropdown a {
   color: inherit;
   text-decoration: none;
 }
```

Originally added in d14107a for the "All modules" action, which
was an anchor link at the time. This hasn't been the case of a while.
Krinkle added a commit that referenced this issue Apr 17, 2022
* Remove eager rendering of the dropdown menu from the critical
  path of `QUnit.begin()`. Defer this to first focus instead.

* Limit both initial and fuzzysort results to 20 items, and
  display them in full without any inline scrollbars.

* Render the dropdown in a single parse HTML task via one innerHTML
  assignment, instead of N createElement calls and N parse HTML tasks.

Fixes #1664.
@Krinkle Krinkle self-assigned this Apr 17, 2022
@Krinkle Krinkle added this to the 2.x release milestone Apr 17, 2022
Krinkle added a commit that referenced this issue Apr 17, 2022
== Background ==

As part of fixing #1664, I
found that the minimal patch of simply appending the module HTML
on focus, no longer worked correctly in certain edge cases due to
various refactors that took place since.

This commit is the prep work to make the code simpler again, which
then makes fixing #1664 really simple. I also fixed numerous bugs
I found along the way, and improved the usability in a couple of
ways.

== Fix bugs ==

Most of these are specific to a narrow viewport, such as the iframe
on the https://qunitjs.com homepage!

* Dropdown menu was forced to 400px min-width, causing overflow
  on narrow viewports.

  Fixed for modern browsers by limiting min-width to 80vw.

* The dropdown menu was positioned too high in narrow viewports. The
  position was based on `top: 50%; margin-top: 0.8em;`, which
  approximated "1 line-height from the top".
  This worked in most viewports, where the dropdown label and input
  render next to each other. However, in narrow viewports, they wrap
  are render below each other, and "1 line from the top" is then
  overlapping half-way through the input field.

  Fixed by positioning the menu at `top: 100%` instead, which is
  always below the input field.

* The dropdown "arrow" icon was dislodged from the input field in
  narrow viewports due to being positioned absolutely on the right
  edge of the parent `<form>` and `<label>`. These are inline-block,
  which automatically follows the needed width, except when children
  wrap over multiple lines, then it reports its width as 100% even
  if there isn't anything in that space.

  Fixed by giving the parent element `text-align: right;` so that
  the dropdown menu and input field start from the right even in
  narrow viewports. On wider viewports this was already accomodated
  by `float: right;` for the then-single-line element.

* Dropdown items had `text-overflow: ellipsis;` but this did not
  work in practice because it was applied to the `<li>` element
  instead of the `<label>` where the text resides. The second reason
  is that the dropdown menu did not have a maximum width, which meant
  longer module names resulted in the menu getting wider instead of
  the options being trimmed with ellipsis. This is good and desirable
  when there is sufficient space.

  Fixed by seting a liberal max-width, and moving overflow to the
  `<label>` element, which the ellipsis will appear as-needed.

* There was an uncoloured padding between the line that divided
  the dropdown actions from the dropdown list, and the first selected
  item's background color.

  Fixed by removing padding from `#qunit-modulefilter-dropdown-list`.

== Usability improvements ==

* When searching for a module, selecting it, and then searching
  for something else; previously the only indication of what you
  selected so far was the "placeholder" of the input field.
  Apart from being cropped and thus not showing most of the info,
  this was by definition not visible when actually using the filter
  to search. It was only shown when not using the filter.

  Get rid of this placeholder, and instead hoist the selected modules
  to the top of the results, shown always, even if they don't match.

  To ensure a retention of vision and ofer an easy way to undo
  accidental clicks, we only hoist options when re-rendering the
  dropdown (e.g. after deciding to refine or change your search).
  When the results are idle, clicks simply toggle the options
  in their current position.

* Remove display toggling of the Reset/Apply buttons. Let these
  always be visible, with "Apply" always available, and the "Reset"
  button disabled when the selection is identical to the last-used
  (same as before).

* Replace the "All modules" checkbox with a "Select none" button,
  and for the common case where this is functionally identical
  to the "Reset" button, hide it to reduce clutter.

* Add tooltips to buttons to clarify their meaning.

* Add proper button styles, including high-contrast border
  and hover/active/disabled styles.

* For most viewports (wider than 350px) remove the reserved space for
  dropdown action buttons, and instead float them to the side and
  overlap the right side of the first dropdown result.

== Remove ==

Unused code:

```
 #qunit-modulefilter-dropdown a {
   color: inherit;
   text-decoration: none;
 }
```

Originally added in d14107a for the "All modules" action, which
was an anchor link at the time. This hasn't been the case of a while.
@Krinkle
Copy link
Member

Krinkle commented Apr 18, 2022

For future reference, here is a before and after of the usability and design changes I made in preparation for fixing this bug. The code changes were committed separately (design and prep in 06c3951, actual bug fix in 1222426).

Before After
Keyboard navigation (animation) Kapture before Kapture after
Button and list design Before After
Selection and cursor Before After
Placeholder Before After

Highlights:

  • There is no longer a "dead" tab target between the action buttons and the first search result (see above animation).
  • The focus ring for the "Apply" button is no longer clipped on two sides by hidden overflow (see above animation).
  • Previously selected choices are always shown, even if not matched by current search input.
  • More breathable design for results and buttons. Cursor background is bright instead of dark. Toolbar button no longer lost in the greyness of the background.

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

No branches or pull requests

2 participants