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

Feature: Suspend commit without blocking render #26398

Merged
merged 3 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,17 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}

export function shouldSuspendCommit(type, props) {
return false;
}

export function startSuspendingCommit() {}

export function suspendInstance(type, props) {}

export function waitForCommitToBeReady() {
return null;
}
// eslint-disable-next-line no-undef
export function prepareRendererToRender(container: Container): void {
// noop
Expand Down
13 changes: 13 additions & 0 deletions packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,19 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
localRequestAnimationFrame(time => callback(time));
});
}

export function shouldSuspendCommit(type: Type, props: Props): boolean {
return false;
}

export function startSuspendingCommit(): void {}

export function suspendInstance(type: Type, props: Props): void {}

export function waitForCommitToBeReady(): null {
return null;
}

// -------------------
// Resources
// -------------------
Expand Down
12 changes: 12 additions & 0 deletions packages/react-native-renderer/src/ReactFabricHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,18 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}

export function shouldSuspendCommit(type: Type, props: Props): boolean {
return false;
}

export function startSuspendingCommit(): void {}

export function suspendInstance(type: Type, props: Props): void {}

export function waitForCommitToBeReady(): null {
return null;
}

export function prepareRendererToRender(container: Container): void {
// noop
}
Expand Down
12 changes: 12 additions & 0 deletions packages/react-native-renderer/src/ReactNativeHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,18 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}

export function shouldSuspendCommit(type: Type, props: Props): boolean {
return false;
}

export function startSuspendingCommit(): void {}

export function suspendInstance(type: Type, props: Props): void {}

export function waitForCommitToBeReady(): null {
return null;
}

