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

[Table] Batch Child Rendering #1201

Merged
merged 18 commits into from
Jun 12, 2017
Merged

[Table] Batch Child Rendering #1201

merged 18 commits into from
Jun 12, 2017

Conversation

themadcreator
Copy link
Contributor

Allows react components to add or remove a limited number of children at a time to reduce to DOM reconciliation time.

This removes jank while scrolling, but introduces a "scanline" render style for table cells. This is preferable to slow, janky frame rates.

Includes unit test for Batcher.

Batch renders are invoked using a polyfill for requestIdleCallback that has these semantics:

Invokes the provided callback on the next available frame after the stack frame is empty.

At most one callback per frame is invoked, and the callback may be delayed multiple frames until the page is idle.

Add or remove a limited number of children at a time
to reduce to DOM reconcilation time. This removes
jank from scrolling, but introduces a "scanline"
render style for table cells. This is preferable to
slow, janky framerates.

export interface IIndex<T> {
[key: string]: T;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Dict<string, T> 😱

[key: string]: T;
}

export type SimpleStringifyable = string | number | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

undefined too?

*
* A typical usage would be:
*
* ```typescript
Copy link
Contributor

Choose a reason for hiding this comment

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

"```tsx", and remove initial blank line

* batcher.startNewBatch();
*
* allChildrenKeys.forEach((prop1, index) => {
* batcher.addArgsToBatch(prop1, "prop2", index);
Copy link
Contributor

Choose a reason for hiding this comment

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

prop1, "prop2" 1, 2 intentional?

* The arguments must be simple stringifyable objects.
*/
public addArgsToBatch(...args: SimpleStringifyable[]) {
this.batchArgs[args.join("-")] = args;
Copy link
Contributor

Choose a reason for hiding this comment

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

if you must join-hash, at least use a less common character, such as | or 😱

* part of the "batch" set but not the "current" set are candidates for
* addition.
*
* The number of objects added and removed maybe limited with the `..Limit`
Copy link
Contributor

Choose a reason for hiding this comment

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

may be limited

Copy link
Contributor

Choose a reason for hiding this comment

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

ok comments in this file need some general editing, i'm not going to call out each typo here 📖

"startIndex",
"style",
"viewportRect",
] as Array<keyof IInternalHeaderProps>;
Copy link
Contributor

Choose a reason for hiding this comment

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

declare type up front instead of casting. const X: Array<...> = [...]

Copy link
Contributor

Choose a reason for hiding this comment

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

@PureRender
export class Header extends React.Component<IInternalHeaderProps, IHeaderState> {
public state: IHeaderState = {
hasSelectionEnded: false,
};

protected activationIndex: number;
private batcher = new Batcher<React.ReactElement<any>>();
Copy link
Contributor

Choose a reason for hiding this comment

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

use JSX.Element instead--it's equivalent and shorter

@PureRender
export class Header extends React.Component<IInternalHeaderProps, IHeaderState> {
public state: IHeaderState = {
hasSelectionEnded: false,
};

protected activationIndex: number;
private batcher = new Batcher<React.ReactElement<any>>();
Copy link
Contributor

Choose a reason for hiding this comment

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

use JSX.Element instead--it's equivalent and shorter

public componentWillReceiveProps(nextProps?: IHeaderProps) {
public componentWillReceiveProps(nextProps?: IInternalHeaderProps) {
const resetKeysBlacklist = { exclude: RESET_CELL_KEYS_BLACKLIST };
const resetBatcher = !Utils.shallowCompareKeys(this.props, nextProps, resetKeysBlacklist);
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldResetBatcher?

@adidahiya adidahiya changed the title Batch Child Rendering [Table] Batch Child Rendering Jun 8, 2017
@blueprint-bot
Copy link

fix isotest

Preview: documentation | table
Coverage: core | datetime

@themadcreator
Copy link
Contributor Author

Performance comparison:

Prior to these changes:
https://2997-71939872-gh.circle-artifacts.com/0/home/ubuntu/blueprint/packages/table/preview/index.html
image

With these changes:
https://3003-71939872-gh.circle-artifacts.com/0/home/ubuntu/blueprint/packages/table/preview/index.html
image

/**
* This class helps batch updates to large lists.
*
* For example, if your react component has many children, updating them all at
Copy link
Contributor

Choose a reason for hiding this comment

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

ubernit: React

* The arguments must be simple stringifyable objects.
*/
public addArgsToBatch(...args: SimpleStringifyable[]) {
this.batchArgs[args.join("|")] = args;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: going off of @giladgray's earlier comment, would be clearer for me as a newcomer to this code to see this character pulled out as a DELIMITER or RARE_DELIMITER constant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

addNewLimit = Batcher.DEFAULT_ADD_LIMIT,
removeOldLimit = Batcher.DEFAULT_REMOVE_LIMIT,
updateLimit = Batcher.DEFAULT_UPDATE_LIMIT,
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Cool formatting. 👍

const toRemove = this.setKeysOperation(this.currentObjects, this.batchArgs, "difference", removeOldLimit);
toRemove.forEach((key) => delete this.currentObjects[key]);

// remove ALL old object not in batch
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: objects

toRemove.forEach((key) => delete this.currentObjects[key]);

// remove ALL old object not in batch
const toRemoveOld = this.setKeysOperation(this.oldObjects, this.batchArgs, "difference", -1);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: a null limit seems more intuitive to me than a -1 limit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i disagree. I think it's more natural (or at least more common) to use a number than a null value

*/
requestAnimationFrame(() => {
requestAnimationFrame(() => {
postMessage(MESSAGE_EVENT_DATA, "*");
Copy link
Contributor

Choose a reason for hiding this comment

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

Putting the BILL in BrILLiant. 👏 👏 👏

* At most one callback per frame is invoked, and the callback may be delayed
* multiple frames until the page is idle.
*
* TODO: return a token from this method that allows you to cancel the callback
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we track this TODO in a Github issue if it's not going to get done immediately?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let shouldResetBatcher = !Utils.shallowCompareKeys(this.props, nextProps, resetKeysBlacklist);
// const stateDiff = Utils.shallowCompareKeys(this.state, nextState);
// //tslint:disable-next-line
// console.log(stateDiff, shouldResetBatcher, this.state, nextState);
Copy link
Contributor

Choose a reason for hiding this comment

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

remove commented code

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh whoops

return this.renderCell(row, col, extremaClasses, isGhost);
});
if (!this.batcher.isDone()) {
this.batcher.idleCallback(() => this.forceUpdate());
Copy link
Contributor

Choose a reason for hiding this comment

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

To avoid instantiating a new anonymous function on each render, can we just do this.batcher.idleCallback(this.forceUpdate)?

Copy link
Contributor Author

@themadcreator themadcreator Jun 12, 2017

Choose a reason for hiding this comment

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

the forceUpdate method not bound to this and this.forceUpdate.bind(this) is equivalent.

@@ -146,6 +180,7 @@ export class TableBody extends React.Component<ITableBodyProps, {}> {
</ContextMenuTargetWrapper>
</DragSelectable>
);

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: don't need this newline

@blueprint-bot
Copy link

lint

Preview: documentation | table
Coverage: core | datetime

@adidahiya
Copy link
Contributor

strangely, the big table stops rendering at 838,861 rows. this seems to be a problem on master and #1209 too though, so outside the scope of this PR.

image

@themadcreator
Copy link
Contributor Author

@themadcreator
Copy link
Contributor Author

Furthermore:
px-limit

@blueprint-bot
Copy link

Merge remote-tracking branch 'origin/master' into bd/table-perf

Preview: documentation | table
Coverage: core | datetime

@blueprint-bot
Copy link

Merge remote-tracking branch 'origin/master' into bd/table-perf

Preview: documentation | table
Coverage: core | datetime

@blueprint-bot
Copy link

PR Comments

Preview: documentation | table
Coverage: core | datetime

Copy link
Contributor

@cmslewis cmslewis left a comment

Choose a reason for hiding this comment

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

Looks and feels good to me. Approving! 😬

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.

5 participants