Skip to content

Commit

Permalink
[SIEM] Adds performance enhancements such by removing wasted renderer…
Browse files Browse the repository at this point in the history
…s and adding incremental DOM rendering (#43157) (#43500)

## Summary

Whenever you wiggle the "resize" bar on the timeline or any other action within the UI which alters the redux store, all the components `mapStateProps` will and redux will re-render if it sees any of the properties have a shallow compare that returns false.

Ref: https://react-redux.js.org/using-react-redux/connect-mapstate

This PR fixes Super Date Selector which unnecessarily causes re-renders due to not using selectors or memoizations per guidance from the react-redux project.

---

Super Date Selector will re-render on every single change to the redux store regardless of if it needs to render or not.

<img width="2072" alt="Screen Shot 2019-08-12 at 11 49 13 AM" src="https://user-images.githubusercontent.com/1151048/62900648-eaa39f80-bd17-11e9-91fd-ed4b1e81e27b.png">

You can see that with something as simple as a slight wiggle of the resizer of timeline causes around 23'ish re-renders
 
<img width="2338" alt="Screen Shot 2019-08-12 at 11 52 54 AM" src="https://user-images.githubusercontent.com/1151048/62900666-f7c08e80-bd17-11e9-8ddd-77093553aaf9.png">

After the fixes it no longer shows it being re-rendered on a redux change and the number or renders drop significantly when resizing timeline.

It renders only once: 
<img width="1857" alt="Screen Shot 2019-08-12 at 3 51 20 PM" src="https://user-images.githubusercontent.com/1151048/62901080-1c693600-bd19-11e9-8f8e-f19df029dd31.png">

---
Some parts of the application are expensive to render and are wasted renders by something such as anonymous functions. By swapping out the `pure` for `React.memo` and using the property compare we can cut out those renders and speed up the application considerably.

Before with all the flyout sections causing re-renders when state did not change:
<img width="1718" alt="Screen Shot 2019-08-12 at 4 46 29 PM" src="https://user-images.githubusercontent.com/1151048/62959566-589ea400-bdb6-11e9-89f1-047c268e74fd.png">

After with the fix to where the ms is really cheap and the expensive pieces are not rendered unless needed now:
<img width="1682" alt="Screen Shot 2019-08-13 at 10 32 55 AM" src="https://user-images.githubusercontent.com/1151048/62959667-82f06180-bdb6-11e9-9cc9-c9c609a0fb71.png">

---
For the timeline and its properties section it contained a large volume of JSX logic called "PropertiesLeft" and "PropertiesRight" in which both were having re-renders involving expensive calculations such as width changes to the date time when you did something as simple as type in a new title or description into the timeline. I broke those two out into PropertiesLeft and PropertiesRight subsections utilizing `React.Memo` for performance improvements.

Before when typing a title of timeline, PropertiesRight renders 49+ times:
<img width="2030" alt="properties-right-before" src="https://user-images.githubusercontent.com/1151048/63056285-f7093300-bea4-11e9-8f33-a2e63d0468ac.png">

Now it renders only 2 times and most of the time it is skipped with something as simple as a title changing:
<img width="2020" alt="properties-right-after" src="https://user-images.githubusercontent.com/1151048/63056339-10aa7a80-bea5-11e9-9c0c-ae12be5b5851.png">

---

When rending feature rich renderers on the timeline it was having a difficult time rendering them all within a reasonable amount of time. This introduces a simple scheduler of incremental loading using `requestIdleCallback` when it is available from the browser, otherwise this will fall back on a `setTimeout` as a polyfill/shim.

Ref:
https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
https://developers.google.com/web/updates/2015/08/using-requestidlecallback
https://www.w3.org/TR/requestidlecallback/

Tested on Safari, Firefox, and Chrome. IE-11 looked like it didn't crash :-)

Gif of each of the browsers incrementally rendering the rows:

Chrome:
![incremental-load-1](https://user-images.githubusercontent.com/1151048/63057036-8fec7e00-bea6-11e9-83f9-7bad3f79746f.gif)
![incremental-load-2](https://user-images.githubusercontent.com/1151048/63057043-92e76e80-bea6-11e9-8f0d-95ea2559b6c7.gif)

Firefox:
![firefox-incremental-1](https://user-images.githubusercontent.com/1151048/63057052-97138c00-bea6-11e9-8e15-19f5b7cb0a5e.gif)
![firefox-incremental-2](https://user-images.githubusercontent.com/1151048/63057060-98dd4f80-bea6-11e9-9bf3-c8ae798db9e4.gif)

Safari (polyfil/shim only working):
![safari-polyfill-1](https://user-images.githubusercontent.com/1151048/63057099-adb9e300-bea6-11e9-9b94-32a08b5905ad.gif)
![safari-polyfill-2](https://user-images.githubusercontent.com/1151048/63057101-b01c3d00-bea6-11e9-9261-77444272992d.gif)


Before Performance picture of the full load. You can see that it will render all rows of 25+ and that causes a lag of up to 2 seconds depending on how complex the renderers are:
<img width="1346" alt="Screen Shot 2019-08-13 at 4 24 56 PM" src="https://user-images.githubusercontent.com/1151048/63059739-77339680-bead-11e9-818a-ee19df596855.png">


Afterwards you can see it chops it more evenly up between the rendering and tries to get in 5 at a time and maintain a 60 FPS:

<img width="691" alt="Screen Shot 2019-08-14 at 3 59 31 PM" src="https://user-images.githubusercontent.com/1151048/63059764-83b7ef00-bead-11e9-84da-bf66de8a35e6.png">



 
### Checklist

Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR.

~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~

~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~

~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~

- [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios

~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~

### For maintainers

- [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
- [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
  • Loading branch information
FrankHassanabad committed Aug 17, 2019
1 parent 2010743 commit c08d790
Show file tree
Hide file tree
Showing 59 changed files with 2,581 additions and 2,263 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,13 @@ export const getColumns = ({
browserFields,
columnHeaders,
eventId,
isLoading,
onUpdateColumns,
timelineId,
toggleColumn,
}: {
browserFields: BrowserFields;
columnHeaders: ColumnHeader[];
eventId: string;
isLoading: boolean;
onUpdateColumns: OnUpdateColumns;
timelineId: string;
toggleColumn: (column: ColumnHeader) => void;
Expand Down Expand Up @@ -146,7 +144,6 @@ export const getColumns = ({
})}
data-test-subj="field-name"
fieldId={field}
isLoading={isLoading}
onUpdateColumns={onUpdateColumns}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe('EventDetails', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
id={mockDetailItemDataId}
isLoading={false}
view="table-view"
onUpdateColumns={jest.fn()}
onViewSelected={jest.fn()}
Expand All @@ -48,7 +47,6 @@ describe('EventDetails', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
id={mockDetailItemDataId}
isLoading={false}
view="table-view"
onUpdateColumns={jest.fn()}
onViewSelected={jest.fn()}
Expand All @@ -75,7 +73,6 @@ describe('EventDetails', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
id={mockDetailItemDataId}
isLoading={false}
view="table-view"
onUpdateColumns={jest.fn()}
onViewSelected={jest.fn()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
import * as React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';

import { BrowserFields } from '../../containers/source';
Expand All @@ -25,7 +24,6 @@ interface Props {
columnHeaders: ColumnHeader[];
data: DetailItem[];
id: string;
isLoading: boolean;
view: View;
onUpdateColumns: OnUpdateColumns;
onViewSelected: (selected: View) => void;
Expand All @@ -40,13 +38,12 @@ const Details = styled.div`

Details.displayName = 'Details';

export const EventDetails = pure<Props>(
export const EventDetails = React.memo<Props>(
({
browserFields,
columnHeaders,
data,
id,
isLoading,
view,
onUpdateColumns,
onViewSelected,
Expand All @@ -63,7 +60,6 @@ export const EventDetails = pure<Props>(
columnHeaders={columnHeaders}
data={data}
eventId={id}
isLoading={isLoading}
onUpdateColumns={onUpdateColumns}
timelineId={timelineId}
toggleColumn={toggleColumn}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe('EventFieldsBrowser', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
isLoading={false}
onUpdateColumns={jest.fn()}
timelineId="test"
toggleColumn={jest.fn()}
Expand All @@ -47,7 +46,6 @@ describe('EventFieldsBrowser', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
isLoading={false}
onUpdateColumns={jest.fn()}
timelineId="test"
toggleColumn={jest.fn()}
Expand All @@ -74,7 +72,6 @@ describe('EventFieldsBrowser', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
eventId={eventId}
isLoading={false}
onUpdateColumns={jest.fn()}
timelineId="test"
toggleColumn={jest.fn()}
Expand All @@ -100,7 +97,6 @@ describe('EventFieldsBrowser', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
eventId={eventId}
isLoading={false}
onUpdateColumns={jest.fn()}
timelineId="test"
toggleColumn={jest.fn()}
Expand All @@ -127,7 +123,6 @@ describe('EventFieldsBrowser', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
eventId={eventId}
isLoading={false}
onUpdateColumns={jest.fn()}
timelineId="test"
toggleColumn={toggleColumn}
Expand Down Expand Up @@ -161,7 +156,6 @@ describe('EventFieldsBrowser', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
isLoading={false}
onUpdateColumns={jest.fn()}
timelineId="test"
toggleColumn={jest.fn()}
Expand Down Expand Up @@ -189,7 +183,6 @@ describe('EventFieldsBrowser', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
isLoading={false}
onUpdateColumns={jest.fn()}
timelineId="test"
toggleColumn={jest.fn()}
Expand All @@ -214,7 +207,6 @@ describe('EventFieldsBrowser', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
isLoading={false}
onUpdateColumns={jest.fn()}
timelineId="test"
toggleColumn={jest.fn()}
Expand All @@ -239,7 +231,6 @@ describe('EventFieldsBrowser', () => {
columnHeaders={defaultHeaders}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
isLoading={false}
onUpdateColumns={jest.fn()}
timelineId="test"
toggleColumn={jest.fn()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
EuiInMemoryTable,
} from '@elastic/eui';
import * as React from 'react';
import { pure } from 'recompose';

import { ColumnHeader } from '../timeline/body/column_headers/column_header';
import { BrowserFields, getAllFieldsByName } from '../../containers/source';
Expand All @@ -25,24 +24,14 @@ interface Props {
columnHeaders: ColumnHeader[];
data: DetailItem[];
eventId: string;
isLoading: boolean;
onUpdateColumns: OnUpdateColumns;
timelineId: string;
toggleColumn: (column: ColumnHeader) => void;
}

/** Renders a table view or JSON view of the `ECS` `data` */
export const EventFieldsBrowser = pure<Props>(
({
browserFields,
columnHeaders,
data,
eventId,
isLoading,
onUpdateColumns,
timelineId,
toggleColumn,
}) => {
export const EventFieldsBrowser = React.memo<Props>(
({ browserFields, columnHeaders, data, eventId, onUpdateColumns, timelineId, toggleColumn }) => {
const fieldsByName = getAllFieldsByName(browserFields);
return (
<EuiInMemoryTable
Expand All @@ -57,7 +46,6 @@ export const EventFieldsBrowser = pure<Props>(
browserFields,
columnHeaders,
eventId,
isLoading,
onUpdateColumns,
timelineId,
toggleColumn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ interface Props {
columnHeaders: ColumnHeader[];
data: DetailItem[];
id: string;
isLoading: boolean;
onUpdateColumns: OnUpdateColumns;
timelineId: string;
toggleColumn: (column: ColumnHeader) => void;
Expand All @@ -45,19 +44,16 @@ export class StatefulEventDetails extends React.PureComponent<Props, State> {
columnHeaders,
data,
id,
isLoading,
onUpdateColumns,
timelineId,
toggleColumn,
} = this.props;

return (
<EventDetails
browserFields={browserFields}
columnHeaders={columnHeaders}
data={data}
id={id}
isLoading={isLoading}
view={this.state.view}
onUpdateColumns={onUpdateColumns}
onViewSelected={this.onViewSelected}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ describe('CategoriesPane', () => {
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
width={CATEGORY_PANE_WIDTH}
isLoading={false}
onCategorySelected={jest.fn()}
onUpdateColumns={jest.fn()}
selectedCategoryId={''}
Expand All @@ -45,7 +44,6 @@ describe('CategoriesPane', () => {
browserFields={mockBrowserFields}
filteredBrowserFields={{}}
width={CATEGORY_PANE_WIDTH}
isLoading={false}
onCategorySelected={jest.fn()}
onUpdateColumns={jest.fn()}
selectedCategoryId={''}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import { EuiInMemoryTable, EuiTitle } from '@elastic/eui';
import * as React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';

import { BrowserFields } from '../../containers/source';
Expand Down Expand Up @@ -35,10 +34,7 @@ const Title = styled(EuiTitle)`

Title.displayName = 'Title';

type Props = Pick<
FieldBrowserProps,
'browserFields' | 'isLoading' | 'timelineId' | 'onUpdateColumns'
> & {
type Props = Pick<FieldBrowserProps, 'browserFields' | 'timelineId' | 'onUpdateColumns'> & {
/**
* A map of categoryId -> metadata about the fields in that category,
* filtered such that the name of every field in the category includes
Expand All @@ -55,11 +51,11 @@ type Props = Pick<
/** The width of the categories pane */
width: number;
};
export const CategoriesPane = pure<Props>(

export const CategoriesPane = React.memo<Props>(
({
browserFields,
filteredBrowserFields,
isLoading,
onCategorySelected,
onUpdateColumns,
selectedCategoryId,
Expand All @@ -76,7 +72,6 @@ export const CategoriesPane = pure<Props>(
columns={getCategoryColumns({
browserFields,
filteredBrowserFields,
isLoading,
onCategorySelected,
onUpdateColumns,
selectedCategoryId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ describe('Category', () => {
categoryId: selectedCategoryId,
columnHeaders: [],
highlight: '',
isLoading: false,
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
Expand Down Expand Up @@ -66,7 +65,6 @@ describe('Category', () => {
categoryId: selectedCategoryId,
columnHeaders: [],
highlight: '',
isLoading: false,
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
Expand Down Expand Up @@ -99,7 +97,6 @@ describe('Category', () => {
categoryId: selectedCategoryId,
columnHeaders: [],
highlight: '',
isLoading: false,
onUpdateColumns: jest.fn(),
timelineId,
toggleColumn: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ describe('getCategoryColumns', () => {
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
width={CATEGORY_PANE_WIDTH}
isLoading={false}
onCategorySelected={jest.fn()}
onUpdateColumns={jest.fn()}
selectedCategoryId={''}
Expand All @@ -50,7 +49,6 @@ describe('getCategoryColumns', () => {
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
width={CATEGORY_PANE_WIDTH}
isLoading={false}
onCategorySelected={jest.fn()}
onUpdateColumns={jest.fn()}
selectedCategoryId={''}
Expand All @@ -75,7 +73,6 @@ describe('getCategoryColumns', () => {
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
width={CATEGORY_PANE_WIDTH}
isLoading={false}
onCategorySelected={jest.fn()}
onUpdateColumns={jest.fn()}
selectedCategoryId={''}
Expand Down Expand Up @@ -103,7 +100,6 @@ describe('getCategoryColumns', () => {
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
width={CATEGORY_PANE_WIDTH}
isLoading={false}
onCategorySelected={jest.fn()}
onUpdateColumns={jest.fn()}
selectedCategoryId={selectedCategoryId}
Expand All @@ -127,7 +123,6 @@ describe('getCategoryColumns', () => {
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
width={CATEGORY_PANE_WIDTH}
isLoading={false}
onCategorySelected={jest.fn()}
onUpdateColumns={jest.fn()}
selectedCategoryId={selectedCategoryId}
Expand All @@ -153,7 +148,6 @@ describe('getCategoryColumns', () => {
browserFields={mockBrowserFields}
filteredBrowserFields={mockBrowserFields}
width={CATEGORY_PANE_WIDTH}
isLoading={false}
onCategorySelected={onCategorySelected}
onUpdateColumns={jest.fn()}
selectedCategoryId={selectedCategoryId}
Expand Down
Loading

0 comments on commit c08d790

Please sign in to comment.