export function prepareRendererToRender(container: Container): void {
// noop
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react-noop-renderer/src/ReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export const {
createLegacyRoot,
getChildrenAsJSX,
getPendingChildrenAsJSX,
getSuspenseyThingStatus,
resolveSuspenseyThing,
resetSuspenseyThingCache,
createPortal,
render,
renderLegacySyncRoot,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-noop-renderer/src/ReactNoopPersistent.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export const {
createLegacyRoot,
getChildrenAsJSX,
getPendingChildrenAsJSX,
getSuspenseyThingStatus,
resolveSuspenseyThing,
resetSuspenseyThingCache,
createPortal,
render,
renderLegacySyncRoot,
Expand Down
179 changes: 179 additions & 0 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type Props = {
left?: null | number,
right?: null | number,
top?: null | number,
src?: string,
...
};
type Instance = {
Expand All @@ -72,6 +73,11 @@ type CreateRootOptions = {
...
};

type SuspenseyCommitSubscription = {
pendingCount: number,
commit: null | (() => void),
};

const NO_CONTEXT = {};
const UPPERCASE_CONTEXT = {};
const UPDATE_SIGNAL = {};
Expand Down Expand Up @@ -238,6 +244,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
hidden: !!newProps.hidden,
context: instance.context,
};

if (type === 'suspensey-thing' && typeof newProps.src === 'string') {
clone.src = newProps.src;
}

Object.defineProperty(clone, 'id', {
value: clone.id,
enumerable: false,
Expand Down Expand Up @@ -271,6 +282,78 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return hostContext === UPPERCASE_CONTEXT ? rawText.toUpperCase() : rawText;
}

type SuspenseyThingRecord = {
status: 'pending' | 'fulfilled',
subscriptions: Array<SuspenseyCommitSubscription> | null,
};

let suspenseyThingCache: Map<
SuspenseyThingRecord,
'pending' | 'fulfilled',
> | null = null;

// Represents a subscription for all the suspensey things that block a
// particular commit. Once they've all loaded, the commit phase can proceed.
let suspenseyCommitSubscription: SuspenseyCommitSubscription | null = null;

function startSuspendingCommit(): void {
// This is where we might suspend on things that aren't associated with a
// particular node, like document.fonts.ready.
suspenseyCommitSubscription = null;
}

function suspendInstance(type: string, props: Props): void {
const src = props.src;
if (type === 'suspensey-thing' && typeof src === 'string') {
// Attach a listener to the suspensey thing and create a subscription
// object that uses reference counting to track when all the suspensey
// things have loaded.
const record = suspenseyThingCache.get(src);
if (record === undefined) {
throw new Error('Could not find record for key.');
}
if (record.status === 'pending') {
if (suspenseyCommitSubscription === null) {
suspenseyCommitSubscription = {
pendingCount: 1,
commit: null,
};
} else {
suspenseyCommitSubscription.pendingCount++;
}
}
// Stash the subscription on the record. In `resolveSuspenseyThing`,
// we'll use this fire the commit once all the things have loaded.
if (record.subscriptions === null) {
record.subscriptions = [];
}
record.subscriptions.push(suspenseyCommitSubscription);
} else {
throw new Error(
'Did not expect this host component to be visited when suspending ' +
'the commit. Did you check the SuspendCommit flag?',
);
}
return suspenseyCommitSubscription;
}

function waitForCommitToBeReady():
| ((commit: () => mixed) => () => void)
| null {
const subscription = suspenseyCommitSubscription;
if (subscription !== null) {
suspenseyCommitSubscription = null;
return (commit: () => void) => {
subscription.commit = commit;
const cancelCommit = () => {
subscription.commit = null;
};
return cancelCommit;
};
}
return null;
}

const sharedHostConfig = {
supportsSingletons: false,

Expand Down Expand Up @@ -322,6 +405,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
hidden: !!props.hidden,
context: hostContext,
};

if (type === 'suspensey-thing' && typeof props.src === 'string') {
inst.src = props.src;
}

// Hide from unit tests
Object.defineProperty(inst, 'id', {value: inst.id, enumerable: false});
Object.defineProperty(inst, 'parent', {
Expand Down Expand Up @@ -480,6 +568,45 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
const endTime = Scheduler.unstable_now();
callback(endTime);
},

shouldSuspendCommit(type: string, props: Props): boolean {
if (type === 'suspensey-thing' && typeof props.src === 'string') {
if (suspenseyThingCache === null) {
suspenseyThingCache = new Map();
}
const record = suspenseyThingCache.get(props.src);
if (record === undefined) {
const newRecord: SuspenseyThingRecord = {
status: 'pending',
subscriptions: null,
};
suspenseyThingCache.set(props.src, newRecord);
const onLoadStart = props.onLoadStart;
if (typeof onLoadStart === 'function') {
onLoadStart();
}
return props.src;
} else {
if (record.status === 'pending') {
// The resource was already requested, but it hasn't finished
// loading yet.
return true;
} else {
// The resource has already loaded. If the renderer is confident that
// the resource will still be cached by the time the render commits,
// then it can return false, like we do here.
return false;
}
}
}
// Don't need to suspend.
return false;
},

startSuspendingCommit,
suspendInstance,
waitForCommitToBeReady,

prepareRendererToRender() {},
resetRendererAfterRender() {},
};
Expand Down Expand Up @@ -508,6 +635,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
hostUpdateCounter++;
instance.prop = newProps.prop;
instance.hidden = !!newProps.hidden;

if (type === 'suspensey-thing' && typeof newProps.src === 'string') {
instance.src = newProps.src;
}

if (shouldSetTextContent(type, newProps)) {
if (__DEV__) {
checkPropStringCoercion(newProps.children, 'children');
Expand Down Expand Up @@ -689,6 +821,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
if (instance.hidden) {
props.hidden = true;
}
if (instance.src) {
props.src = instance.src;
}
if (children !== null) {
props.children = children;
}
Expand Down Expand Up @@ -915,6 +1050,50 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return getPendingChildrenAsJSX(container);
},

getSuspenseyThingStatus(src): string | null {
if (suspenseyThingCache === null) {
return null;
} else {
const record = suspenseyThingCache.get(src);
return record === undefined ? null : record.status;
}
},

resolveSuspenseyThing(key: string): void {
if (suspenseyThingCache === null) {
suspenseyThingCache = new Map();
}
const record = suspenseyThingCache.get(key);
if (record === undefined) {
const newRecord: SuspenseyThingRecord = {
status: 'fulfilled',
subscriptions: null,
};
suspenseyThingCache.set(key, newRecord);
} else {
if (record.status === 'pending') {
record.status = 'fulfilled';
const subscriptions = record.subscriptions;
if (subscriptions !== null) {
record.subscriptions = null;
for (let i = 0; i < subscriptions.length; i++) {
const subscription = subscriptions[i];
subscription.pendingCount--;
if (subscription.pendingCount === 0) {
const commit = subscription.commit;
subscription.commit = null;
commit();
}
}
}
}
}
},

resetSuspenseyThingCache() {
suspenseyThingCache = null;
},

createPortal(
children: ReactNodeList,
container: Container,
Expand Down
8 changes: 4 additions & 4 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -2298,7 +2298,7 @@ function updateSuspenseComponent(
const newOffscreenQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
wakeables: null,
retryQueue: null,
};
primaryChildFragment.updateQueue = newOffscreenQueue;
} else {
Expand Down Expand Up @@ -2399,7 +2399,7 @@ function updateSuspenseComponent(
const newOffscreenQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
wakeables: null,
retryQueue: null,
};
primaryChildFragment.updateQueue = newOffscreenQueue;
} else if (offscreenQueue === currentOffscreenQueue) {
Expand All @@ -2408,9 +2408,9 @@ function updateSuspenseComponent(
const newOffscreenQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
wakeables:
retryQueue:
currentOffscreenQueue !== null
? currentOffscreenQueue.wakeables
? currentOffscreenQueue.retryQueue
: null,
};
primaryChildFragment.updateQueue = newOffscreenQueue;
Expand Down
Loading