runtime: IOptionsSnapshot is very slow
This came up after a perf investigation on a recently refactored endpoint. Moving us back from IOptionsSnapshot to IOptionsMonitor knocked off ~100us, nothing else changed.
This option type of ours is fairly costly to create and looking into OptionsManager<TOptions> showed an obvious clue why we saw issues. OptionsManager does not just cache the instance for the duration of the DI scope but it also creates from scratch an instance through the factory per scope.
Looking through the docs this is vaguely alluded to (though there are contra-indicators) but after doing a search on SO and blogs on the subject most users seem to focus entirely on the ‘you get a constant value during a scope/request’ aspect of it.
As nobody (famous last words) seems to care whether it is entirely recreated my question would be:
Why doesn’t OptionsManager<TOptions> cache the result of an IOptionsMonitor.Get call? (leaving out the backwards compatibility part for the sake of argument)
About this issue
- Original URL
- State: open
- Created 3 years ago
- Reactions: 3
- Comments: 40 (27 by maintainers)
Links to this issue
- Options pattern in ASP.NET Core | Microsoft Learn
- Patrón de opciones en ASP.NET Core | Microsoft Learn
- ASP.NET Core 中的选项模式 | Microsoft Learn
- Optionsmuster in ASP.NET Core | Microsoft Learn
- Padrão de opções no ASP.NET Core | Microsoft Learn
- Wzorzec opcji na platformie ASP.NET Core | Microsoft Learn
- Patrón de opciones en ASP.NET Core | Microsoft Learn
- ASP.NET Core 中的选项模式 | Microsoft Learn
Right. We should consider changing the implementation so that it caches the currently cached value so scoped services are a consistent view of the data but there’s no need to recompute if nothing changes
IOptionsSnapshot can’t use IOptionsMonitor. In fact, we tried to do that in .NET 7 and it is a major breaking change. IOptionsSnapshot supports injecting scoped
IConfigureOptions<T>. That’s the problem.The combination of IOptionsSnapshot with uncached configuration binding is a performance pit of failure worth investigating. Specifically, the problem is that calling:
@deeprobin I think the bottleneck is this: #33954. And if we use
IOptionsSnapshot, it runs on every request. If we useIOptionsMonitor, it runs once, and then it’s cached until the configuration is changed (and reloaded). I encourage you to try this with IConfiguration that contains several thousand key-value pairs (and nesting, etc.)I agree, alright will be cooking it up.
Yes it’s a workaround that we can’t ship but you can use in your apps if they don’t hit this scenario
This is a similar solution that I implemented as a workaround for issue in our code. I ended up writing an extension to provide a custom binder, and wrote some binders by hand that were written for the exact types used. It performed very well.
A source generator that will generate similar code as the one I wrote by hand sounds like a very good solution to me.
@davidfowl that was an honest question, I would like to fix it up but suggestions are welcome.
If we are talking specifically about IOptionsSnapshot, that was intended to be really the no-caching you get a really new fresh instance on every scope, so it literally was intended to recompute everything on every request