Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/EventLogExpert.Runtime/Concurrency/ConcurrencyLimits.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Runtime.Concurrency;

/// <summary>Shared concurrency limits for background, I/O-bound work across the runtime.</summary>
internal static class ConcurrencyLimits
{
/// <summary>
/// Max degree of parallelism for background I/O-bound work (live-log resolution, exported-log folder scans).
/// Capped at one below the processor count and floored at 1, so a burst of concurrent file opens leaves a core for the
/// UI and never saturates the disk. Centralized here so every I/O fan-out shares one policy.
/// </summary>
internal static int MaxBackgroundIoParallelism { get; } = Math.Max(1, Environment.ProcessorCount - 1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ public IServiceCollection AddEventLogRuntime()
services.AddSingleton<IScenarioSource, BuiltInScenarioSource>();
services.AddSingleton<BuiltInScenarioRegistry>();
services.AddSingleton<IChannelPresenceProbe, ChannelPresenceProbe>();
services.AddSingleton<IEvtxChannelReader, EvtxChannelReader>();
services.AddSingleton<IScenarioQueryService, ScenarioQueryService>();
services.AddSingleton<IScenarioLaunchService, ScenarioLaunchService>();
services.AddSingleton<IScenarioApplyService, ScenarioApplyService>();
Expand Down
2 changes: 1 addition & 1 deletion src/EventLogExpert.Runtime/EventLog/OpenLogEffects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ internal sealed class OpenLogEffects(
// EvtNext batch: benchmarked Win11 throughput sweet spot; 512 regresses.
private const int ReadBatchSize = 256;

private static readonly int s_maxGlobalConcurrency = Math.Max(1, Environment.ProcessorCount - 1);
private static readonly int s_maxGlobalConcurrency = ConcurrencyLimits.MaxBackgroundIoParallelism;
private static readonly PrioritySemaphore s_resolutionGate = new(s_maxGlobalConcurrency);

private readonly LogCloseCoordinator _closeCoordinator = closeCoordinator;
Expand Down
31 changes: 0 additions & 31 deletions src/EventLogExpert.Runtime/Histogram/Effects.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/EventLogExpert.Runtime/Histogram/HistogramState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ namespace EventLogExpert.Runtime.Histogram;
[FeatureState]
public sealed record HistogramState
{
public bool IsVisible { get; init; } = true;
public bool IsVisible { get; init; }
}

This file was deleted.

2 changes: 2 additions & 0 deletions src/EventLogExpert.Runtime/Menu/IMenuActionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public interface IMenuActionService

Task<OpenLogsBatchResult> OpenLiveLogsAsync(IEnumerable<string> logNames, bool combineLog);

Task<OpenLogsBatchResult> OpenLogFilesAsync(IEnumerable<string> filePaths, bool combineLog);

Task<bool> OpenSettingsAsync();

Task SaveFiltersAsFilterSetAsync();
Expand Down
14 changes: 8 additions & 6 deletions src/EventLogExpert.Runtime/Scenarios/ChannelPresenceProbe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ internal ChannelPresenceProbe(ITraceLogger traceLogger, Func<IEnumerable<string>
_readChannels = readChannels;
}

public IReadOnlySet<string> GetPresentChannels()
public IReadOnlySet<string> GetPresentChannels() => TryGetPresentChannels() ?? s_empty;

public bool IsPresent(string logName) => GetPresentChannels().Contains(logName);

public Task PrimeAsync() => Task.Run(GetPresentChannels);

public IReadOnlySet<string>? TryGetPresentChannels()
{
var cached = _cache;

Expand All @@ -46,18 +52,14 @@ public IReadOnlySet<string> GetPresentChannels()
// A failed read is not cached, so the next read retries.
if (loaded is not null) { _cache = loaded; }

return loaded ?? s_empty;
return loaded;
}
finally
{
_gate.Release();
}
}

public bool IsPresent(string logName) => GetPresentChannels().Contains(logName);

public Task PrimeAsync() => Task.Run(GetPresentChannels);

private IReadOnlySet<string>? TryReadChannels()
{
try
Expand Down
15 changes: 15 additions & 0 deletions src/EventLogExpert.Runtime/Scenarios/EvtxChannelReadResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Runtime.Scenarios;

internal readonly record struct EvtxChannelReadResult(string? Channel, bool Failed)
{
/// <summary>A log that opened but had no records, so no channel could be read; not a failure.</summary>
public static EvtxChannelReadResult Empty { get; } = new(null, false);

/// <summary>A file that could not be opened or read.</summary>
public static EvtxChannelReadResult Unreadable { get; } = new(null, true);

public static EvtxChannelReadResult FromChannel(string channel) => new(channel, false);
}
42 changes: 42 additions & 0 deletions src/EventLogExpert.Runtime/Scenarios/EvtxChannelReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Eventing.Common.Channels;
using EventLogExpert.Eventing.Readers;

namespace EventLogExpert.Runtime.Scenarios;

/// <summary>
/// Reads a file's channel with a single lightweight record read (System render only, no description resolution).
/// Fully non-throwing so one corrupt or locked file cannot fault a parallel folder scan.
/// </summary>
internal sealed class EvtxChannelReader : IEvtxChannelReader
{
public EvtxChannelReadResult ReadChannel(string filePath)
{
try
{
using var reader = new EventLogReader(filePath, LogPathType.File, renderXml: false, reverseDirection: false);

if (!reader.IsValid) { return EvtxChannelReadResult.Unreadable; }

if (!reader.TryGetEvents(out var events, batchSize: 1))
{
// A false with no error code is a clean end-of-log (empty); a Win32 error is a read failure.
return reader.LastErrorCode is null ? EvtxChannelReadResult.Empty : EvtxChannelReadResult.Unreadable;
}

if (events.Length == 0) { return EvtxChannelReadResult.Empty; }

var record = events[0];

return record.IsSuccess && !string.IsNullOrEmpty(record.LogName)
? EvtxChannelReadResult.FromChannel(record.LogName)
: EvtxChannelReadResult.Unreadable;
}
catch (Exception)
{
return EvtxChannelReadResult.Unreadable;
}
}
}
28 changes: 28 additions & 0 deletions src/EventLogExpert.Runtime/Scenarios/EvtxFolderScanResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using System.Collections.Immutable;

namespace EventLogExpert.Runtime.Scenarios;

/// <summary>
/// Discriminated result for <see cref="IEvtxFolderEnumerator.EnumerateTopLevel" /> so a folder that cannot be read
/// is reported distinctly from one that merely holds no <c>.evtx</c> files.
/// </summary>
public abstract record EvtxFolderScanResult
{
private EvtxFolderScanResult() { }

public sealed record Files(ImmutableArray<string> Paths) : EvtxFolderScanResult;

public sealed record Empty : EvtxFolderScanResult
{
private Empty() { }

public static Empty Instance { get; } = new();
}

public sealed record AccessDenied(string Message) : EvtxFolderScanResult;

public sealed record IoError(string Message) : EvtxFolderScanResult;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ internal interface IChannelPresenceProbe

/// <summary>Warms the cache on a background thread.</summary>
Task PrimeAsync();

/// <summary>Present channel names, or null when the channel set could not be read.</summary>
IReadOnlySet<string>? TryGetPresentChannels();
}
10 changes: 10 additions & 0 deletions src/EventLogExpert.Runtime/Scenarios/IEvtxChannelReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Runtime.Scenarios;

/// <summary>Reads the channel (log name) an exported <c>.evtx</c> file's events belong to.</summary>
internal interface IEvtxChannelReader
{
EvtxChannelReadResult ReadChannel(string filePath);
}
10 changes: 10 additions & 0 deletions src/EventLogExpert.Runtime/Scenarios/IEvtxFolderEnumerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Runtime.Scenarios;

/// <summary>Lists the top-level <c>.evtx</c> files in a folder, surfacing access and IO failures distinctly.</summary>
public interface IEvtxFolderEnumerator
{
EvtxFolderScanResult EnumerateTopLevel(string folderPath, CancellationToken cancellationToken);
}
10 changes: 10 additions & 0 deletions src/EventLogExpert.Runtime/Scenarios/IScenarioLaunchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,14 @@ public interface IScenarioLaunchService
/// filter; <paramref name="combineLog" /> false opens a fresh view, true merges into the active workspace.
/// </summary>
Task<ScenarioLaunchResult> LaunchAsync(ScenarioDefinition scenario, DateFilter? dateWindow, bool combineLog = false);

/// <summary>
/// Prompts for a folder, opens the exported <c>.evtx</c> files whose channel matches the scenario, and applies
/// the scenario's filters to a fresh view. The scenario's filters and channels are read from the definition, so this
/// works even for logs not present on the local host. The folder enumeration and per-file channel probe run off the
/// caller's thread and honor <paramref name="cancellationToken" />, returning
/// <see cref="ScenarioFolderOutcome.Cancelled" /> if the scan is abandoned before any log is opened.
/// </summary>
Task<ScenarioFolderLaunchResult> LaunchFromFolderAsync(
ScenarioDefinition scenario, DateFilter? dateWindow, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public interface IScenarioQueryService
/// <summary>Scenarios whose channels match a currently-loaded log name.</summary>
IReadOnlyList<ScenarioDefinition> GetInAppScenarios(IReadOnlyCollection<string> loadedLogNames);

/// <summary>Scenarios with at least one required channel present on the host.</summary>
/// <summary>A snapshot of which channels exist on this host, for gating live launches.</summary>
LivePresence GetLivePresence();

/// <summary>Every channel-presence scenario in the catalog, regardless of local availability.</summary>
IReadOnlyList<ScenarioDefinition> GetSplashScenarios();
}
10 changes: 10 additions & 0 deletions src/EventLogExpert.Runtime/Scenarios/LivePresence.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Runtime.Scenarios;

/// <summary>
/// A snapshot of the host's channel set. <see cref="Known" /> is false when the channel set could not be read, in
/// which case callers should treat every scenario as launchable rather than falsely reporting it offline.
/// </summary>
public sealed record LivePresence(bool Known, IReadOnlySet<string> Present);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using System.Collections.Immutable;

namespace EventLogExpert.Runtime.Scenarios;

/// <summary>The terminal state of <see cref="IScenarioLaunchService.LaunchFromFolderAsync" />.</summary>
public enum ScenarioFolderOutcome
{
Cancelled,
Error,
NoMatchingLogs,
NoLogsOpened,
Completed
}

/// <summary>The result of launching a scenario against exported logs in a folder.</summary>
public sealed record ScenarioFolderLaunchResult
{
public required ScenarioFolderOutcome Outcome { get; init; }

public int Matched { get; init; }

public int Unreadable { get; init; }

public int Opened { get; init; }

public int Empty { get; init; }

public int Failed { get; init; }

public ImmutableArray<string> MatchedChannels { get; init; } = [];

public ImmutableArray<string> MissingChannels { get; init; } = [];

public string? Message { get; init; }

public static ScenarioFolderLaunchResult Cancelled { get; } = new() { Outcome = ScenarioFolderOutcome.Cancelled };

public static ScenarioFolderLaunchResult Error(string message, int unreadable = 0) =>
new() { Outcome = ScenarioFolderOutcome.Error, Message = message, Unreadable = unreadable };
}
Loading
Loading