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

[Polyfill] FIx - Deduplicate watched signals #191

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/signal-polyfill/src/wrapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,20 @@ describe("Watcher", () => {
signal.set(1);
expect(mockGetPending).toBeCalled();
});

it("does not store duplicated watchers for the same signal", () => {
const watcher = new Signal.subtle.Watcher(() => {});
const signal = new Signal.State<number>(0);
const computed = new Signal.Computed(() => signal.get() + 1);
watcher.watch(signal);
watcher.watch(signal);
watcher.watch(computed);
watcher.watch(computed);
watcher.watch(computed);
expect(Signal.subtle.introspectSources(watcher).length).toBe(2);
expect(Signal.subtle.introspectSources(watcher)[0]).toBe(signal);
expect(Signal.subtle.introspectSources(watcher)[1]).toBe(computed);
});
});

describe("Expected class shape", () => {
Expand Down
8 changes: 7 additions & 1 deletion packages/signal-polyfill/src/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,16 @@ export namespace subtle {
this.#assertSignals(signals);

const node = this[NODE];
assertConsumerNode(node);

node.dirty = false; // Give the watcher a chance to trigger again
const prev = setActiveConsumer(node);

const producerNodeSet = new Set(node.producerNode);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems a bit expensive to reconstruct this set on each watch call. Is there any way to reduce this cost, whether by maintaining the set across several method calls, or somehow avoiding constructing a set and using the producerNode data structure in some other way instead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 - the current implementation is specifically the result of profiling / perf optimizations and the observations that arrays are much faster collections as compared to sets / maps.

Copy link
Author

@issammani issammani Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... are much faster collections as compared to sets / maps.

Makes sense. Does that generally include traversing an array though ? If so we can just filter out duplicates first instead of constructing a set. What do you think ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that generally include traversing an array though ? If so we can just filter out duplicates first instead of constructing a set. What do you think ?

Traversing isn't a problem. Filtering would be, though, as it would require creation of a new collection or shift elements in an array. More importantly, how would you eliminate duplicates without a set or sorting?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like signals.filter(s => node.producerNode.includes(s)).forEach(producerAccessed) should work ( or even just a for loop without creating a new array with the filter ). We are at O(n.m) though, since we "traverse" producerNode for each signal.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 0684e8a to use .includes instead. It's not the ideal option perf wise though. If we want better performance i think we should rethink the producerNode data structure all together. @littledan @pkozlowski-opensource what do you think ?

for (const signal of signals) {
producerAccessed(signal[NODE]);
if(!producerNodeSet.has(signal[NODE])) {
producerAccessed(signal[NODE]);
}
}
setActiveConsumer(prev);
}
Expand Down