-
Notifications
You must be signed in to change notification settings - Fork 3k
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
perf(SafeSubscriber): avoid using Object.create
#5646
Conversation
if (isSubscription(observerOrNext)) { | ||
observerOrNext.add(this.unsubscribe.bind(this)); | ||
} | ||
context.unsubscribe = this.unsubscribe.bind(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to remove this statement without affecting any tests; this makes me a bit unsure on what this was trying to achieve and if it's safe to remove it, although I can't think of what this was supposed to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I realized after opening this that without this assignment, it becomes possible to just use context = observerOrNext
as an alternative to binding the subscriber callbacks. If that is indeed possible (again, no tests start to fail) then I think switching to use that approach makes sense.
Edit: this assignment does allow anonymous subscribers to call this.unsubscribe()
, which this PR would break. Since there's no tests for this, how intentional is that? Furthermore, I'd consider this to cause inconsistencies for anonymous objects that do have <- Actually, that appears to work, resulting in unsubscription and the anonymous' object's unsubscribe
themselves.unsubscribe
to be called. Here's a small StackBlitz sample.
It adds When the implementation moves to an |
Thanks for the additional context @cartant, that gives me exactly the information I was missing before. The I opened a PR in the Angular CLI to switch the anonymous observer to use separate callbacks instead, also avoiding this penalty: angular/angular-cli#18549. I wouldn't normally recommend using that syntax, but this occurs in a hot code path where the performance penalty starts to make a real difference. |
😩 I really want to get rid of this. But... one thing at a time. |
@cartant Do you know if we have any tests or documentation that covers the edge case outlined in the comment you linked to? I suspect we do not. Also, the promise of all of this sort of fails, as outlined by those firehose tests that I had you add last week. |
What I'm looking for is a way to make this change non-breaking.. OR a way to introduce the change as breaking with some sort of backwards compatibility package. It seems like the latter would have to be it, honestly. I'm going to guess that this change would break fewer than 0.001% of projects using RxJS, as I'm all but sure that this is undocumented. (And it's also relatively impossible to type with TypeScript). |
At this point, I'm sort of in favor of just making this breaking change. If people gripe during the beta, or even after, we can reenable it behind a flag in config.ts or something. I suspect it will not cause many, if any, issues. |
Nope. Only the comment of Paul's. I recall this coming up a couple of times in issues. Paul's comment is the simplest to find in a search and I've referred to it a few times. This was all well before my time, so I'm unaware of the history re: no tes.ts/docs.
Adding a |
In profiling sessions, the `SafeSubscriber` constructor was showing as hot function for code that creates and tear downs Observables very frequently. The usage of `Object.create` attributes to the slowness; avoiding using it completely removes any overhead of `SafeSubscriber`. The `Object.create` result was used to call anonymous subscribers in their own context, i.e. the `this` pointer in `next`/`error`/`complete` is the anonymous object itself. This commit implements an alternative to achieve the same effect; binding the subscriber functions to the observer object.
… behind a flag - Adds a flag to `import { config } from 'rxjs';` that allows users to use the undocumented feature that gives access to `unsubscribe` via the `this` context of a `next` handler passed as part of an observer object to `subscribe`. This behavior is deprecated because it has very bad performance implications on all subscriptions and is relatively unused, definitely undocumented, and certainly mostly unknown. - Adds a flag to silence console warn messages that are emitted when "bad" configuration settings are used. - Adds some documentation. - Adds tests. BREAKING CHANGE: `unsubscribe` no longer available via the `this` context of observer functions. To reenable, set `config.useDeprecatedNextContext = true` on the rxjs `config` found at `import { config } from 'rxjs';`. Note that enabling this will result in a performance penalty for all consumer subscriptions.
a1222ba
to
2acf96b
Compare
@cartant I've added the flag, as well as tests, and a little documentation about the breaking change. I've also rebased as there was a conflict in the spec file (since we just recently added more tests, and I also added more tests, no actual tests changed). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@benlesh LGTM, but I think we shouldn't provide the subscriber to plain callbacks either.
@@ -230,7 +234,7 @@ export class SafeSubscriber<T> extends Subscriber<T> { | |||
if (!this.isStopped) { | |||
const { _parentSubscriber } = this; | |||
if (this._complete) { | |||
const wrappedComplete = () => this._complete.call(this._context); | |||
const wrappedComplete = () => this._complete.call(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue: I think this wrappedComplete
business should also be conditional on config.useDeprecatedNextContext
. I don't think we should provide the subscriber to the caller - via the context - for plain callbacks either.
const wrappedComplete = config.useDeprecatedNextContext
? () => this._complete.call(this)
: this._complete;
And I think we need tests for plain callbacks, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am about to make a pass through all of this. I'm going to merge this as-is, and address the rest in another PR.
- Simplifies the call patterns in SafeSubscriber to not be quite so insane. - Note: for some reason one mocha test is passing, but still printing out the error it is catching in `expect(fn).to.throw()` in the console. Annoying, but I stepped through the RxJS code and verified the behavior was correct. Related ReactiveX#5646
- Simplifies the call patterns in SafeSubscriber to not be quite so insane. - Note: for some reason one mocha test is passing, but still printing out the error it is catching in `expect(fn).to.throw()` in the console. Annoying, but I stepped through the RxJS code and verified the behavior was correct. Related #5646
In profiling sessions, the
SafeSubscriber
constructor was showing as hotfunction for code that creates and tear downs Observables very frequently.
The usage of
Object.create
attributes to the slowness; avoiding using itcompletely removes any overhead of
SafeSubscriber
.The
Object.create
result was used to call anonymous subscribers in theirown context, i.e. the
this
pointer innext
/error
/complete
is theanonymous object itself. This commit implements an alternative to achieve
the same effect; binding the subscriber functions to the observer object.
Timings
The below timings are with NodeJS 14.8.0 in a full Angular CLI build of https://github.com/JoostK/ng9-aot-build-times/tree/extra.
Before:
After: