8000 perf(core): detect existing signal dependency without checking all pr… by JoostK · Pull Request #67506 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content
Open
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
30 changes: 6 additions & 24 deletions packages/core/primitives/signals/src/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const REACTIVE_NODE: ReactiveNode = {
interface ReactiveLink {
producer: ReactiveNode;
consumer: ReactiveNode;
knownValidAtEpoch: Version;
lastReadVersion: number;
prevConsumer: ReactiveLink | undefined;
nextConsumer: ReactiveLink | undefined;
Expand Down Expand Up @@ -239,6 +240,7 @@ export function producerAccessed(node: ReactiveNode): void {
// If the next producer is the same as the one we're trying to add, we can just update the
// last read version, update the tail of the producers list of this rerun, and return.
activeConsumer.producersTail = nextProducerLink;
nextProducerLink.knownValidAtEpoch = epoch;
nextProducerLink.lastReadVersion = node.version;
return;
}
Expand All @@ -247,26 +249,25 @@ export function producerAccessed(node: ReactiveNode): void {
const prevConsumerLink = node.consumersTail;

// If the producer we're accessing already has a link to this consumer, we can skip adding a new
// link. This can short circuit the creation of a new link in the case where the consumer reads alternating ReeactiveNodes
// link. This can short circuit the creation of a new link in the case where the consumer reads alternating ReactiveNodes
if (
prevConsumerLink !== undefined &&
prevConsumerLink.consumer === activeConsumer &&
// However, we have to make sure that the link we've discovered isn't from a node that is incrementally rebuilding its producer list
(!isRecomputing || isValidLink(prevConsumerLink, activeConsumer))
(!isRecomputing || prevConsumerLink.knownValidAtEpoch === epoch)
) {
// If we found an existing link to the consumer we can just return.
return;
}

// If we got here, it means that we need to create a new link between the producer and the consumer.
const isLive = consumerIsLive(activeConsumer);
const newLink = {
const newLink: ReactiveLink = {
producer: node,
consumer: activeConsumer,
// instead of eagerly destroying the previous link, we delay until we've finished recomputing
// the producers list, so that we can destroy all of the old links at once.
nextProducer: nextProducerLink,
prevConsumer: prevConsumerLink,
knownValidAtEpoch: epoch,
lastReadVersion: node.version,
nextConsumer: undefined,
};
Expand Down Expand Up @@ -558,22 +559,3 @@ export function setPostProducerCreatedFn(fn: ReactiveHookFn | null): ReactiveHoo
postProducerCreatedFn = fn;
return prev;
}

// While a ReactiveNode is recomputing, it may not have destroyed previous links
// This allows us to check if a given link will be destroyed by a reactivenode if it were to finish running immediately without accesing any more producers
function isValidLink(checkLink: ReactiveLink, consumer: ReactiveNode): boolean {
const producersTail = consumer.producersTail;
if (producersTail !== undefined) {
let link = consumer.producers!;
do {
if (link === checkLink) {
return true;
}
if (link === producersTail) {
break;
}
link = link.nextProducer!;
} while (link !== undefined);
}
return false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,6 @@
"isTemplateNode",
"isType",
"isTypeProvider",
"isValidLink",
"isValueProvider",
"lastNodeWasCreated",
"lastSelectedElementIdx",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,6 @@
"isTemplateNode",
"isType",
"isTypeProvider",
"isValidLink",
"isValidatorFn",
"isValueProvider",
"iterateListLike",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,6 @@
"isTemplateNode",
"isType",
"isTypeProvider",
"isValidLink",
"isValidatorFn",
"isValueProvider",
"isWritableSignal",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,6 @@
"isType",
"isTypeProvider",
"isUrlTree",
"isValidLink",
"isValueProvider",
"isWrappedDefaultExport",
"iterator",
Expand Down
47 changes: 47 additions & 0 deletions packages/core/test/signals/computed_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
defaultEquals,
setPostProducerCreatedFn,
} from '../../primitives/signals';
import {flushEffects, testingEffect} from './effect_util';

describe('computed', () => {
it('should create computed', () => {
Expand Down Expand Up @@ -333,4 +334,50 @@ describe('computed', () => {
expect(producerKindsCreated).toEqual(['signal', 'computed']);
setPostProducerCreatedFn(prev);
});

it('should keep links alive in a dynamic graph', () => {
// This test verifies that reactive links between producer <> consumers are correctly maintained in a dynamic graph.
const decoy = signal(0);
const dynamic = signal(0);
const trigger = signal(false);

let executed = 0;
testingEffect(() => {
// Let the initial execution of the effect evaluate the decoy signal, incurring a consumer edge in the graph.
if (executed == 0) {
decoy();
}

// Evaluate a second signal; in the first execution this is the second consumer while it will be the first
// consumer in subsequent executions. The dynamic nature of this consumer means that its reactive link from the
// initial execution is not being reused in the second execution, as it is masked by the presence of the by-then
// stale link of `decoy`. Since the decoy is set to be unlinked, so will its followers as a mismatch in
// consumer ordering cause the entire chain of consumers to become invalid.
dynamic();

// Evaluate another signal last such that `dynamic` is not at the tail end of the effect's producer links, as that
// would also allow the consumer link of `dynamic` to be found and reused.
trigger();

executed++;
});
flushEffects();
expect(executed).toEqual(1);

// Initiate a change through the trigger signal, causing the removal of `decoy` to be noticed without touching the
// value of `dynamic`.
trigger.set(true);
flushEffects();
expect(executed).toEqual(2);

// Verify that updates to the decoy no longer cause the effect to run.
decoy.update((v) => v + 1);
flushEffects();
expect(executed).toEqual(2);

// Also verify that updates of the dynamic consumer are still tracked, causing the effect to rerun.
dynamic.update((v) => v + 1);
flushEffects();
expect(executed).toEqual(3);
});
});
0