Skip to content

Commit 9307605

Browse files
jschick04NikTilton
authored andcommitted
Optimized hot paths, less redundancy and memory usage
1 parent 614a9e6 commit 9307605

File tree

14 files changed

+361
-151
lines changed

14 files changed

+361
-151
lines changed

src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
4444

4545
modelBuilder.Entity<ProviderDetails>()
4646
.Property(e => e.Messages)
47-
.HasConversion<CompressedJsonValueConverter<IEnumerable<MessageModel>>>();
47+
.HasConversion<CompressedJsonValueConverter<IReadOnlyList<MessageModel>>>();
4848

4949
modelBuilder.Entity<ProviderDetails>()
5050
.Property(e => e.Parameters)
5151
.HasConversion<CompressedJsonValueConverter<IEnumerable<MessageModel>>>();
5252

5353
modelBuilder.Entity<ProviderDetails>()
5454
.Property(e => e.Events)
55-
.HasConversion<CompressedJsonValueConverter<IEnumerable<EventModel>>>();
55+
.HasConversion<CompressedJsonValueConverter<IReadOnlyList<EventModel>>>();
5656

5757
modelBuilder.Entity<ProviderDetails>()
5858
.Property(e => e.Keywords)
@@ -137,8 +137,6 @@ public void PerformUpgradeIfNeeded()
137137
};
138138
allProviderDetails.Add(p);
139139
}
140-
141-
detailsReader.Close();
142140
}
143141
else
144142
{
@@ -156,10 +154,10 @@ public void PerformUpgradeIfNeeded()
156154
};
157155
allProviderDetails.Add(p);
158156
}
159-
160-
detailsReader.Close();
161157
}
162158

159+
detailsReader.Close();
160+
163161
command.CommandText = "DROP TABLE \"ProviderDetails\"";
164162
command.ExecuteNonQuery();
165163
command.CommandText = "VACUUM";

src/EventLogExpert.Eventing/EventResolvers/EventProviderDatabaseEventResolver.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,18 @@ protected override void Dispose(bool disposing)
7070

