-
Notifications
You must be signed in to change notification settings - Fork 104
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
Size based EventId LogEventProperty cache #260
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.
Thanks for looking into this! Had a few comments, let me know if you need any more info. Cheers!
src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs
Outdated
Show resolved
Hide resolved
src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs
Outdated
Show resolved
Hide resolved
src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs
Outdated
Show resolved
Hide resolved
src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs
Outdated
Show resolved
Hide resolved
src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs
Outdated
Show resolved
Hide resolved
|
||
cachedProperty = CreateCore(in eventKey); | ||
|
||
if (count < _maxCapacity) |
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.
Applications log a lot of things during start-up that they never log again. Perhaps clearing the cache when it reaches the size limit, and then (lazily) repopulating it, is worth considering, here? Bumping the cache size up to e.g. 256 items would help to avoid degenerate cases.
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 think we could consider making this cache shared (as a static class) among all loggers. Since ConcurrentDictionary
is lock-free on reads, I believe this approach could perform as well as a per-logger cache.
Perhaps clearing the cache when it reaches the size limit
This problem can be addressed by choosing the correct max-cached-items parameter. Speculatively, the total set of application and library event IDs can typically fit into a single cache. A maximum of 1024 items seems reasonable to me.
Implementing an LRU cache could help save some memory. However, this approach might introduce more lock contention, potentially impacting performance.
So, I'd like to suggest using the approach the Microsoft team implemented for caching in FormattedLogValues.
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.
Almost everything exactly the same as in FormattedLogValues source code.
private const int MaxCachedProperties = 1024;
private static readonly ConcurrentDictionary<EventKey, LogEventProperty> s_propertyCache = new ();
private static int s_count;
public static LogEventProperty GetOrCreateProperty(in EventId eventId)
{
var eventKey = new EventKey(eventId);
LogEventProperty? property;
if (s_count >= MaxCachedProperties)
{
if (!s_propertyCache.TryGetValue(eventKey, out property))
{
property = CreateCore(in eventKey);
}
}
else
{
property = s_propertyCache.GetOrAdd(
eventKey,
static key =>
{
Interlocked.Increment(ref s_count);
return CreateCore(in key);
});
}
return property;
}
Actually, the factory in GetOrAdd
might be called multiple times. I don't think this is a problem, as total correctness is not required in this case.
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.
Looks great, thanks for following up on this. Apologies for the slow review, I'm stuck in the middle of a busy few weeks 😅
src/Serilog.Extensions.Logging/Extensions/Logging/EventIdPropertyCache.cs
Outdated
Show resolved
Hide resolved
using Microsoft.Extensions.Logging; | ||
using Serilog.Events; | ||
|
||
static class EventIdPropertyCache |
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.
Might be best to make this a non-static class and declare an instance in a static variable, the same way CachingMessageTemplateParser
is used; in the long run, both of these caches should probably be scoped to the logger factory instead of being static, so this organization will help towards that.
{ | ||
const int MaxCachedProperties = 1024; | ||
|
||
static readonly ConcurrentDictionary<EventKey, LogEventProperty> s_propertyCache = new(); |
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.
Nit: static field names in this codebase use PascalCase
.
static readonly ConcurrentDictionary<EventKey, LogEventProperty> s_propertyCache = new(); | ||
static int s_count; | ||
|
||
public static LogEventProperty GetOrCreateProperty(in EventId eventId) |
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.
With the class converted to non-static, it'd be possible to test that this method returns the same instance (ReferenceEquals()
) for the same eventId
properties, and different instances when different event ids are passed/when the cache is full.
|
||
static class EventIdPropertyCache | ||
{ | ||
const int MaxCachedProperties = 1024; |
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.
Passing this as a constructor arg (non-static class) would make testing behavior around cache limits clearer/easier.
Looks great; thanks @AndreReise! |
Address #259
The
LogEventConstructionBenchmark
resultsBenchmarkDotNet v0.13.10, Windows 10 (10.0.19045.5073/22H2/2022Update)
Intel Core i5-10300H CPU 2.50GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.100
[Host] : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX2
Before:
After: