Skip to content

Commit

Permalink
- Fiber architecture changes to imrove deferred rendering .
Browse files Browse the repository at this point in the history
- Use custom schedular instead requestIdle callback.
- Handle deferred rendering starving due to continuous sync update
  • Loading branch information
s-yadav committed Aug 9, 2020
1 parent 50f8ba7 commit bc339b4
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 43 deletions.
2 changes: 1 addition & 1 deletion example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<style>
html,
body {
font: 14px/1.21 'Helvetica Neue', arial, sans-serif;
font: 14px/1.21 'Helvetica Neue', Helvetica, arial, sans-serif;
font-weight: 400;
color: #444;
-webkit-font-smoothing: antialiased;
Expand Down
10 changes: 5 additions & 5 deletions example/sierpinski-triangle/SierpinskiTriangle.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ class Dot extends Component {
lineHeight: s + 'px',
background: this.state.hover ? '#ff0' : dotStyle.background,
};
if (this.state.hover) console.log(this.state.hover, props.text);
return (
<div style={style} onMouseEnter={() => this.enter()} onMouseLeave={() => this.leave()}>
{this.state.hover ? '*' + props.text + '*' : props.text}
Expand All @@ -74,14 +73,15 @@ class SierpinskiTriangle extends Component {

render() {
let { x, y, s, children } = this.props;

if (s <= targetSize) {
return (
<Dot x={x - targetSize / 2} y={y - targetSize / 2} size={targetSize} text={children} />
);
return r;
}
var newSize = s / 2;
var slowDown = false;
var slowDown = true;
if (slowDown) {
var e = performance.now() + 0.8;
while (performance.now() < e) {
Expand Down Expand Up @@ -151,7 +151,7 @@ class SierpinskiWrapper extends Component {
if (this.state.useTimeSlicing) {
// Update is time-sliced.
unstable_deferredUpdates(() => {
this.setState((state) => ({ seconds: (state.seconds % 10) + 1 }));
this.setState({ seconds: (this.state.seconds % 10) + 1 });
});
} else {
// Update is not time-sliced. Causes demo to stutter.
Expand Down Expand Up @@ -186,9 +186,9 @@ class SierpinskiWrapper extends Component {
/>
</div>
<div style={{ ...containerStyle, transform }}>
<div>
<div className="dot-container">
<SierpinskiTriangle x={0} y={0} s={1000}>
{this.state.seconds}
{seconds}
</SierpinskiTriangle>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class Component {
nodes: null,
mounted: false,
committedValues: {},
memoizedValues: null,
isDirty: false,
};

Expand Down
2 changes: 2 additions & 0 deletions src/effectLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ function handleComponentPostCommitEffect(fiber) {
// after commit is done set the current prop and state on committed values
committedValues.props = props;
committedValues.state = state;

brahmosData.memoizedValues = null;
} else {
// call effects of functional component
runEffects(fiber);
Expand Down
22 changes: 12 additions & 10 deletions src/fiber.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isComponentNode, isTagNode, isPrimitiveNode, ATTRIBUTE_NODE } from './brahmosNode';
import { UPDATE_TYPE_DEFERRED, BRAHMOS_DATA_KEY, EFFECT_TYPE_NONE } from './configs';
import { now } from './utils';
import { now, isNil } from './utils';

let currentComponentFiber;

Expand Down Expand Up @@ -48,7 +48,8 @@ export function markPendingEffect(fiber, effectType) {
}

export function cloneCurrentFiber(fiber, wipFiber, refFiber, parentFiber) {
const { root, node, part, nodeInstance, child, deferredUpdateTime } = fiber;
const { root, node, part, nodeInstance, child } = fiber;
const updateTimeKey = getUpdateTimeKey(root.updateType);

if (!wipFiber) {
wipFiber = createFiber(root, node, part);
Expand Down Expand Up @@ -84,13 +85,9 @@ export function cloneCurrentFiber(fiber, wipFiber, refFiber, parentFiber) {
wipFiber.child = child;

/**
* We should add deferred update times from current fiber.
* We don't need to add updateTime as in sync mode cloneCurrentFiber called only
* when a new fiber is created, in which case it will get the time from its parent
* and the other case is when we clone current to deferred tree,
* in which case we should add the deferredUpdateTime from the current fiber.
* We should add update times from parent fiber.
*/
wipFiber.deferredUpdateTime = deferredUpdateTime;
wipFiber[updateTimeKey] = parentFiber[updateTimeKey];

// link the new fiber to its parent or it's previous sibling
linkFiber(wipFiber, refFiber, parentFiber);
Expand Down Expand Up @@ -135,7 +132,7 @@ export function createHostFiber(domNode) {
return {
updateType: 'sync',
updateSource: 'js',
requestIdleHandle: null,
scheduleId: 0,
domNode,
forcedUpdateWith: null,
current: null,
Expand Down Expand Up @@ -213,7 +210,12 @@ export function createAndLink(node, part, currentFiber, refFiber, parentFiber) {
const { root } = refFiber;
const updateTimeKey = getUpdateTimeKey(root.updateType);
let fiber;
if (currentFiber && currentFiber.node && node && shouldClone(node, currentFiber.node)) {
if (
currentFiber &&
!isNil(currentFiber.node) &&
!isNil(node) &&
shouldClone(node, currentFiber.node)
) {
fiber = cloneCurrentFiber(currentFiber, currentFiber.alternate, refFiber, parentFiber);

// assign new node and part to the fiber
Expand Down
1 change: 1 addition & 0 deletions src/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ export function useTransition({ timeoutMs }) {

const hook = {
transitionId: getUniqueId(),
tryCount: 0,
isPending: false,
transitionTimeout: null,
pendingSuspense: [],
Expand Down
58 changes: 51 additions & 7 deletions src/processComponentFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { callLifeCycle } from './utils';
import { getPendingUpdates } from './updateMetaUtils';

import shallowEqual from './helpers/shallowEqual';
import { BRAHMOS_DATA_KEY, EFFECT_TYPE_OTHER } from './configs';
import { BRAHMOS_DATA_KEY, EFFECT_TYPE_OTHER, UPDATE_TYPE_DEFERRED } from './configs';
import { Component } from './Component';

export function getErrorBoundaryFiber(fiber) {
Expand Down Expand Up @@ -47,7 +47,7 @@ export function getErrorInfo(fiber) {
};
}

function getCurrentContext(fiber, isReused) {
function getCurrentContext(fiber) {
const {
node: { type: Component },
nodeInstance,
Expand All @@ -68,7 +68,7 @@ function getCurrentContext(fiber, isReused) {
* if provider in parent hierarchy is changed, the whole child hierarchy will
* be different and nodeInstance are not reused.
*/
if (currentContext && isReused) return currentContext;
if (currentContext) return currentContext;

// for new provider instance create a new context extending the parent context
const newContext = Object.create(context);
Expand Down Expand Up @@ -101,8 +101,10 @@ export default function processComponentFiber(fiber) {
const { node, part, root, childFiberError } = fiber;
const { type: Component, nodeType, props = {} } = node;

const isReused = false;
const isDeferredUpdate = root.updateType === UPDATE_TYPE_DEFERRED;

let shouldUpdate = true;
let usedMemoizedValue = false;
const isClassComponent = nodeType === CLASS_COMPONENT_NODE;

/**
Expand All @@ -126,7 +128,7 @@ export default function processComponentFiber(fiber) {
const brahmosData = nodeInstance[BRAHMOS_DATA_KEY];

// get current context
const context = getCurrentContext(fiber, isReused);
const context = getCurrentContext(fiber);

// store context in fiber
fiber.context = context;
Expand All @@ -137,12 +139,27 @@ export default function processComponentFiber(fiber) {
* and call all the life cycle method which comes before rendering.
*/
if (isClassComponent) {
const { committedValues } = brahmosData;
const { committedValues, memoizedValues } = brahmosData;

// if it is first render we should store the initial state on committedValues
if (isFirstRender) committedValues.state = nodeInstance.state;

const { props: prevProps, state: prevState } = committedValues;
let { props: prevProps, state: prevState } = committedValues;

if (memoizedValues && isDeferredUpdate) {
({ props: prevProps, state: prevState } = memoizedValues);
usedMemoizedValue = true;
}

//
/**
* reset the component instance values to prevProps and prevState,
* The state and prop value might have been effected by deferred rendering
* For sync render it should point to previous committed value, and for
* deferred render it should point to memoized values
*/
nodeInstance.props = prevProps;
nodeInstance.state = prevState;

const { shouldComponentUpdate } = nodeInstance;

Expand Down Expand Up @@ -205,6 +222,14 @@ export default function processComponentFiber(fiber) {
// set the new state, props, context and reset uncommitted state
nodeInstance.state = state;
nodeInstance.props = props;

// store the state and props on memoized value as well
if (isDeferredUpdate) {
brahmosData.memoizedValues = {
state,
props,
};
}
} else if (!isFirstRender) {
// for functional component call cleanEffect only on second render
// alternate will be set on second render
Expand Down Expand Up @@ -269,6 +294,25 @@ export default function processComponentFiber(fiber) {
}
// mark that the fiber has uncommitted effects
markPendingEffect(fiber, EFFECT_TYPE_OTHER);
/**
* If we are using memoized values and shouldUpdate is false,
* we might have some pending nodes from last render, in which case
* we should create new child fibers with pending nodes.
*/
} else if (usedMemoizedValue) {
const { child } = fiber;

if (!child || child.node !== brahmosData.nodes) {
createAndLink(brahmosData.nodes, part, child, fiber, fiber);

// mark that the fiber has uncommitted effects
markPendingEffect(fiber, EFFECT_TYPE_OTHER);
}
/**
* if we don't need to update the child fibers, we should clone the child fiber from current tree
* But if had memoized props and states and no update is required, it means we already are
* pointing to correct child fibers, in which case we shouldn't clone the child
*/
} else {
// clone the existing nodes
cloneChildrenFibers(fiber);
Expand Down
39 changes: 39 additions & 0 deletions src/schedular.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const RENDER_CYCLE_TIME = 5;
let lastFrameTime;

function updateFrameTime() {
requestAnimationFrame((time) => {
lastFrameTime = time;
updateFrameTime();
});
}

updateFrameTime();

function frameRemainingTime(allowedTime) {
return lastFrameTime + allowedTime - performance.now();
}

export default function schedule(root, shouldSchedule, cb) {
const { scheduleId } = root;
if (scheduleId) clearTimeout(scheduleId);

if (shouldSchedule) {
const timeOutTime = frameRemainingTime(16);
root.scheduleId = setTimeout(() => {
const { currentTransition } = root;
const tryCount = Math.min(25, currentTransition ? currentTransition.tryCount : 0);
const timeRemaining = () => {
return frameRemainingTime(RENDER_CYCLE_TIME + tryCount);
};

cb(timeRemaining);
}, timeOutTime);

return;
}

const executeAlways = () => 1;

cb(executeAlways);
}
2 changes: 2 additions & 0 deletions src/transitionUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export const TRANSITION_STATE_TIMED_OUT = 'timedOut';

export const PREDEFINED_TRANSITION_SYNC = {
transitionId: '',
tryCount: 0,
transitionState: TRANSITION_STATE_TIMED_OUT,
};

export const PREDEFINED_TRANSITION_DEFERRED = {
transitionId: getUniqueId(),
tryCount: 0,
transitionState: TRANSITION_STATE_TIMED_OUT,
};

Expand Down
30 changes: 10 additions & 20 deletions src/workLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,7 @@ import {
import processArrayFiber from './processArrayFiber';
import tearDown from './tearDown';
import { now } from './utils';

const TIME_REQUIRE_TO_PROCESS_FIBER = 2;

export function schedule(root, shouldSchedule, cb) {
// cancel the previous requestIdle handle
if (root.requestIdleHandle) cancelIdleCallback(root.requestIdleHandle);

if (shouldSchedule) {
root.requestIdleHandle = requestIdleCallback(cb, { timeout: 1000 });
return;
}

cb();
}
import schedule from './schedular';

function fiberHasUnprocessedUpdates(fiber) {
const { node, nodeInstance } = fiber;
Expand Down Expand Up @@ -172,19 +159,16 @@ export default function workLoop(fiber, topFiber) {
*/
const shouldSchedule = !shouldPreventSchedule(root);

schedule(root, shouldSchedule, (deadline) => {
schedule(root, shouldSchedule, (timeRemaining) => {
while (fiber !== topFiber) {
// process the current fiber which will return the next fiber
/**
* If there is time remaining to do some chunk of work,
* process the current fiber, and then move to next
* and keep doing it till we are out of time.
*/
if (
!shouldSchedule ||
deadline.didTimeout ||
deadline.timeRemaining() >= TIME_REQUIRE_TO_PROCESS_FIBER
) {
// if (deadline) console.log(deadline.timeRemaining());
if (timeRemaining() > 0) {
processFiber(fiber);

/**
Expand Down Expand Up @@ -215,6 +199,9 @@ export default function workLoop(fiber, topFiber) {
// set transition complete if it is not on suspended or timed out state
setTransitionComplete(currentTransition);

// reset try count
currentTransition.tryCount = 0;

/**
* if transition is completed and it does not have any effect to commit, we should remove the
* transition from pending transition
Expand Down Expand Up @@ -252,7 +239,10 @@ export function doDeferredProcessing(root) {
// set the pending transition as current transition
root.currentTransition = pendingTransition;

pendingTransition.tryCount += 1;

root.wip = cloneCurrentFiber(root.current, root.wip, root, root);

workLoop(root.wip, root);
}

Expand Down

0 comments on commit bc339b4

Please sign in to comment.