-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
feat: improve context.find/findByTag() and interceptor perf #4377
Conversation
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 reviewed the pull request at high level. It's nice to see that a relatively easy change can improve the performance so much.
I am not very happy about the proposed (internal) design, let's do few more iterations please to improve it.
packages/context/src/binding.ts
Outdated
@@ -148,7 +157,7 @@ type ValueGetter<T> = ( | |||
* Binding represents an entry in the `Context`. Each binding has a key and a | |||
* corresponding value getter. | |||
*/ | |||
export class Binding<T = BoundValue> { | |||
export class Binding<T = BoundValue> extends EventEmitter { |
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 concerned about the performance implications of turning every binding into an event emitter. I am expecting that consumers of these events will want to listen on Context instances, not on individual bindings.
Have you considered changing this implementation to emit the events on the Context object the binding is added to? If the binding is not added to any Context object yet, then I am arguing that the events can be silently discarded.
I am also surprised that we did not need these Binding-level events when implementing Context Observer. Does it mean that Context observers were not able to be notified about these kinds of changes before? Did we perhaps use a different mean to achieve this functionality for context observers?
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.
At this moment, the same binding instance can be added to multiple context objects. If we decide to treat each binding to be optionally owned by one context, we can emit such events on the owning context.
When we implemented ContextObserver - which is async, we choose to process bind
and unbind
events at a late cycle to work around the issue that bindings can be changed. Now with binding events, we can probably revisit the implementation to track binding.changed
events.
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.
IMO, setting Context
ref on Binding
is similar as Context
registering itself as a listener on Binding
from performance overhead perspective.
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.
At this moment, the same binding instance can be added to multiple context objects.
Somehow I thought that a Binding is already holding a reference to the single owning Context. I check the source code and see that it does not. As you wrote, it's possible to add the same binding to multiple contexts, therefore I agree with your originally proposed design, where each Binding is an EventEmitter 👍
When we implemented ContextObserver - which is async, we choose to process
bind
andunbind
events at a late cycle to work around the issue that bindings can be changed. Now with binding events, we can probably revisit the implementation to trackbinding.changed
events.
Make sense. I'd like to revisit this part sooner rather than later, to ensure consistency.
packages/context/src/context.ts
Outdated
binding: Readonly<Binding<unknown>>, | ||
context: Context, | ||
event: string, | ||
) => void; |
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.
Can you please explain what's the difference between ContextObserver
and ContextEventListener
? When should our users use which?
Please add a tsdoc comment for this interface. I think we should update the documentation to mention this new way of observing context changes.
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.
ContextEventListener
is a sync function as typical Node.js event listeners. The ContextObserver
is async handler.
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.
ContextEventListener
is a sync function as typical Node.js event listeners. TheContextObserver
is async handler.
Sure, I can tell this part from the source code. I am asking for user-centric documentation - when should our users choose ContextObserver and when to choose ContextEventListener? As framework authors, we should set clear guidance here.
packages/context/src/context.ts
Outdated
) { | ||
return this.find(filterByTag(tagFilter)); | ||
} | ||
return this._findByTagIndex(tagFilter); |
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.
While this solution works for the particular case of improving performance of controller methods when there are no interceptors configured, I am concerned it's too specialized and won't work for other scenarios. For example, if the application has 100 different interceptors bound in the context, to execute a controller method with no interceptors, we will still have to scan that array of 100 interceptors.
Also IIRC our previous conversations, we wanted to move away from findByTag
in favor of more generic filtering. In this pull request, you are reversing that direction.
Have you considered implementing a more generic solution, one that will allow consumers outside of @loopback/context
to implement their own cache and have an easy solution for invalidating it?
I think ideally, we want to:
- for each controller method, cache the actual list of interceptors to invoke
- invalidate cache entries when the controller method metadata changes or when an interceptors is bound or unbound
Maybe we can treat this idea as a long-term goal and implement tag indexing as a short-term performance improvement.
Thoughts?
/cc @strongloop/loopback-maintainers
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 have added a few more commits to fully leverage BindingTagFilter
whenever it is possible - even for ContextView
.
d040e6b
to
213642c
Compare
@bajtos PTAL. |
213642c
to
ab3fe0f
Compare
packages/context/src/__tests__/acceptance/interceptor.acceptance.ts
Outdated
Show resolved
Hide resolved
packages/context/src/__tests__/acceptance/interceptor.acceptance.ts
Outdated
Show resolved
Hide resolved
ab3fe0f
to
5fe3852
Compare
This pull request is introducing several changes: it introduces new Binding-level events, adds I agree it's practical to see all changes in one place to understand how the high-level intentions map to underlying implementation details, and also to verify that all pieces fit together. At the same time, this arrangement makes it very difficult to review the pull request in whole (because it's too large) and also to incrementally improve it by making small changes and reviewing only the recent changes. That's why we have a spike process, where we start with a throw-away prototype demonstrating feasibility of the chosen approach, and follow with a series of smaller pull requests that are easier to digest, review and incrementally improve. I would really appreciate if you could split this PR into smaller chunks, it will make it much easier to review the proposed changes. The first step could be the code & docs for new binding-level events and |
5fe3852
to
953f5ff
Compare
4e4f238
to
c8068c6
Compare
It now depends on #4430 |
c33a82c
to
89ede5a
Compare
Awesome!
That's helpful too.
As far as I can see, this is not a chore, you are introducing a new feature See the thread in #4377 (comment):
|
e259f45
to
e39cc65
Compare
It now depends #4451 |
615dffa
to
d4589d5
Compare
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 scan through the PR especially the tests. Most of them make sense to me. Just have one question.
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.
☝️
This metadata allows optimization of context.find() to leverage tag index.
Fixes #4356 - create index for bindings by tag - optimize find bindings by tag - leverage findByTag for filterByTag to find matching bindings
d4589d5
to
3db144f
Compare
@bajtos I also added a commit to extract context observer subscription logic into |
64a0208
to
a6778b3
Compare
@raymondfeng thank you for extracting smaller pieces out of the context class/file, the code structure looks much better now! I am afraid I don't have enough energy to review the pull request in detail this week. I quickly skimmed through the changes and don't see any obvious major issues. Let's ask other owners of this functional area to review & approve the changes. |
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.
👍
Fixes #4356 and #4363
Matching all bindings by a filter function can be expensive. This PR improves performance for one of the primary usage - find bindings by tags.
Main changes in this PR:
Binding
to beEventEmitter
- emitting events when binding scope/tags/value are changedContext
to react to binding events to maintain an index of bindings by tagContext.findByTag
to leverage binding index if possibleChecklist
👉 Read and sign the CLA (Contributor License Agreement) 👈
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated👉 Check out how to submit a PR 👈