Skip to content

Commit

Permalink
feat(ViewportProvider): implement experimental scheduler to prioritiz…
Browse files Browse the repository at this point in the history
…e updates
  • Loading branch information
garthenweb committed Oct 28, 2018
1 parent 70814b7 commit 3d717f5
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 16 deletions.
6 changes: 4 additions & 2 deletions examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ const ViewportHeader = connectViewport({ omit: ['scroll'] })<{ a: string }>(
);

const DisplayScroll = () => {
const { x, y } = useScroll();
const { x, y } = useScroll({
priority: 'low',
});
return (
<>
x: {x}, y: {y}
Expand Down Expand Up @@ -116,7 +118,7 @@ class Example extends React.PureComponent<{}, { disabled: boolean }> {
}

render(
<ViewportProvider>
<ViewportProvider experimentalSchedulerEnabled>
<main role="main">
<Example />
<Placeholder />
Expand Down
6 changes: 5 additions & 1 deletion lib/ObserveViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
IViewport,
TViewportChangeHandler,
IViewportChangeOptions,
PriorityType,
} from './types';
import { warnNoContextAvailable } from './utils';

Expand All @@ -29,6 +30,7 @@ interface IProps {
disableScrollUpdates: boolean;
disableDimensionsUpdates: boolean;
deferUpdateUntilIdle: boolean;
priority: PriorityType;
}

interface IContext {
Expand All @@ -54,10 +56,11 @@ export default class ObserveViewport extends React.Component<IProps, IState> {

private tickId: NodeJS.Timer;

static defaultProps = {
static defaultProps: IProps = {
disableScrollUpdates: false,
disableDimensionsUpdates: false,
deferUpdateUntilIdle: false,
priority: 'normal',
};

constructor(props: IProps) {
Expand Down Expand Up @@ -134,6 +137,7 @@ export default class ObserveViewport extends React.Component<IProps, IState> {
notifyScroll: () => !this.props.disableScrollUpdates,
notifyDimensions: () => !this.props.disableDimensionsUpdates,
notifyOnlyWhenIdle: () => this.props.deferUpdateUntilIdle,
priority: () => this.props.priority,
recalculateLayoutBeforeUpdate: (viewport: IViewport) => {
if (this.props.recalculateLayoutBeforeUpdate) {
return this.props.recalculateLayoutBeforeUpdate(viewport);
Expand Down
95 changes: 85 additions & 10 deletions lib/ViewportProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ import {
} from './types';
import ViewportCollector from './ViewportCollector';

interface IProps {
experimentalSchedulerEnabled?: boolean;
}

interface IListener extends IViewportChangeOptions {
handler: TViewportChangeHandler;
iterations: number;
averageExecutionCost: number;
skippedIterations: number;
}

export const ViewportContext = React.createContext({
Expand All @@ -22,14 +29,51 @@ export const ViewportContext = React.createContext({
version: '__VERSION__',
});

const maxIterations = (priority: 'highest' | 'high' | 'normal' | 'low') => {
switch (priority) {
case 'highest':
return 0;
case 'high':
return 4;
case 'normal':
return 16;
case 'low':
return 64;
}
};

const shouldSkipIteration = (
{ priority: getPriority, averageExecutionCost, skippedIterations }: IListener,
budget: number,
): boolean => {
const priority = getPriority();
if (priority === 'highest') {
return false;
}
if (priority !== 'low' && averageExecutionCost <= budget) {
return false;
}
if (averageExecutionCost <= budget / 10) {
return false;
}
const probability = skippedIterations / maxIterations(priority);
if (probability >= 1) {
return false;
}
return Math.random() > probability;
};

export default class ViewportProvider extends React.PureComponent<
{},
IProps,
{ hasListeners: boolean }
> {
static defaultProps: {
experimentalSchedulerEnabled: false;
};
private listeners: IListener[] = [];
private updateListenersTick: NodeJS.Timer;

constructor(props: {}) {
constructor(props: IProps) {
super(props);
this.state = {
hasListeners: false,
Expand All @@ -46,7 +90,7 @@ export default class ViewportProvider extends React.PureComponent<
options?: { isIdle: boolean },
) => {
const { isIdle } = Object.assign({ isIdle: false }, options);
const updatableListeners = this.listeners.filter(
let updatableListeners = this.listeners.filter(
({ notifyScroll, notifyDimensions, notifyOnlyWhenIdle }) => {
if (notifyOnlyWhenIdle() && !isIdle) {
return false;
Expand All @@ -56,35 +100,66 @@ export default class ViewportProvider extends React.PureComponent<
return updateForScroll || updateForDimensions;
},
);
if (this.props.experimentalSchedulerEnabled) {
if (!isIdle) {
const budget = 16 / updatableListeners.length;
updatableListeners = updatableListeners.filter(listener => {
const skip = shouldSkipIteration(listener, budget);
if (skip) {
listener.skippedIterations++;
return false;
}
listener.skippedIterations = 0;
return true;
});
}
}
const layouts = updatableListeners.map(
({ recalculateLayoutBeforeUpdate }) => {
if (recalculateLayoutBeforeUpdate) {
return recalculateLayoutBeforeUpdate(state);
const start = performance.now();
const layoutState = recalculateLayoutBeforeUpdate(state);
return [layoutState, performance.now() - start];
}
return null;
},
);

updatableListeners.forEach(({ handler }, index) => {
const layout = layouts[index];
updatableListeners.forEach((listener, index) => {
const { handler, averageExecutionCost, iterations } = listener;
const [layout, layoutCost] = layouts[index] || [null, 0];

const start = performance.now();
handler(state, layout);
const totalCost = layoutCost + performance.now() - start;
const diff = totalCost - averageExecutionCost;
const i = iterations + 1;

listener.averageExecutionCost = averageExecutionCost + diff / i;
listener.iterations = i;
});
};

addViewportChangeListener = (
handler: TViewportChangeHandler,
options: IViewportChangeOptions,
) => {
this.listeners.push({ handler, ...options });
this.updateListenersLazy();
this.listeners.push({
handler,
iterations: 0,
averageExecutionCost: 0,
skippedIterations: 0,
...options,
});
this.updateHasListenersState();
};

removeViewportChangeListener = (h: TViewportChangeHandler) => {
this.listeners = this.listeners.filter(({ handler }) => handler !== h);
this.updateListenersLazy();
this.updateHasListenersState();
};

updateListenersLazy() {
updateHasListenersState() {
clearTimeout(this.updateListenersTick);
this.updateListenersTick = setTimeout(() => {
this.setState({
Expand Down
7 changes: 4 additions & 3 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import {
createInitScrollState,
createInitDimensionsState,
} from './ViewportCollector';
import { IViewport, IScroll, IDimensions } from './types';
import { IViewport, IScroll, IDimensions, PriorityType } from './types';
import { warnNoContextAvailable } from './utils';

interface IFullOptions {
recalculateLayoutBeforeUpdate?: (props: IViewport) => any;
interface IFullOptions extends IOptions {
disableScrollUpdates?: boolean;
disableDimensionsUpdates?: boolean;
deferUpdateUntilIdle?: boolean;
}

interface IOptions {
deferUpdateUntilIdle?: boolean;
priority?: PriorityType;
recalculateLayoutBeforeUpdate?: (props: IViewport) => any;
}

Expand All @@ -43,6 +43,7 @@ const useViewportEffect = (
notifyScroll: () => !options.disableScrollUpdates,
notifyDimensions: () => !options.disableDimensionsUpdates,
notifyOnlyWhenIdle: () => Boolean(options.deferUpdateUntilIdle),
priority: () => options.priority || 'normal',
recalculateLayoutBeforeUpdate: options.recalculateLayoutBeforeUpdate,
});
return () => removeViewportChangeListener(handleViewportChange);
Expand Down
3 changes: 3 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface IViewportChangeOptions {
notifyScroll: () => boolean;
notifyDimensions: () => boolean;
notifyOnlyWhenIdle: () => boolean;
priority: () => PriorityType;
recalculateLayoutBeforeUpdate?: (viewport: IViewport) => any;
}

Expand All @@ -57,3 +58,5 @@ export type OnUpdateType = (
props: IViewport,
options: IViewportCollectorUpdateOptions,
) => void;

export type PriorityType = 'highest' | 'high' | 'normal' | 'low'

0 comments on commit 3d717f5

Please sign in to comment.