Skip to content

Commit 816d72f

Browse files
committed
Removed IsLoading for table updates with multi log loading
1 parent 2dbdc03 commit 816d72f

File tree

5 files changed

+120
-18
lines changed

5 files changed

+120
-18
lines changed

src/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,72 @@ public void FilterActiveLogs_WhenNoFiltersEnabled_ShouldReturnAllEvents()
9999
Assert.Equal(2, result[logId].Count);
100100
}
101101

102+
[Theory]
103+
[InlineData(100)]
104+
[InlineData(9_999)]
105+
[InlineData(10_000)]
106+
[InlineData(10_001)]
107+
public void GetFilteredEvents_WhenFilteringEnabled_ShouldFilterCorrectlyAcrossThreshold(int eventCount)
108+
{
109+
// Arrange
110+
var filterService = CreateFilterService();
111+
var baseTime = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
112+
113+
// Create events: half before cutoff, half after
114+
var events = Enumerable.Range(0, eventCount)
115+
.Select(i => EventUtils.CreateTestEvent(
116+
id: i,
117+
timeCreated: baseTime.AddMinutes(i),
118+
recordId: i))
119+
.ToList();
120+
121+
var cutoff = baseTime.AddMinutes(eventCount / 2);
122+
var dateFilter = new FilterDateModel { After = cutoff, Before = baseTime.AddMinutes(eventCount), IsEnabled = true };
123+
var eventFilter = new EventFilter(dateFilter, []);
124+
125+
// Act
126+
var result = filterService.GetFilteredEvents(events, eventFilter);
127+
128+
// Assert — should return only events at or after the cutoff
129+
var expectedCount = events.Count(e => e.TimeCreated >= cutoff && e.TimeCreated <= baseTime.AddMinutes(eventCount));
130+
Assert.Equal(expectedCount, result.Count);
131+
Assert.All(result, e => Assert.True(e.TimeCreated >= cutoff));
132+
}
133+
134+
[Fact]
135+
public void GetFilteredEvents_WhenFilteringSmallCollection_ShouldReturnSameResultsAsLargeCollection()
136+
{
137+
// Arrange — verify both paths produce identical results
138+
var filterService = CreateFilterService();
139+
var baseTime = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
140+
var cutoff = baseTime.AddMinutes(50);
141+
142+
var dateFilter = new FilterDateModel { After = cutoff, Before = baseTime.AddMinutes(200), IsEnabled = true };
143+
var eventFilter = new EventFilter(dateFilter, []);
144+
145+
// Small collection (sequential path)
146+
var smallEvents = Enumerable.Range(0, 100)
147+
.Select(i => EventUtils.CreateTestEvent(id: i, timeCreated: baseTime.AddMinutes(i), recordId: i))
148+
.ToList();
149+
150+
// Large collection (PLINQ path) with the same first 100 events
151+
var largeEvents = Enumerable.Range(0, 15_000)
152+
.Select(i => EventUtils.CreateTestEvent(id: i, timeCreated: baseTime.AddMinutes(i), recordId: i))
153+
.ToList();
154+
155+
// Act
156+
var smallResult = filterService.GetFilteredEvents(smallEvents, eventFilter);
157+
var largeResult = filterService.GetFilteredEvents(largeEvents, eventFilter);
158+
159+
// Assert — small result should match the first 100 events' filtered subset
160+
var expectedSmallCount = smallEvents.Count(e => e.TimeCreated >= cutoff && e.TimeCreated <= baseTime.AddMinutes(200));
161+
Assert.Equal(expectedSmallCount, smallResult.Count);
162+
163+
// Large result should include the same events as small result (plus more from the larger set)
164+
Assert.True(largeResult.Count > smallResult.Count);
165+
Assert.All(smallResult, e => Assert.Contains(e, largeResult));
166+
}
167+
102168
[Fact]
103169
public void GetFilteredEvents_WhenNoFiltersEnabled_ShouldReturnAllEvents()
104170
{

src/EventLogExpert.UI/FilterMethods.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public static IReadOnlyList<DisplayEventModel> SortEvents(
7373
var sorted = new List<DisplayEventModel>(events);
7474
sorted.Sort(GetComparer(orderBy, isDescending));
7575

76-
return sorted;
76+
return sorted.AsReadOnly();
7777
}
7878

7979
internal static Comparison<DisplayEventModel> GetComparer(ColumnName? orderBy, bool isDescending)

src/EventLogExpert.UI/Services/FilterService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,15 @@ public IReadOnlyList<DisplayEventModel> GetFilteredEvents(
4747
return collection
4848
.Where(e => e.FilterByDate(eventFilter.DateFilter)
4949
.Filter(eventFilter.Filters, IsXmlEnabled))
50-
.ToList();
50+
.ToList()
51+
.AsReadOnly();
5152
}
5253

5354
return events.AsParallel()
5455
.Where(e => e.FilterByDate(eventFilter.DateFilter)
5556
.Filter(eventFilter.Filters, IsXmlEnabled))
56-
.ToList();
57+
.ToList()
58+
.AsReadOnly();
5759
}
5860

