@samitouri / QOS-React-2 / commits / ad6e6ec7bb

[Fizz] Prepare Recursive Loop for More Types (#21186)

* Split out into helper functions This is similar to the structure of beginWork in Fiber. * Split the rendering of a node from recursively rendering a node This lets us reuse render node at the root which doesn't spawn new work.

Sebastian Markbåge committed Apr 7, 2021 at 14:29 UTC ad6e6ec7bbf7c37c668db57570b296b2eff69d0a
1 file changed +225 -158
packages/react-server/src/ReactFizzServer.js
+225 -158
@@ -276,7 +276,164 @@ function fatalError(request: Request, error: mixed): void {
276 closeWithError(request.destination, error);
277 }
278
279 -function renderNode(request: Request, task: Task, node: ReactNodeList): void {
279 +function renderSuspenseBoundary(
280 + request: Request,
281 + task: Task,
282 + props: Object,
283 +): void {
284 + const parentBoundary = task.blockedBoundary;
285 + const parentSegment = task.blockedSegment;
286 +
287 + // We need to push an "empty" thing here to identify the parent suspense boundary.
288 + pushEmpty(parentSegment.chunks, request.responseState, task.assignID);
289 + task.assignID = null;
290 + // Each time we enter a suspense boundary, we split out into a new segment for
291 + // the fallback so that we can later replace that segment with the content.
292 + // This also lets us split out the main content even if it doesn't suspend,
293 + // in case it ends up generating a large subtree of content.
294 + const fallback: ReactNodeList = props.fallback;
295 + const content: ReactNodeList = props.children;
296 +
297 + const fallbackAbortSet: Set<Task> = new Set();
298 + const newBoundary = createSuspenseBoundary(request, fallbackAbortSet);
299 + const insertionIndex = parentSegment.chunks.length;
300 + // The children of the boundary segment is actually the fallback.
301 + const boundarySegment = createPendingSegment(
302 + request,
303 + insertionIndex,
304 + newBoundary,
305 + parentSegment.formatContext,
306 + );
307 + parentSegment.children.push(boundarySegment);
308 +
309 + // This segment is the actual child content. We can start rendering that immediately.
310 + const contentRootSegment = createPendingSegment(
311 + request,
312 + 0,
313 + null,
314 + parentSegment.formatContext,
315 + );
316 + // We mark the root segment as having its parent flushed. It's not really flushed but there is
317 + // no parent segment so there's nothing to wait on.
318 + contentRootSegment.parentFlushed = true;
319 +
320 + // Currently this is running synchronously. We could instead schedule this to pingedTasks.
321 + // I suspect that there might be some efficiency benefits from not creating the suspended task
322 + // and instead just using the stack if possible.
323 + // TODO: Call this directly instead of messing with saving and restoring contexts.
324 +
325 + // We can reuse the current context and task to render the content immediately without
326 + // context switching. We just need to temporarily switch which boundary and which segment
327 + // we're writing to. If something suspends, it'll spawn new suspended task with that context.
328 + task.blockedBoundary = newBoundary;
329 + task.blockedSegment = contentRootSegment;
330 + try {
331 + // We use the safe form because we don't handle suspending here. Only error handling.
332 + renderNode(request, task, content);
333 + contentRootSegment.status = COMPLETED;
334 + newBoundary.completedSegments.push(contentRootSegment);
335 + if (newBoundary.pendingTasks === 0) {
336 + // This must have been the last segment we were waiting on. This boundary is now complete.
337 + // Therefore we won't need the fallback. We early return so that we don't have to create
338 + // the fallback.
339 + return;
340 + }
341 + } catch (error) {
342 + contentRootSegment.status = ERRORED;
343 + reportError(request, error);
344 + newBoundary.forceClientRender = true;
345 + // We don't need to decrement any task numbers because we didn't spawn any new task.
346 + // We don't need to schedule any task because we know the parent has written yet.
347 + // We do need to fallthrough to create the fallback though.
348 + } finally {
349 + task.blockedBoundary = parentBoundary;
350 + task.blockedSegment = parentSegment;
351 + }
352 +
353 + // We create suspended task for the fallback because we don't want to actually task
354 + // on it yet in case we finish the main content, so we queue for later.
355 + const suspendedFallbackTask = createTask(
356 + request,
357 + fallback,
358 + parentBoundary,
359 + boundarySegment,
360 + fallbackAbortSet,
361 + newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
362 + );
363 + // TODO: This should be queued at a separate lower priority queue so that we only task
364 + // on preparing fallbacks if we don't have any more main content to task on.
365 + request.pingedTasks.push(suspendedFallbackTask);
366 +}
367 +
368 +function renderHostElement(
369 + request: Request,
370 + task: Task,
371 + type: string,
372 + props: Object,
373 +): void {
374 + const segment = task.blockedSegment;
375 + const children = pushStartInstance(
376 + segment.chunks,
377 + type,
378 + props,
379 + request.responseState,
380 + segment.formatContext,
381 + task.assignID,
382 + );
383 + // We must have assigned it already above so we don't need this anymore.
384 + task.assignID = null;
385 + const prevContext = segment.formatContext;
386 + segment.formatContext = getChildFormatContext(prevContext, type, props);
387 + // We use the non-destructive form because if something suspends, we still
388 + // need to pop back up and finish this subtree of HTML.
389 + renderNode(request, task, children);
390 + // We expect that errors will fatal the whole task and that we don't need
391 + // the correct context. Therefore this is not in a finally.
392 + segment.formatContext = prevContext;
393 + pushEndInstance(segment.chunks, type, props);
394 +}
395 +
396 +function renderFunctionComponent(
397 + request: Request,
398 + task: Task,
399 + type: (props: any) => ReactNodeList,
400 + props: any,
401 +): void {
402 + const result = type(props);
403 + // We're now successfully past this task, and we don't have to pop back to
404 + // the previous task every again, so we can use the destructive recursive form.
405 + renderNodeDestructive(request, task, result);
406 +}
407 +
408 +function renderElement(
409 + request: Request,
410 + task: Task,
411 + type: any,
412 + props: Object,
413 + node: ReactNodeList,
414 +): void {
415 + if (typeof type === 'function') {
416 + renderFunctionComponent(request, task, type, props);
417 + } else if (typeof type === 'string') {
418 + renderHostElement(request, task, type, props);
419 + } else if (type === REACT_SUSPENSE_TYPE) {
420 + renderSuspenseBoundary(request, task, props);
421 + } else {
422 + throw new Error('Not yet implemented element type.');
423 + }
424 +}
425 +
426 +// This function by it self renders a node and consumes the task by mutating it
427 +// to update the current execution state.
428 +function renderNodeDestructive(
429 + request: Request,
430 + task: Task,
431 + node: ReactNodeList,
432 +): void {
433 + // Stash the node we're working on. We'll pick up from this task in case
434 + // something suspends.
435 + task.node = node;
436 +
437 if (typeof node === 'string') {
438 pushTextInstance(
439 task.blockedSegment.chunks,
@@ -291,6 +448,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
448 if (isArray(node)) {
449 if (node.length > 0) {
450 for (let i = 0; i < node.length; i++) {
451 + // Recursively render the rest. We need to use the non-destructive form
452 + // so that we can safely pop back up and render the sibling if something
453 + // suspends.
454 renderNode(request, task, node[i]);
455 }
456 } else {
@@ -310,151 +470,71 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
470 }
471
472 if (
313 - typeof node !== 'object' ||
314 - !node ||
315 - (node: any).$$typeof !== REACT_ELEMENT_TYPE
473 + typeof node === 'object' &&
474 + node &&
475 + (node: any).$$typeof === REACT_ELEMENT_TYPE
476 ) {
317 - throw new Error('Not yet implemented node type.');
477 + const element: React$Element<any> = (node: any);
478 + const type = element.type;
479 + const props = element.props;
480 + renderElement(request, task, type, props, node);
481 + return;
482 }
319 - const element: React$Element<any> = (node: any);
320 - const type = element.type;
321 - const props = element.props;
322 - if (typeof type === 'function') {
323 - try {
324 - const result = type(props);
325 - renderNode(request, task, result);
326 - } catch (x) {
327 - if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
328 - // Something suspended, we'll need to create a new segment and resolve it later.
329 - const segment = task.blockedSegment;
330 - const insertionIndex = segment.chunks.length;
331 - const newSegment = createPendingSegment(
332 - request,
333 - insertionIndex,
334 - null,
335 - segment.formatContext,
336 - );
337 - segment.children.push(newSegment);
338 - const newTask = createTask(
339 - request,
340 - node,
341 - task.blockedBoundary,
342 - newSegment,
343 - task.abortSet,
344 - task.assignID,
345 - );
346 - // We've delegated the assignment.
347 - task.assignID = null;
348 - const ping = newTask.ping;
349 - x.then(ping, ping);
350 - } else {
351 - // We can rethrow to terminate the rest of this tree.
352 - throw x;
353 - }
354 - }
355 - } else if (typeof type === 'string') {
356 - const segment = task.blockedSegment;
357 - const children = pushStartInstance(
358 - segment.chunks,
359 - type,
360 - props,
361 - request.responseState,
362 - segment.formatContext,
363 - task.assignID,
364 - );
365 - // We must have assigned it already above so we don't need this anymore.
366 - task.assignID = null;
367 - const prevContext = segment.formatContext;
368 - segment.formatContext = getChildFormatContext(prevContext, type, props);
369 - renderNode(request, task, children);
370 - // We expect that errors will fatal the whole task and that we don't need
371 - // the correct context. Therefore this is not in a finally.
372 - segment.formatContext = prevContext;
373 - pushEndInstance(segment.chunks, type, props);
374 - } else if (type === REACT_SUSPENSE_TYPE) {
375 - const parentBoundary = task.blockedBoundary;
376 - const parentSegment = task.blockedSegment;
483
378 - // We need to push an "empty" thing here to identify the parent suspense boundary.
379 - pushEmpty(parentSegment.chunks, request.responseState, task.assignID);
380 - task.assignID = null;
381 - // Each time we enter a suspense boundary, we split out into a new segment for
382 - // the fallback so that we can later replace that segment with the content.
383 - // This also lets us split out the main content even if it doesn't suspend,
384 - // in case it ends up generating a large subtree of content.
385 - const fallback: ReactNodeList = props.fallback;
386 - const content: ReactNodeList = props.children;
387 -
388 - const fallbackAbortSet: Set<Task> = new Set();
389 - const newBoundary = createSuspenseBoundary(request, fallbackAbortSet);
390 - const insertionIndex = parentSegment.chunks.length;
391 - // The children of the boundary segment is actually the fallback.
392 - const boundarySegment = createPendingSegment(
393 - request,
394 - insertionIndex,
395 - newBoundary,
396 - parentSegment.formatContext,
397 - );
398 - parentSegment.children.push(boundarySegment);
399 -
400 - // This segment is the actual child content. We can start rendering that immediately.
401 - const contentRootSegment = createPendingSegment(
402 - request,
403 - 0,
404 - null,
405 - parentSegment.formatContext,
406 - );
407 - // We mark the root segment as having its parent flushed. It's not really flushed but there is
408 - // no parent segment so there's nothing to wait on.
409 - contentRootSegment.parentFlushed = true;
410 -
411 - // Currently this is running synchronously. We could instead schedule this to pingedTasks.
412 - // I suspect that there might be some efficiency benefits from not creating the suspended task
413 - // and instead just using the stack if possible.
414 - // TODO: Call this directly instead of messing with saving and restoring contexts.
415 -
416 - // We can reuse the current context and task to render the content immediately without
417 - // context switching. We just need to temporarily switch which boundary and which segment
418 - // we're writing to. If something suspends, it'll spawn new suspended task with that context.
419 - task.blockedBoundary = newBoundary;
420 - task.blockedSegment = contentRootSegment;
421 - try {
422 - renderNode(request, task, content);
423 - contentRootSegment.status = COMPLETED;
424 - newBoundary.completedSegments.push(contentRootSegment);
425 - if (newBoundary.pendingTasks === 0) {
426 - // This must have been the last segment we were waiting on. This boundary is now complete.
427 - // Therefore we won't need the fallback. We early return so that we don't have to create
428 - // the fallback.
429 - return;
430 - }
431 - } catch (error) {
432 - contentRootSegment.status = ERRORED;
433 - reportError(request, error);
434 - newBoundary.forceClientRender = true;
435 - // We don't need to decrement any task numbers because we didn't spawn any new task.
436 - // We don't need to schedule any task because we know the parent has written yet.
437 - // We do need to fallthrough to create the fallback though.
438 - } finally {
439 - task.blockedBoundary = parentBoundary;
440 - task.blockedSegment = parentSegment;
441 - }
484 + throw new Error('Not yet implemented node type.');
485 +}
486
443 - // We create suspended task for the fallback because we don't want to actually task
444 - // on it yet in case we finish the main content, so we queue for later.
445 - const suspendedFallbackTask = createTask(
446 - request,
447 - fallback,
448 - parentBoundary,
449 - boundarySegment,
450 - fallbackAbortSet,
451 - newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
452 - );
453 - // TODO: This should be queued at a separate lower priority queue so that we only task
454 - // on preparing fallbacks if we don't have any more main content to task on.
455 - request.pingedTasks.push(suspendedFallbackTask);
456 - } else {
457 - throw new Error('Not yet implemented element type.');
487 +function spawnNewSuspendedTask(
488 + request: Request,
489 + task: Task,
490 + x: Promise<any>,
491 +): void {
492 + // Something suspended, we'll need to create a new segment and resolve it later.
493 + const segment = task.blockedSegment;
494 + const insertionIndex = segment.chunks.length;
495 + const newSegment = createPendingSegment(
496 + request,
497 + insertionIndex,
498 + null,
499 + segment.formatContext,
500 + );
501 + segment.children.push(newSegment);
502 + const newTask = createTask(
503 + request,
504 + task.node,
505 + task.blockedBoundary,
506 + newSegment,
507 + task.abortSet,
508 + task.assignID,
509 + );
510 + // We've delegated the assignment.
511 + task.assignID = null;
512 + const ping = newTask.ping;
513 + x.then(ping, ping);
514 +}
515 +
516 +// This is a non-destructive form of rendering a node. If it suspends it spawns
517 +// a new task and restores the context of this task to what it was before.
518 +function renderNode(request: Request, task: Task, node: ReactNodeList): void {
519 + // TODO: Store segment.children.length here and reset it in case something
520 + // suspended partially through writing something.
521 +
522 + // Snapshot the current context in case something throws to interrupt the
523 + // process.
524 + const previousContext = task.blockedSegment.formatContext;
525 + try {
526 + return renderNodeDestructive(request, task, node);
527 + } catch (x) {
528 + if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
529 + spawnNewSuspendedTask(request, task, x);
530 + // Restore the context. We assume that this will be restored by the inner
531 + // functions in case nothing throws so we don't use "finally" here.
532 + task.blockedSegment.formatContext = previousContext;
533 + } else {
534 + // We assume that we don't need the correct context.
535 + // Let's terminate the rest of the tree and don't render any siblings.
536 + throw x;
537 + }
538 }
539 }
540
@@ -610,22 +690,9 @@ function retryTask(request: Request, task: Task): void {
690 return;
691 }
692 try {
613 - let node = task.node;
614 - while (
615 - typeof node === 'object' &&
616 - node !== null &&
617 - (node: any).$$typeof === REACT_ELEMENT_TYPE &&
618 - typeof node.type === 'function'
619 - ) {
620 - // Doing this here lets us reuse this same Segment if the next component
621 - // also suspends.
622 - const element: React$Element<any> = (node: any);
623 - task.node = node;
624 - // TODO: Classes and legacy context etc.
625 - node = element.type(element.props);
626 - }
627 -
628 - renderNode(request, task, node);
693 + // We call the destructive form that mutates this task. That way if something
694 + // suspends again, we can reuse the same task instead of spawning a new one.
695 + renderNodeDestructive(request, task, task.node);
696
697 task.abortSet.delete(task);
698 segment.status = COMPLETED;