7171
public void ResolveProviderDetails(EventRecord eventRecord)
7272
{
73+
ObjectDisposedException.ThrowIf(IsDisposed, nameof(EventProviderDatabaseEventResolver));
74+
75+
// Fast path: ConcurrentDictionary is thread-safe for reads, so we can check
76+
// without any lock. This avoids serializing all 8 parallel reader threads on
77+
// the UpgradeableReadLock for providers that are already cached.
78+
if (ProviderDetails.ContainsKey(eventRecord.ProviderName)) { return; }
79+
7380
ProviderDetailsLock.EnterUpgradeableReadLock();
7481

7582
try
7683
{
77-
// Early disposed check for fail-fast behavior. This is a defensive check only;
78-
// the authoritative check is inside _databaseAccessLock below.
79-
// A race window exists here, but it's handled by the check inside the lock.
80-
ObjectDisposedException.ThrowIf(IsDisposed, nameof(EventProviderDatabaseEventResolver));
81-
84+
// Re-check after acquiring lock - another thread may have added this provider
8285
if (ProviderDetails.ContainsKey(eventRecord.ProviderName))
8386
{
8487
return;
@@ -88,7 +91,7 @@ public void ResolveProviderDetails(EventRecord eventRecord)
8891

8992
try
9093
{
91-
// Double-check after acquiring write lock - another thread may have added this provider
94+
// Triple-check after acquiring write lock
9295
if (ProviderDetails.ContainsKey(eventRecord.ProviderName))
9396
{
9497
return;

src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,15 @@ public virtual DisplayEventModel ResolveEvent(EventRecord eventRecord)
7979

8080
var keywords = GetKeywordsFromBitmask(eventRecord);
8181

82+
// Resolve the modern event once and reuse for both description and task name
83+
ProviderDetails.TryGetValue(eventRecord.ProviderName, out var details);
84+
var modernEvent = details is not null ? GetModernEvent(eventRecord, details) : null;
85+
8286
return new DisplayEventModel(eventRecord.PathName, eventRecord.PathType)
8387
{
8488
ActivityId = eventRecord.ActivityId,
8589
ComputerName = _cache?.GetOrAddValue(eventRecord.ComputerName) ?? eventRecord.ComputerName,
86-
Description = ResolveDescription(eventRecord),
90+
Description = ResolveDescription(eventRecord, details, modernEvent),
8791
Id = eventRecord.Id,
8892
Keywords = keywords,
8993
KeywordsDisplayName = string.Join(", ", keywords),
@@ -92,7 +96,7 @@ public virtual DisplayEventModel ResolveEvent(EventRecord eventRecord)
9296
ProcessId = eventRecord.ProcessId,
9397
RecordId = eventRecord.RecordId,
9498
Source = _cache?.GetOrAddValue(eventRecord.ProviderName) ?? eventRecord.ProviderName,
95-
TaskCategory = ResolveTaskName(eventRecord),
99+
TaskCategory = ResolveTaskName(eventRecord, details, modernEvent),
96100
ThreadId = eventRecord.ThreadId,
97101
TimeCreated = eventRecord.TimeCreated,
98102
UserId = eventRecord.UserId,
@@ -322,6 +326,7 @@ private string FormatDescription(
322326
IEnumerable<MessageModel> parameters)
323327
{
324328
string returnDescription;
329+
325330
if (string.IsNullOrWhiteSpace(descriptionTemplate))
326331
{
327332
// If there is only one property then this is what certain EventRecords look like
@@ -470,11 +475,10 @@ private List<string> GetKeywordsFromBitmask(EventRecord eventRecord)
470475

471476
foreach (var k in s_standardKeywords.Keys)
472477
{
473-
if ((eventRecord.Keywords.Value & k) == k)
474-
{
475-
var keyword = s_standardKeywords[k].TrimEnd('\0');
476-
returnValue.Add(_cache?.GetOrAddValue(keyword) ?? keyword);
477-
}
478+
if ((eventRecord.Keywords.Value & k) != k) { continue; }
479+
480+
var keyword = s_standardKeywords[k].TrimEnd('\0');
481+
returnValue.Add(_cache?.GetOrAddValue(keyword) ?? keyword);
478482
}
479483

480484
if (!ProviderDetails.TryGetValue(eventRecord.ProviderName, out var details) || details is null)
@@ -486,18 +490,16 @@ private List<string> GetKeywordsFromBitmask(EventRecord eventRecord)
486490
// so let's skip those.
487491
var lower32 = eventRecord.Keywords.Value & 0xFFFFFFFF;
488492

489-
if (lower32 != 0)
493+
if (lower32 == 0) { return returnValue; }
494+
495+
foreach (var k in details.Keywords.Keys)
490496
{
491-
foreach (var k in details.Keywords.Keys)
492-
{
493-
if ((lower32 & k) == k)
494-
{
495-
var keyword = details.Keywords[k].TrimEnd('\0');
496-
returnValue.Add(_cache?.GetOrAddValue(keyword) ?? keyword);
497-
}
498-
}
499-
}
497+
if ((lower32 & k) != k) { continue; }
500498

499+
var keyword = details.Keywords[k].TrimEnd('\0');
500+
returnValue.Add(_cache?.GetOrAddValue(keyword) ?? keyword);
501+
}
502+
501503
return returnValue;
502504
}
503505

@@ -513,10 +515,22 @@ private List<string> GetKeywordsFromBitmask(EventRecord eventRecord)
513515

514516
int eventPropertyCount = eventRecord.Properties.Count;
515517

516-
EventModel? modernEvent = details.Events
517-
.FirstOrDefault(e => e.Id == eventRecord.Id &&
518-
e.Version == eventRecord.Version &&
519-
e.LogName == eventRecord.LogName);
518+
// Use indexed lookup instead of linear scan
519+
var candidateEvents = details.GetEventsById(eventRecord.Id);
520+
521+
EventModel? modernEvent = null;
522+
523+
foreach (var e in candidateEvents)
524+
{
525+
if (e.Id != eventRecord.Id || e.Version != eventRecord.Version || e.LogName != eventRecord.LogName)
526+
{
527+
continue;
528+
}
529+
530+
modernEvent = e;
531+
532+
break;
533+
}
520534

521535
if (modernEvent is not null && DoesTemplateMatchPropertyCount(modernEvent.Template, eventPropertyCount))
522536
{
@@ -532,8 +546,10 @@ private List<string> GetKeywordsFromBitmask(EventRecord eventRecord)
532546
LogLevel.Debug);
533547
}
534548

535-
foreach (var @event in details.Events.Where(e => e.Id == eventRecord.Id && e.LogName == eventRecord.LogName))
549+
foreach (var @event in candidateEvents)
536550
{
551+
if (@event.LogName != eventRecord.LogName) { continue; }
552+
537553
if (!DoesTemplateMatchPropertyCount(@event.Template, eventPropertyCount)) { continue; }
538554

539555
Logger?.Trace($"{nameof(GetModernEvent)}: Match by Id/LogName with template - EventId={eventRecord.Id}, LogName={eventRecord.LogName}, MatchedVersion={@event.Version}",
@@ -544,8 +560,10 @@ private List<string> GetKeywordsFromBitmask(EventRecord eventRecord)
544560

545561
// Try again forcing the long to a short and with no log name.
546562
// This is needed for providers such as Microsoft-Windows-Complus
547-
foreach (var @event in details.Events.Where(e => (short)e.Id == eventRecord.Id && e.Version == eventRecord.Version))
563+
foreach (var @event in details.Events)
548564
{
565+
if ((short)@event.Id != eventRecord.Id || @event.Version != eventRecord.Version) { continue; }
566+
549567
if (!DoesTemplateMatchPropertyCount(@event.Template, eventPropertyCount)) { continue; }
550568

551569
Logger?.Trace($"{nameof(GetModernEvent)}: Match by short Id/Version fallback - EventId={eventRecord.Id}, Version={eventRecord.Version}",
@@ -554,50 +572,47 @@ private List<string> GetKeywordsFromBitmask(EventRecord eventRecord)
554572
return @event;
555573
}
556574

557-
var candidateCount = details.Events.Count(e => e.Id == eventRecord.Id);
558-
559-
Logger?.Trace($"{nameof(GetModernEvent)}: No matching event found - EventId={eventRecord.Id}, Version={eventRecord.Version}, LogName={eventRecord.LogName}, PropertyCount={eventPropertyCount}, CandidateEventsWithSameId={candidateCount}",
575+
Logger?.Trace($"{nameof(GetModernEvent)}: No matching event found - EventId={eventRecord.Id}, Version={eventRecord.Version}, LogName={eventRecord.LogName}, PropertyCount={eventPropertyCount}, CandidateEventsWithSameId={candidateEvents.Count}",
560576
LogLevel.Debug);
561577

562578
return null;
563579
}
564580

565581
/// <summary>Resolve event descriptions from an event record.</summary>
566-
private string ResolveDescription(EventRecord eventRecord)
582+
private string ResolveDescription(EventRecord eventRecord, ProviderDetails? details, EventModel? modernEvent)
567583
{
568-
if (!ProviderDetails.TryGetValue(eventRecord.ProviderName, out var details) || details is null)
584+
if (details is null)
569585
{
570586
Logger?.Trace($"{nameof(ResolveDescription)}: No provider details available - Provider={eventRecord.ProviderName}, EventId={eventRecord.Id}, RecordId={eventRecord.RecordId}",
571587
LogLevel.Debug);
572588

573589
return DefaultNoProviderDescription;
574590
}
575591

576-
var @event = GetModernEvent(eventRecord, details);
577-
var properties = GetFormattedProperties(@event?.Template, eventRecord.Properties);
592+
var properties = GetFormattedProperties(modernEvent?.Template, eventRecord.Properties);
578593

579-
if (!string.IsNullOrEmpty(@event?.Description))
594+
if (!string.IsNullOrEmpty(modernEvent?.Description))
580595
{
581596
Logger?.Trace($"{nameof(ResolveDescription)}: Using modern event description - Provider={eventRecord.ProviderName}, EventId={eventRecord.Id}, PropertyCount={properties.Count}",
582597
LogLevel.Debug);
583598

584-
return FormatDescription(properties, @event?.Description, details.Parameters);
599+
return FormatDescription(properties, modernEvent?.Description, details.Parameters);
585600
}
586601

587-
// Legacy provider message lookup, if there is multiple messages then move on so we don't show wrong description
588-
var legacyMessage = details.Messages.Where(m => m.ShortId == eventRecord.Id).ToList();
602+
// Legacy provider message lookup using indexed dictionary
603+
var legacyMessages = details.GetMessagesByShortId((short)eventRecord.Id);
589604

590-
if (legacyMessage.Count == 1)
605+
if (legacyMessages.Count == 1)
591606
{
592607
Logger?.Trace($"{nameof(ResolveDescription)}: Using legacy message - Provider={eventRecord.ProviderName}, EventId={eventRecord.Id}, PropertyCount={properties.Count}",
593608
LogLevel.Debug);
594609

595-
return FormatDescription(properties, legacyMessage.First().Text, details.Parameters);
610+
return FormatDescription(properties, legacyMessages[0].Text, details.Parameters);
596611
}
597612

598-
if (legacyMessage.Count > 1)
613+
if (legacyMessages.Count > 1)
599614
{
600-
Logger?.Trace($"{nameof(ResolveDescription)}: Multiple legacy messages found, skipping - Provider={eventRecord.ProviderName}, EventId={eventRecord.Id}, MessageCount={legacyMessage.Count}",
615+
Logger?.Trace($"{nameof(ResolveDescription)}: Multiple legacy messages found, skipping - Provider={eventRecord.ProviderName}, EventId={eventRecord.Id}, MessageCount={legacyMessages.Count}",
601616
LogLevel.Debug);
602617
}
603618

@@ -617,16 +632,14 @@ private string ResolveDescription(EventRecord eventRecord)
617632
}
618633

619634
/// <summary>Resolve event task names from an event record.</summary>
620-
private string ResolveTaskName(EventRecord eventRecord)
635+
private string ResolveTaskName(EventRecord eventRecord, ProviderDetails? details, EventModel? modernEvent)
621636
{
622-
if (!ProviderDetails.TryGetValue(eventRecord.ProviderName, out var details) || details is null)
637+
if (details is null)
623638
{
624639
return string.Empty;
625640
}
626641

627-
var @event = GetModernEvent(eventRecord, details);
628-
629-
if (@event?.Task is not null && details.Tasks.TryGetValue(@event.Task, out var taskName))
642+
if (modernEvent?.Task is not null && details.Tasks.TryGetValue(modernEvent.Task, out var taskName))
630643
{
631644
taskName = taskName.TrimEnd('\0');
632645
return _cache?.GetOrAddValue(taskName) ?? taskName;
@@ -645,9 +658,18 @@ private string ResolveTaskName(EventRecord eventRecord)
645658
return _cache?.GetOrAddValue(taskName) ?? taskName;
646659
}
647660

648-
var potentialTaskNames = details.Messages
649-
.Where(m => m.ShortId == eventRecord.Task && m.LogLink != null && m.LogLink == eventRecord.LogName)
650-
.ToList();
661+
// Use indexed lookup instead of linear scan with .ToList()
662+
var messagesByShortId = details.GetMessagesByShortId((short)eventRecord.Task);
663+
664+
List<MessageModel>? potentialTaskNames = null;
665+
666+
foreach (var m in messagesByShortId)
667+
{
668+
if (m.LogLink is null || m.LogLink != eventRecord.LogName) { continue; }
669+
670+
potentialTaskNames ??= [];
671+
potentialTaskNames.Add(m);
672+
}
651673

652674
if (potentialTaskNames is { Count: > 0 })
653675
{

src/EventLogExpert.Eventing/EventResolvers/LocalProviderEventResolver.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,22 @@ internal LocalProviderEventResolver(IEventResolverCache? cache = null, ITraceLog
1818

1919
public void ResolveProviderDetails(EventRecord eventRecord)
2020
{
21+
ObjectDisposedException.ThrowIf(IsDisposed, nameof(LocalProviderEventResolver));
22+
23+
// Fast path: ConcurrentDictionary is thread-safe for reads
24+
if (ProviderDetails.ContainsKey(eventRecord.ProviderName)) { return; }
25+
2126
ProviderDetailsLock.EnterUpgradeableReadLock();
2227

2328
try
2429
{
25-
ObjectDisposedException.ThrowIf(IsDisposed, nameof(LocalProviderEventResolver));
26-
30+
// Re-check after acquiring lock
2731
if (ProviderDetails.ContainsKey(eventRecord.ProviderName)) { return; }
2832

2933
ProviderDetailsLock.EnterWriteLock();
3034

3135
try
3236
{
33-
// Double-check in case another thread added the provider details while we were waiting.
3437
if (ProviderDetails.ContainsKey(eventRecord.ProviderName)) { return; }
3538

3639
var details = new EventMessageProvider(eventRecord.ProviderName, Logger).LoadProviderDetails();

src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ private ProviderDetails LoadMessagesFromModernProvider(ProviderMetadata provider
244244
_logger?.Trace($"Failed to load Tasks for modern provider: {_providerName}. Exception:\n{ex}");
245245
}
246246

247-
_logger?.Trace($"Returning {provider.Events.Count()} events for provider {_providerName}");
247+
_logger?.Trace($"Returning {provider.Events.Count} events for provider {_providerName}");
248248

249249
return provider;
250250
}

0 commit comments

Comments
 (0)