@samitouri / QOS-React / commits / 6292398241

[DevTools] Simplify Context Change Tracking in Profiler (#30896)

When Context change tracking was added to support modern Context it relied on the "memoizedValue" to read the current value. This only works in React 18+ when it was added to support Lazy Context Propagation. However, the backend stored the old value the same way it used to work for legacy Context in a global map. This was unnecessary since we *also* have the old value on the previous Fiber. This removes all the costly tracking of previous values for every Fiber that uses Contexts slowing down profiling. Instead, we just compare the Contexts from The downside is that this no longer supports detecting changes due to legacy Context because it doesn't have a similar "previous" value. However, legacy Context has long been deprecated and is completely removed in 19. So I don't think it's worth supporting since you have to be on an old version *and* actually use legacy Context *and* trying to profile something that updates it. Which btw, updating legacy contexts only worked at all from 16 something when we made updates work. So it was unusual even in the slight gap where you could and before you had migrated to modern Context introduced in 16.3.

Sebastian Markbåge committed Sep 6, 2024 at 21:58 UTC 629239824116436521eaf00ddf02aa3b263de6de
2 files changed +62 -193
packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js
+1 -1
@@ -123,7 +123,7 @@ describe('Profiler change descriptions', () => {
123 expect(commitData.changeDescriptions.get(element.id))
124 .toMatchInlineSnapshot(`
125 {
126 - "context": null,
126 + "context": false,
127 "didHooksChange": false,
128 "hooks": null,
129 "isFirstMount": false,
packages/react-devtools-shared/src/backend/fiber/renderer.js
+61 -192
@@ -1530,16 +1530,6 @@ export function attach(
1530 // When a mount or update is in progress, this value tracks the root that is being operated on.
1531 let currentRootID: number = -1;
1532
1533 - function getFiberIDThrows(fiber: Fiber): number {
1534 - const fiberInstance = getFiberInstanceUnsafe(fiber);
1535 - if (fiberInstance !== null) {
1536 - return fiberInstance.id;
1537 - }
1538 - throw Error(
1539 - `Could not find ID for Fiber "${getDisplayNameForFiber(fiber) || ''}"`,
1540 - );
1541 - }
1542 -
1533 // Returns a FiberInstance if one has already been generated for the Fiber or null if one has not been generated.
1534 // Use this method while e.g. logging to avoid over-retaining Fibers.
1535 function getFiberInstanceUnsafe(fiber: Fiber): FiberInstance | null {
@@ -1613,11 +1603,8 @@ export function attach(
1603 prevFiber: Fiber | null,
1604 nextFiber: Fiber,
1605 ): ChangeDescription | null {
1616 - switch (getElementTypeForFiber(nextFiber)) {
1617 - case ElementTypeClass:
1618 - case ElementTypeFunction:
1619 - case ElementTypeMemo:
1620 - case ElementTypeForwardRef:
1606 + switch (nextFiber.tag) {
1607 + case ClassComponent:
1608 if (prevFiber === null) {
1609 return {
1610 context: null,
@@ -1628,7 +1615,7 @@ export function attach(
1615 };
1616 } else {
1617 const data: ChangeDescription = {
1631 - context: getContextChangedKeys(nextFiber),
1618 + context: getContextChanged(prevFiber, nextFiber),
1619 didHooksChange: false,
1620 isFirstMount: false,
1621 props: getChangedKeys(
@@ -1640,15 +1627,39 @@ export function attach(
1627 nextFiber.memoizedState,
1628 ),
1629 };
1643 -
1644 - // Only traverse the hooks list once, depending on what info we're returning.
1630 + return data;
1631 + }
1632 + case IncompleteFunctionComponent:
1633 + case FunctionComponent:
1634 + case IndeterminateComponent:
1635 + case ForwardRef:
1636 + case MemoComponent:
1637 + case SimpleMemoComponent:
1638 + if (prevFiber === null) {
1639 + return {
1640 + context: null,
1641 + didHooksChange: false,
1642 + isFirstMount: true,
1643 + props: null,
1644 + state: null,
1645 + };
1646 + } else {
1647 const indices = getChangedHooksIndices(
1648 prevFiber.memoizedState,
1649 nextFiber.memoizedState,
1650 );
1649 - data.hooks = indices;
1650 - data.didHooksChange = indices !== null && indices.length > 0;
1651 -
1651 + const data: ChangeDescription = {
1652 + context: getContextChanged(prevFiber, nextFiber),
1653 + didHooksChange: indices !== null && indices.length > 0,
1654 + isFirstMount: false,
1655 + props: getChangedKeys(
1656 + prevFiber.memoizedProps,
1657 + nextFiber.memoizedProps,
1658 + ),
1659 + state: null,
1660 + hooks: indices,
1661 + };
1662 + // Only traverse the hooks list once, depending on what info we're returning.
1663 return data;
1664 }
1665 default:
@@ -1656,147 +1667,33 @@ export function attach(
1667 }
1668 }
1669
1659 - function updateContextsForFiber(fiber: Fiber) {
1660 - switch (getElementTypeForFiber(fiber)) {
1661 - case ElementTypeClass:
1662 - case ElementTypeForwardRef:
1663 - case ElementTypeFunction:
1664 - case ElementTypeMemo:
1665 - if (idToContextsMap !== null) {
1666 - const id = getFiberIDThrows(fiber);
1667 - const contexts = getContextsForFiber(fiber);
1668 - if (contexts !== null) {
1669 - // $FlowFixMe[incompatible-use] found when upgrading Flow
1670 - idToContextsMap.set(id, contexts);
1671 - }
1672 - }
1673 - break;
1674 - default:
1675 - break;
1676 - }
1677 - }
1678 -
1679 - // Differentiates between a null context value and no context.
1680 - const NO_CONTEXT = {};
1681 -
1682 - function getContextsForFiber(fiber: Fiber): [Object, any] | null {
1683 - let legacyContext = NO_CONTEXT;
1684 - let modernContext = NO_CONTEXT;
1685 -
1686 - switch (getElementTypeForFiber(fiber)) {
1687 - case ElementTypeClass:
1688 - const instance = fiber.stateNode;
1689 - if (instance != null) {
1690 - if (
1691 - instance.constructor &&
1692 - instance.constructor.contextType != null
1693 - ) {
1694 - modernContext = instance.context;
1695 - } else {
1696 - legacyContext = instance.context;
1697 - if (legacyContext && Object.keys(legacyContext).length === 0) {
1698 - legacyContext = NO_CONTEXT;
1699 - }
1700 - }
1701 - }
1702 - return [legacyContext, modernContext];
1703 - case ElementTypeForwardRef:
1704 - case ElementTypeFunction:
1705 - case ElementTypeMemo:
1706 - const dependencies = fiber.dependencies;
1707 - if (dependencies && dependencies.firstContext) {
1708 - modernContext = dependencies.firstContext;
1709 - }
1710 -
1711 - return [legacyContext, modernContext];
1712 - default:
1713 - return null;
1714 - }
1715 - }
1716 -
1717 - // Record all contexts at the time profiling is started.
1718 - // Fibers only store the current context value,
1719 - // so we need to track them separately in order to determine changed keys.
1720 - function crawlToInitializeContextsMap(fiber: Fiber) {
1721 - const id = getFiberIDUnsafe(fiber);
1722 -
1723 - // Not all Fibers in the subtree have mounted yet.
1724 - // For example, Offscreen (hidden) or Suspense (suspended) subtrees won't yet be tracked.
1725 - // We can safely skip these subtrees.
1726 - if (id !== null) {
1727 - updateContextsForFiber(fiber);
1728 -
1729 - let current = fiber.child;
1730 - while (current !== null) {
1731 - crawlToInitializeContextsMap(current);
1732 - current = current.sibling;
1670 + function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
1671 + let prevContext =
1672 + prevFiber.dependencies && prevFiber.dependencies.firstContext;
1673 + let nextContext =
1674 + nextFiber.dependencies && nextFiber.dependencies.firstContext;
1675 +
1676 + while (prevContext && nextContext) {
1677 + // Note this only works for versions of React that support this key (e.v. 18+)
1678 + // For older versions, there's no good way to read the current context value after render has completed.
1679 + // This is because React maintains a stack of context values during render,
1680 + // but by the time DevTools is called, render has finished and the stack is empty.
1681 + if (prevContext.context !== nextContext.context) {
1682 + // If the order of context has changed, then the later context values might have
1683 + // changed too but the main reason it rerendered was earlier. Either an earlier
1684 + // context changed value but then we would have exited already. If we end up here
1685 + // it's because a state or props change caused the order of contexts used to change.
1686 + // So the main cause is not the contexts themselves.
1687 + return false;
1688 }
1734 - }
1735 - }
1736 -
1737 - function getContextChangedKeys(fiber: Fiber): null | boolean | Array<string> {
1738 - if (idToContextsMap !== null) {
1739 - const id = getFiberIDThrows(fiber);
1740 - // $FlowFixMe[incompatible-use] found when upgrading Flow
1741 - const prevContexts = idToContextsMap.has(id)
1742 - ? // $FlowFixMe[incompatible-use] found when upgrading Flow
1743 - idToContextsMap.get(id)
1744 - : null;
1745 - const nextContexts = getContextsForFiber(fiber);
1746 -
1747 - if (prevContexts == null || nextContexts == null) {
1748 - return null;
1689 + if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
1690 + return true;
1691 }
1692
1751 - const [prevLegacyContext, prevModernContext] = prevContexts;
1752 - const [nextLegacyContext, nextModernContext] = nextContexts;
1753 -
1754 - switch (getElementTypeForFiber(fiber)) {
1755 - case ElementTypeClass:
1756 - if (prevContexts && nextContexts) {
1757 - if (nextLegacyContext !== NO_CONTEXT) {
1758 - return getChangedKeys(prevLegacyContext, nextLegacyContext);
1759 - } else if (nextModernContext !== NO_CONTEXT) {
1760 - return prevModernContext !== nextModernContext;
1761 - }
1762 - }
1763 - break;
1764 - case ElementTypeForwardRef:
1765 - case ElementTypeFunction:
1766 - case ElementTypeMemo:
1767 - if (nextModernContext !== NO_CONTEXT) {
1768 - let prevContext = prevModernContext;
1769 - let nextContext = nextModernContext;
1770 -
1771 - while (prevContext && nextContext) {
1772 - // Note this only works for versions of React that support this key (e.v. 18+)
1773 - // For older versions, there's no good way to read the current context value after render has completed.
1774 - // This is because React maintains a stack of context values during render,
1775 - // but by the time DevTools is called, render has finished and the stack is empty.
1776 - if (prevContext.context !== nextContext.context) {
1777 - // If the order of context has changed, then the later context values might have
1778 - // changed too but the main reason it rerendered was earlier. Either an earlier
1779 - // context changed value but then we would have exited already. If we end up here
1780 - // it's because a state or props change caused the order of contexts used to change.
1781 - // So the main cause is not the contexts themselves.
1782 - return false;
1783 - }
1784 - if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
1785 - return true;
1786 - }
1787 -
1788 - prevContext = prevContext.next;
1789 - nextContext = nextContext.next;
1790 - }
1791 -
1792 - return false;
1793 - }
1794 - break;
1795 - default:
1796 - break;
1797 - }
1693 + prevContext = prevContext.next;
1694 + nextContext = nextContext.next;
1695 }
1799 - return null;
1696 + return false;
1697 }
1698
1699 function isHookThatCanScheduleUpdate(hookObject: any) {
@@ -1841,20 +1738,13 @@ export function attach(
1738
1739 const indices = [];
1740 let index = 0;
1844 - if (
1845 - next.hasOwnProperty('baseState') &&
1846 - next.hasOwnProperty('memoizedState') &&
1847 - next.hasOwnProperty('next') &&
1848 - next.hasOwnProperty('queue')
1849 - ) {
1850 - while (next !== null) {
1851 - if (didStatefulHookChange(prev, next)) {
1852 - indices.push(index);
1853 - }
1854 - next = next.next;
1855 - prev = prev.next;
1856 - index++;
1741 + while (next !== null) {
1742 + if (didStatefulHookChange(prev, next)) {
1743 + indices.push(index);
1744 }
1745 + next = next.next;
1746 + prev = prev.next;
1747 + index++;
1748 }
1749
1750 return indices;
@@ -1865,16 +1755,6 @@ export function attach(
1755 return null;
1756 }
1757
1868 - // We can't report anything meaningful for hooks changes.
1869 - if (
1870 - next.hasOwnProperty('baseState') &&
1871 - next.hasOwnProperty('memoizedState') &&
1872 - next.hasOwnProperty('next') &&
1873 - next.hasOwnProperty('queue')
1874 - ) {
1875 - return null;
1876 - }
1877 -
1758 const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
1759 const changedKeys = [];
1760 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
@@ -2998,8 +2878,6 @@ export function attach(
2878 metadata.changeDescriptions.set(id, changeDescription);
2879 }
2880 }
3001 -
3002 - updateContextsForFiber(fiber);
2881 }
2882 }
2883 }
@@ -5205,7 +5083,6 @@ export function attach(
5083
5084 let currentCommitProfilingMetadata: CommitProfilingData | null = null;
5085 let displayNamesByRootID: DisplayNamesByRootID | null = null;
5208 - let idToContextsMap: Map<number, any> | null = null;
5086 let initialTreeBaseDurationsMap: Map<number, Array<[number, number]>> | null =
5087 null;
5088 let isProfiling: boolean = false;
@@ -5352,7 +5229,6 @@ export function attach(
5229 // (e.g. when a fiber is re-rendered or when a fiber gets removed).
5230 displayNamesByRootID = new Map();
5231 initialTreeBaseDurationsMap = new Map();
5355 - idToContextsMap = new Map();
5232
5233 hook.getFiberRoots(rendererID).forEach(root => {
5234 const rootInstance = rootToFiberInstanceMap.get(root);
@@ -5369,13 +5245,6 @@ export function attach(
5245 const initialTreeBaseDurations: Array<[number, number]> = [];
5246 snapshotTreeBaseDurations(rootInstance, initialTreeBaseDurations);
5247 (initialTreeBaseDurationsMap: any).set(rootID, initialTreeBaseDurations);
5372 -
5373 - if (shouldRecordChangeDescriptions) {
5374 - // Record all contexts at the time profiling is started.
5375 - // Fibers only store the current context value,
5376 - // so we need to track them separately in order to determine changed keys.
5377 - crawlToInitializeContextsMap(root.current);
5378 - }
5248 });
5249
5250 isProfiling = true;