Commit Graph
15114 Commits
Author SHA1 Message Date
Andrew ClarkandGitHub 76da3fb979 Add API to force Scheduler to yield for macrotask (#25044)
We need a way to yield control of the main thread and schedule a
continuation in a separate macrotask. (This is related to some Suspense
optimizations we have planned.)

Our solution needs account for how Scheduler is implemented. Scheduler
tasks are not 1:1 with real browser macrotasks — many Scheduler "tasks"
can be executed within a single browser task. If a Scheduler task yields
control and posts a continuation, but there's still time left in the
frame, Scheduler will execute the continuation immediately
(synchronously) without yielding control back to the main thread. That's
not what we want — we want to schedule a new macrotask regardless of
where we are in the browser's render cycle.

There are several ways we could approach this. What I ended up doing was
adding a new Scheduler method `unstable_requestYield`. (It's similar to
the existing `unstable_requestPaint` that we use to yield at the end of
the frame.)

It works by setting the internal start time of the current work loop to
a large negative number, so that when the `shouldYield` call computes
how much time has elapsed, it's guaranteed to exceed the deadline. The
advantage of doing it this way is that there are no additional checks in
the normal hot path of the work loop.

The existing layering between Scheduler and React DOM is not ideal. None
of the APIs are public, so despite the fact that Scheduler is a separate
package, I consider that a private implementation detail, and think of
them as part of the same unit.

So for now, though, I think it makes sense to implement this macrotask
logic directly inside of Scheduler instead of layering it on top.

The rough eventual plan for Scheduler is turn it into a `postTask`
prollyfill. Because `postTask` does not yet have an equivalent for
`shouldYield`, we would split that out into its own layer, perhaps
directly inside the reconciler. In that world, the macrotask logic I've
added in this commit would likely live in that same layer. When the
native `postTask` is available, we may not even need any additional
logic because it uses actual browser tasks.
2022-08-04 21:20:15 -04:00
Andrew ClarkandGitHub 77af359b24 Clean up unused Deletion flag (#24992)
This was replaced by the ChildDeletion flag when we refactored the 
commit phase.
2022-08-03 17:03:53 -04:00
Luna RuanandGitHub e6e508f09b [Transition Tracing] Add Offscreen Test (#25035)
Add a test to make sure offscreen trees don't stop transitions from completing.
2022-08-03 13:24:41 -04:00
Sebastian MarkbågeandGitHub 728f4ee69c Remove rootContainerInstance from unnecessary places (#25024)
We only really use this for the create APIs since the DOM requires it.

We could probably use the Host Context for this instead since they're
updated at the same time and the namespace is related to this concept.
2022-08-01 23:30:04 -04:00
Konpaku YoumuandGitHub 0af1683980 docs(react-devtools-inline): react-dom@experimental instead of react-dom@experimenal (#25001) 2022-07-30 10:57:13 +02:00
Andrew ClarkandGitHub 1829cd5d5f Mount/unmount passive effects when Offscreen visibility changes (#24977)
* Remove unnecessary try-catch from passive deletion

The individual unmount calls are already wrapped in a catch block, so
this outer one serves no purpose.

* Extract passive unmount effects to separate functions
    
I'm about to add a "disconnect passive effects" function that will share
much of the same code as commitPassiveUnmountOnFiber. To minimize the
duplicated code, I've extracted the shared parts into separate
functions, similar to what I did for commitLayoutEffectOnFiber and
reappearLayoutEffects.

This may not save much on code size because Closure will likely inline
some of it, anyway, but it makes it harder for the two paths to
accidentally diverge.

* Mount/unmount passive effects on hide/show

This changes the behavior of Offscreen so that passive effects are
unmounted when the tree is hidden, and re-mounted when the tree is
revealed again. This is already how layout effects worked.

In the future we will likely add an option or heuristic to only unmount
the effects of a hidden tree after a delay. That way if the tree quickly
switches back to visible, we can skip toggling the effects entirely.

This change does not apply to suspended trees, which happen to use
the Offscreen fiber type as an implementation detail. Passive effects
remain mounted while the tree is suspended, for the reason described
above — it's likely that the suspended tree will resolve and switch
back to visible within a short time span.

At a high level, what this capability enables is a feature we refer to
as "resuable state". The real value proposition here isn't so much the
behavior of effects — it's that you can switch back to a previously
rendered tree without losing the state of the UI.

* Add more coverage for nested Offscreen cases
2022-07-29 19:34:28 -04:00
Andrew ClarkandGitHub 4b9f217d73 Don't fire passive effects during initial mount of a hidden Offscreen tree (#24967)
* Change OffscreenInstance isHidden to bitmask

The isHidden field of OffscreenInstance is a boolean that represents
whether the tree is currently hidden. To implement resuable effects, we
need to also track whether the passive effects are currently connected.
So I've changed this field to a bitmask.

No other behavior has changed in this commit. I'll update the effects
behavior in the following steps.

* Extract passive mount effects to separate functions

I'm about to add a "reappear passive effects" function that will share
much of the same code as commitPassiveMountEffectOnFiber. To minimize
the duplicated code, I've extracted the shared parts into separate
functions, similar to what I did for commitLayoutEffectOnFiber and
reappearLayoutEffects.

This may not save much on code size because Closure will likely inline
some of it, anyway, but it makes it harder for the two paths to
accidentally diverge.

* Don't mount passive effects in a new hidden tree

This changes the behavior of Offscreen so that passive effects do not
fire when prerendering a brand new tree. Previously, Offscreen did not
affect passive effects at all — only layout effects, which mount or
unmount whenever the visibility of the tree changes.

When hiding an already visible tree, the behavior of passive effects is
unchanged, for now; unlike layout effects, the passive effects will not
get unmounted. Pre-rendered updates to a hidden tree in this state will
also fire normally. This is only temporary, though — the plan is for
passive effects to act more like layout effects, and unmount them when
the tree is hidden. Perhaps after a delay so that if the visibility
toggles quickly back and forth, the effects don't need to remount. I'll
implement this separately.

* "Atomic" passive commit effects must always fire

There are a few cases where commit phase logic always needs to fire even
inside a hidden tree. In general, we should try to design algorithms
that don't depend on a commit effect running during prerendering, but
there's at least one case where I think it makes sense.

The experimental Cache component uses reference counting to keep track
of the lifetime of a cache instance. This allows us to expose an
AbortSignal object that data frameworks can use to cancel aborted
requests. These cache objects are considered alive even inside a
prerendered tree.

To implement this I added an "atomic" passive effect traversal that runs
even when a tree is hidden. (As a follow up, we should add a special
subtree flag so that we can skip over nodes that don't have them. There
are a number of similar subtree flag optimizations that we have planned,
so I'll leave them for a later refactor.)

The only other feature that currently depends on this behavior is
Transition Tracing. I did not add a test for this because Transition
Tracing is still in development and doesn't yet work with Offscreen.
2022-07-29 19:22:57 -04:00
Andrew ClarkandGitHub 1a1b62f506 Implement Offscreen in Fizz (#24988)
During server rendering, a visible Offscreen subtree acts exactly like a
fragment: a pure indirection.

A hidden Offscreen subtree is not server rendered at all. It's ignored
during hydration, too. Prerendering happens only on the client. We
considered prerendering hidden trees on the server, too, but our
conclusion is that it's a waste of bytes and server computation. We
can't think of any compelling cases where it's the right trade off. (If
we ever change our mind, though, the way we'll likely model it is to
treat it as if it's a Suspense boundary with an empty fallback.)
2022-07-26 00:35:51 -04:00
Mengdi ChenandGitHub 17126f37f5 [Devtools] add logs for profiler tab switch & settings change (#24966)
* [devtools] add logs for profiler tab switch & settings change

* prettier

* remove unnecessary console.log

* better naming: logEvent -> loggerEvent

* use same object for event and metadata
2022-07-25 11:53:38 -04:00
Sebastian SilbermannandGitHub ac4a40c84a devtools: Remove ForwardRef/Memo from display name if displayName is set (#21952)
* feat(devtools): Remove ForwardRef/Memo from display name if `displayName` is set

* Avoid potentially wasting work by inlining `functionName`
2022-07-25 09:02:12 +02:00
davidrenneandGitHub 2a612858ab Bug fix for <App /> vs. <Counter /> (#24972) 2022-07-22 11:24:49 -04:00
Sebastian SilbermannandGitHub f5a7d7c542 test: Throw custom error instead of relying on runtime error (#24946) 2022-07-21 15:46:57 -04:00
Sebastian SilbermannandGitHub f7bcfabe93 test(react-debug-tools): Improve coverage of currentDispatcher.current setter (#24945) 2022-07-21 15:46:43 -04:00
Sebastian SilbermannandGitHub ce3e781639 Add 4.5.0 release to eslint rules CHANGELOG (#24853)
Eye-balled from https://app.renovatebot.com/package-diff?name=eslint-plugin-react-hooks&from=4.4.0&to=4.5.0#d2h-042857 which changes were included.
2022-07-21 15:46:10 -04:00
Mengdi ChenandGitHub b896bdff44 [devtools] log more events + metadata (#24951)
* [devtools] log more events + metadata

* better typing

* consistent string type
2022-07-19 15:22:09 -04:00
Andrew Clark 9115768ae1 Reused components commit with timing as new ones
When an Offscreen tree goes from hidden -> visible, the tree may include
both reused components that were unmounted when the tree was hidden, and
also brand new components that didn't exist in the hidden tree.

Currently when this happens, we commit all the reused components'
effects first, before committing the new ones, using two separate
traversals of the tree.

Instead, we should fire all the effects with the same timing as if it
were a completely new tree. See the test I wrote for an example.

This is also more efficient because we only need to traverse the
tree once.
2022-07-19 14:11:33 -04:00
Andrew Clark 0139cbcf42 Extract layout effects to separate functions
There's a lot of duplicated code between commitLayoutEffectOnFiber and the
"reappear layout effects" path that happens when an Offscreen tree goes from
hidden back to visible. I'm going to refactor these to share more of the same
code. As a first step, this extracts the shared parts into separate functions.

This may not save much on code size because Closure will likely inline some of
it, anyway, but it makes it harder for the two paths to accidentally diverge.
2022-07-19 14:11:32 -04:00
Andrew Clark 94b4abea62 Use recursion to traverse during "reappear layout" phase
This converts the "reappear layout" phase to iterate over its effects
recursively instead of iteratively. This makes it easier to track
contextual information, like whether a fiber is inside a hidden tree.

We already made this change for several other phases, like mutation and
layout mount. See 8a962b6 for more context.
2022-07-19 14:11:32 -04:00
Andrew Clark 142ee88b86 Use recursion to traverse during "disappear layout" phase
This converts the "disappear layout" phase to iterate over its effects
recursively instead of iteratively. This makes it easier to track
contextual information, like whether a fiber is inside a hidden tree.

We already made this change for several other phases, like mutation and
layout mount. See 8a962b6 for more context.
2022-07-19 14:11:32 -04:00
Mengdi ChenandGitHub 32e571cbde [DevTools] add simple usage events for internal logging (#24888)
* [DevTools] add simple events for internal logging

* fix lint

* fix lint

* better event name

* fix flow

* better way to fix flow

* combine 'select-element'

* use same event name for selecting element by inspecting
2022-07-18 15:52:53 -04:00
Luna RuanandGitHub c6e539e7bf add transistion callbacks to hydrateRoot (#24937)
This PR adds transition callbacks to hydrateRoot.
2022-07-18 10:22:47 -07:00
Andrew ClarkandGitHub 8d5c1d83a0 Use recursion to traverse during passive unmount phase (#24918)
This converts the layout phase to iterate over its effects recursively
instead of iteratively. This makes it easier to track contextual
information, like whether a fiber is inside a hidden tree.

We already made this change for several other phases, like mutation and
layout mount. See 8a962b6 for more context.
2022-07-14 11:08:08 -04:00
Mengdi ChenandGitHub 71cef664e5 React DevTools 4.24.7 -> 4.25.0 (#24919) 2022-07-13 15:57:50 -04:00
Luna RuanandGitHub cc9197152b [Transition Tracing] Rename transitionCallbacks to unstable_transitionCallbacks (#24920)
Renaming transitionCallbacks to unstable_transitionCallbacks as per convention
2022-07-13 15:27:12 -04:00
Mengdi ChenandGitHub 690e4d4f93 [DevTools] enable enableProfilerComponentTree flag for all builds (#24921) 2022-07-13 15:21:32 -04:00
Andrew Clark 761bbbefb2 Use recursion to traverse during passive mount phase
This converts the layout phase to iterate over its effects
recursively instead of iteratively. This makes it easier to track 
contextual information, like whether a fiber is inside a hidden tree.

We already made this change for the mutation phase. See 8a962b6 for
more context.
2022-07-12 16:06:53 -04:00
Andrew Clark e952655046 Wrap try-catch directly around each user function
(This is the same as b459179, but for the passive mount phase rather than the
mutation phase.)

This moves the try-catch from around each fiber's passive mount phase to
direclty around each user function (effect function, callback, etc).

We already do this when unmounting because if one unmount function
errors, we still need to call all the others so they can clean up
their resources.

Previously we didn't bother to do this for anything but unmount,
because if a mount effect throws, we're going to delete that whole
tree anyway.

But now that we're switching from an iterative loop to a recursive one,
we don't want every call frame on the stack to have a try-catch, since
the error handling requires additional memory.

Wrapping every user function is a bit tedious, but it's better
for performance. Many of them already had try blocks around
them already.
2022-07-12 16:06:53 -04:00
Andrew Clark f9ca8d07b8 Move flag check into each switch case
The fiber tag is more specific than the effect flag, so we should always
refine the type of work first, to minimize redundant checks.
2022-07-12 16:06:53 -04:00
Luna RuanandGitHub 5fba299764 [Transition Tracing][Code Cleanup] Delete Marker Name Change Tests (#24908)
We decided that changing the Tracing Marker name wasn't allowed and we would error, so this PR deletes all the tests that tested name change behavior
2022-07-12 12:59:58 -07:00
Luna RuanandGitHub b2dbcbcecd [Transition Tracing] Code Cleanup (#24880)
This PR cleans up some of the transition tracing code by:
* Looping through marker transitions only when we process the markerComplete callback (rather than in the commit phase) so we block for less time during commit.
* Renaming `PendingSuspenseBoundaries` to `pendingBoundaries`
* Cleaning up the callback functions
2022-07-12 12:48:04 -07:00
Luna RuanandGitHub c6f4adf713 [Transition Tracing] onMarkerProgress (#24861)
This PR adds support for `onMarkerProgress` (`onTransitionProgress(transitionName: string, markerName: string, startTime: number, currentTime: number, pending: Array<{name: null | string}>)`)

We call this callback when:
    * When **a child suspense boundary of the marker commits in a fallback state**. Only the suspense boundaries that are triggered and commit in a fallback state when the transition first occurs (and all subsequent suspense boundaries in the initial suspense boundary's subtree) are considered a part of the transition
    * **A child suspense boundary of the marker resolves**
   
When we call `onMarkerProgress`, we call the function with a `pending` array. This array contains the names of the transition's suspense boundaries that are still in a fallback state
2022-07-12 11:59:54 -07:00
Andrew Clark 2c540031e6 Use recursion to traverse during layout phase
This converts the layout phase to iterate over its effects
recursively instead of iteratively. This makes it easier to track 
contextual information, like whether a fiber is inside a hidden tree.

We already made this change for the mutation phase. See 8a962b6 for
more context.
2022-07-12 13:27:22 -04:00
Andrew Clark 058e73ae92 Wrap try-catch directly around each user function
(This is the same as b459179, but for the layout phase rather than the
mutation phase.)

This moves the try-catch from around each fiber's layout phase to
direclty around each user function (effect function, callback, etc).

We already do this when unmounting because if one unmount function
errors, we still need to call all the others so they can clean up
their resources.

Previously we didn't bother to do this for anything but unmount,
because if a mount effect throws, we're going to delete that whole
tree anyway.

But now that we're switching from an iterative loop to a recursive one,
we don't want every call frame on the stack to have a try-catch, since
the error handling requires additional memory.

Wrapping every user function is a bit tedious, but it's better
for performance. Many of them already had try blocks around
them already.
2022-07-12 13:27:22 -04:00
Andrew Clark dd402d4d01 Move flag check into each switch case
The fiber tag is more specific than the effect flag, so we should always
refine the type of work first, to minimize redundant checks.
2022-07-12 13:27:22 -04:00
Andrew Clark 736818287b Move ref commit effects inside switch statement
Only certain fiber types can have refs attached to them, so this moves the
Ref effect logic out of the common path and into the corresponding branch
of the layout phase's switch statement.

The types of fibers this affects are host components and class components.
Function components are not affected because they can only have a ref via
useImperativeHandle, which has a different implementation. The experimental
Scope type attaches its refs in the mutation phase, not the layout phase.
2022-07-12 13:27:22 -04:00
Luna RuanandGitHub 1e77cecd7d [Transition Tracing] Don't call transition callbacks if no transition name specified (#24887)
This PR checks to see if `transition.name` is defined before adding the transition so we avoid doing unnecessary work for transitions without a transition name
2022-07-11 18:00:49 -04:00
Luna RuanandGitHub b6d3bb5883 [Transition Tracing] Tracing Marker Name Change in Update Warning (#24873)
We should only support Tracing Marker's name field during component mount. This PR adds a warning if the Tracing Marker's name changes during an update.
2022-07-08 12:31:44 -04:00
Luna RuanandGitHub e727ffb26c [Transition Tracing] Add onTransitionProgress Callback (#24833)
This PR adds support for `onTransitionProgress` (`onTransitionProgress(transitionName: string, startTime: number, currentTime: number, pending: Array<{name: null | string}>)`)

We call this callback when:
    * When **a child suspense boundary of the transition commits in a fallback state**. Only the suspense boundaries that are triggered and commit in a fallback state when the transition first occurs (and all subsequent suspense boundaries in the initial suspense boundary's subtree) are considered a part of the transition
    * **A child suspense boundary of the transition resolves**
   
When we call `onTransitionProgress`, we call the function with a `pending` array. This array contains the names of the transition's suspense boundaries that are still in a fallback state
2022-07-08 12:13:29 -04:00
Andrew ClarkandGitHub 4e4d5a8528 Land forked reconciler changes (#24878)
This applies forked changes from the "new" reconciler to the "old" one.

Includes:

- 67de5e3 [FORKED] Hidden trees should capture Suspense
- 6ab05ee [FORKED] Track nearest Suspense handler on stack
- 051ac55 [FORKED] Add HiddenContext to track if subtree is hidden
2022-07-08 11:55:53 -04:00
Andrew ClarkandGitHub c8d160ddc4 Defer setState callbacks until component is visible (#24872)
A class component `setState` callback should not fire if a component is inside a
hidden Offscreen tree. Instead, it should wait until the next time the component
is made visible.
2022-07-08 11:51:40 -04:00
Andrew ClarkandGitHub 131ebc355d Delete Partial Renderer SSR implementation (#24868)
This removes the old server rendering implementation (the "Partial Renderer").
It was replaced in React 18 with a new streaming implementation (Fizz).

We hadn't removed it from the codebase yet because Facebook hadn't finished
rolling out Fizz in production; it's been behind a feature flag while we run
performance tests and migrate our internal infrastructure.

The diff to land Fizz will land imminently, and once it does, we can merge
this commit.
2022-07-07 16:57:42 -04:00
Luna RuanandGitHub bf94cba223 [DevTools][Bugfix] Fix DevTools Perf Issue When Unmounting Large React Subtrees (#24863)
We've recently had multiple reports where, if React DevTools was installed, unmounting large React subtrees would take a huge performance hit (ex. from 50ms to 7 seconds). 

Digging in more, we realized for every fiber that unmounts, we called `untrackFibers`, which calls `clearTimeout` (and does some work manipulating a set, but this wasn't the bulk of the time). We ten call `recordUnmount`, which adds the timer back. Adding and removing the timer so many times was taking upwards of 50ms per timer add/remove call, which was resulting in exorbitant amounts of time spent in DevTools deleting subtrees.

It looks like we are calling `untrackFibers` so many times to avoid a race condition with Suspense children where we unmount them twice (first a "virtual" unmount when the suspense boundary is toggled from visible to invisible, and then an actual unmount when the new children are rendered) without modifying `fiberIDMap`. We can fix this race condition by using the `untrackFibersSet` as a lock and not calling `recordUnmount` if the fiber is in the set and hasn't been processed yet. This works because the only way fibers are added in the set is via `recordUnmount` anyway.

This PR also adds a test to make sure this change doesn't regress the previous behavior.

**Before**
![image](https://user-images.githubusercontent.com/2735514/177655428-774ee306-0568-49ce-987e-b5213b613265.png)

**After**
![image](https://user-images.githubusercontent.com/2735514/177655604-a217583f-787e-438e-b6f9-18953fe32444.png)
2022-07-07 10:43:25 -04:00
Luna RuanandGitHub 3e6e8f8ac1 [Transition Tracing] Refactor Code to Remove OffscreeInstance TODOs (#24855)
Refactored code to pass flow and remove TODOs introduced in #24846
2022-07-06 09:38:28 -04:00
Luna RuanandGitHub 615d6d4144 [Transition Tracing] Change Transition Type Passed Pending Transitions (#24856)
This PR changes the type of the object we store in the pending transitions callbacks map. Previously, we were recreating the transition object that we initially created during `startTransition`. However, we can actually reuse the object instead (and it also gives us a stable way to identify a transition). This PR changes the implementation to reuse the transition object instead of creating a new one
2022-07-06 09:37:46 -04:00
Andrew ClarkandGitHub e4815dd531 Suspending inside a hidden tree should not cause fallbacks to appear (#24699)
* [FORKED] Hidden trees should capture Suspense

If something suspends inside a hidden tree, it should not affect
anything in the visible part of the UI. This means that Offscreen acts
like a Suspense boundary whenever it's in its hidden state.

* Add previous commit to forked revisions
2022-07-05 17:51:27 -04:00
Andrew ClarkandGitHub bb315afb35 Add missing null checks to OffscreenInstance code (#24846)
`stateNode` is any-typed, so when reading from `stateNode` we should always cast
it to the specific type for that type of work. I noticed a place in the commit
phase where OffscreenInstance wasn't being cast. When I added the type
assertion, it exposed some type errors where nullable values were being accessed
without first being refined.

I added the required null checks without verifying the logic of the existing
code. If the existing logic was correct, then the extra null checks won't have
any affect on the behavior, because all they do is refine from a nullable type
to a non-nullable type in places where the type was assumed to already be
non-nullable. But the result looks a bit fishy to me, so I also left behind some
TODOs to follow up and verify it's correct.
2022-07-05 11:40:44 -04:00
Luna RuanandGitHub 17447a36c1 Revert "Revert [Transition Tracing] Refactor Transition Tracing Root Code" (#24830)
* refactor root

* old

* Add comments and push actual marker instance in pushMarkerInstance

* old

* refactor pushRootMarkerInstance

* old

* fix test
2022-06-30 13:16:08 -04:00
Andrew ClarkandGitHub 28306c6d96 Revert "[Transition Tracing] Refactor Transition Tracing Root Code (#24766)" (#24829)
This reverts commit 0eb5dc6501 because it's
failing on main, likely due to conflict with something that landed before the
PR was merged. Need to rebase and fix.
2022-06-30 11:48:25 -04:00
Luna RuanandGitHub 0eb5dc6501 [Transition Tracing] Refactor Transition Tracing Root Code (#24766)
This PR refactors the transition tracing root code by reusing the tracing marker code. Namely it:
* Refactors the tracing marker code so that it takes a tracing marker instance instead of a tracing marker fiber and rename the stack to `markerInstance` instead of `tracingMarker`
* Pushes the root code onto the stack
* Moves the instantiation of `root.incompleteTransitions` to the begin phase when we are pushing the root to the stack rather than in the commit phase
2022-06-30 08:16:32 -07:00
Andrew ClarkandGitHub cb221e3f43 Track nearest Suspense handler on stack (#24585)
* [FORKED] Add HiddenContext to track if subtree is hidden

This adds a new stack cursor for tracking whether we're rendering inside
a subtree that's currently hidden.

This corresponds to the same place where we're already tracking the
"base lanes" needed to reveal a hidden subtree — that is, when going
from hidden -> visible, the base lanes are the ones that we skipped
over when we deferred the subtree. We must includes all the base lanes
and their updates in order to avoid an inconsistency with the
surrounding content that already committed.

I consolidated the base lanes logic and the hidden logic into the same
set of push/pop calls.

This is intended to replace the InvisibleParentContext that is currently
part of SuspenseContext, but I haven't done that part yet.

* Add previous commit to forked revisions

* [FORKED] Track nearest Suspense handler on stack

Instead of traversing the return path whenever something suspends to
find the nearest Suspense boundary, we can push the Suspense boundary
onto the stack before entering its subtree. This doesn't affect the
overall algorithm that much, but because we already do all the same
logic in the begin phase, we can save some redundant work by tracking
that information on the stack instead of recomputing it every time.

* Add previous commit to forked revisions
2022-06-30 10:03:29 -04:00