5961
public bool TryParse(FilterModel filterModel, out string comparison)

src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public Task HandleAddEvent(EventLogAction.AddEvent action, IDispatcher dispatche
6565

6666
var full = updatedBuffer.Count >= EventLogState.MaxNewEvents;
6767

68-
dispatcher.Dispatch(new EventLogAction.AddEventBuffered(updatedBuffer, full));
68+
dispatcher.Dispatch(new EventLogAction.AddEventBuffered(updatedBuffer.AsReadOnly(), full));
6969
}
7070

7171
return Task.CompletedTask;
@@ -151,7 +151,7 @@ public async Task HandleOpenLog(EventLogAction.OpenLog action, IDispatcher dispa
151151
List<DisplayEventModel> events = [];
152152

153153
await using Timer timer = new(
154-
_ => { dispatcher.Dispatch(new StatusBarAction.SetEventsLoading(activityId, Volatile.Read(ref resolved), failed)); },
154+
_ => { dispatcher.Dispatch(new StatusBarAction.SetEventsLoading(activityId, Volatile.Read(ref resolved), Volatile.Read(ref failed))); },
155155
null,
156156
TimeSpan.Zero,
157157
TimeSpan.FromSeconds(3));
@@ -200,6 +200,9 @@ await Parallel.ForEachAsync(
200200

201201
try
202202
{
203+
List<DisplayEventModel> localBatch = new(batch.Length);
204+
int localResolved = 0;
205+
203206
foreach (var @event in batch)
204207
{
205208
token.ThrowIfCancellationRequested();
@@ -215,17 +218,21 @@ await Parallel.ForEachAsync(
215218
continue;
216219
}
217220

218-
var resolvedEvent = eventResolver.ResolveEvent(@event);
219-
220-
lock (events) { events.Add(resolvedEvent); }
221-
222-
Interlocked.Increment(ref resolved);
221+
localBatch.Add(eventResolver.ResolveEvent(@event));
222+
localResolved++;
223223
}
224224
catch (Exception ex)
225225
{
226226
_logger?.Warn($"Failed to resolve RecordId: {@event.RecordId}, {ex.Message}");
227227
}
228228
}
229+
230+
if (localBatch.Count > 0)
231+
{
232+
lock (events) { events.AddRange(localBatch); }
233+
234+
Interlocked.Add(ref resolved, localResolved);
235+
}
229236
}
230237
finally
231238
{
@@ -237,17 +244,31 @@ await Parallel.ForEachAsync(
237244

238245
lastEvent = reader.LastBookmark;
239246
}
240-
catch (TaskCanceledException)
247+
catch (OperationCanceledException)
241248
{
249+
await StopProducerAsync(producerTask);
250+
242251
dispatcher.Dispatch(new EventLogAction.CloseLog(logData.Id, logData.Name));
243252
dispatcher.Dispatch(new StatusBarAction.ClearStatus(activityId));
244253

245254
return;
246255
}
256+
catch (Exception ex)
257+
{
258+
_logger?.Error($"Failed to load log {action.LogName}: {ex.Message}");
259+
260+
await StopProducerAsync(producerTask);
261+
262+
dispatcher.Dispatch(new EventLogAction.CloseLog(logData.Id, logData.Name));
263+
dispatcher.Dispatch(new StatusBarAction.ClearStatus(activityId));
264+
dispatcher.Dispatch(new StatusBarAction.SetResolverStatus($"Error: Failed to load {action.LogName}"));
265+
266+
return;
267+
}
247268

248269
events.Sort((a, b) => Comparer<long?>.Default.Compare(b.RecordId, a.RecordId));
249270

250-
dispatcher.Dispatch(new EventLogAction.LoadEvents(logData, events));
271+
dispatcher.Dispatch(new EventLogAction.LoadEvents(logData, events.AsReadOnly()));
251272

252273
dispatcher.Dispatch(new StatusBarAction.SetEventsLoading(activityId, 0, 0));
253274

@@ -285,7 +306,7 @@ private static EventLogData AddEventsToOneLog(EventLogData logData, List<Display
285306
{
286307
eventsToAdd.AddRange(logData.Events);
287308

288-
return logData with { Events = eventsToAdd };
309+
return logData with { Events = eventsToAdd.AsReadOnly() };
289310
}
290311

291312
private static ImmutableDictionary<string, EventLogData> DistributeEventsToManyLogs(
@@ -320,6 +341,17 @@ private static ImmutableDictionary<string, EventLogData> DistributeEventsToManyL
320341
return newLogs;
321342
}
322343

344+
/// <summary>
345+
/// Awaits the producer task, suppressing all exceptions.
346+
/// The sole purpose is to ensure the producer has fully stopped before
347+
/// the reader is disposed. Any meaningful errors are handled by the caller.
348+
/// </summary>
349+
private static async Task StopProducerAsync(Task producerTask)
350+
{
351+
try { await producerTask; }
352+
catch { /* Intentionally swallowed — caller handles error reporting */ }
353+
}
354+
323355
private void ProcessNewEventBuffer(EventLogState state, IDispatcher dispatcher)
324356
{
325357
var activeLogs = DistributeEventsToManyLogs(state.ActiveLogs, state.NewEventBuffer);

src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ public static EventTableState ReduceUpdateCombinedEvents(EventTableState state)
138138
{
139139
if (state.EventTables.Count <= 1) { return state; }
140140

141-
if (state.EventTables.Any(table => table.IsLoading)) { return state; }
141+
var existingCombinedTable = state.EventTables.FirstOrDefault(table => table.IsCombined);
142142

143-
var existingCombinedTable = state.EventTables.First(table => table.IsCombined);
143+
if (existingCombinedTable is null) { return state; }
144144

145145
var combinedEvents = GetCombinedEvents(
146146
state.EventTables
@@ -149,7 +149,9 @@ public static EventTableState ReduceUpdateCombinedEvents(EventTableState state)
149149
state.OrderBy ?? ColumnName.DateAndTime,
150150
state.IsDescending);
151151

152-
if (combinedEvents.SequenceEqual(existingCombinedTable.DisplayedEvents))
152+
if (combinedEvents.Count == existingCombinedTable.DisplayedEvents.Count &&
153+
combinedEvents.Select(e => e.RecordId)
154+
.SequenceEqual(existingCombinedTable.DisplayedEvents.Select(e => e.RecordId)))
153155
{
154156
return state;
155157
}
@@ -204,7 +206,7 @@ public static EventTableState ReduceUpdateTable(EventTableState state, EventTabl
204206
};
205207
}
206208

207-
private static List<DisplayEventModel> GetCombinedEvents(
209+
private static IReadOnlyList<DisplayEventModel> GetCombinedEvents(
208210
IEnumerable<IEnumerable<DisplayEventModel>> eventLists,
209211
ColumnName? orderBy = null,
210212
bool isDescending = false)
@@ -219,7 +221,7 @@ private static List<DisplayEventModel> GetCombinedEvents(
219221
// Sort in-place instead of creating a new list
220222
combinedEvents.Sort(FilterMethods.GetComparer(orderBy, isDescending));
221223

222-
return combinedEvents;
224+
return combinedEvents.AsReadOnly();
223225
}
224226

225227
private static EventTableState SortDisplayEvents(EventTableState state, ColumnName? orderBy, bool isDescending)

0 commit comments

Comments
 (0)