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
243 changes: 242 additions & 1 deletion src/EventLogExpert.Eventing/Common/Events/EventColumnStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public int GetHashCode(int[] obj)
/// </summary>
public sealed class EventColumnStore
{
// Resolve the tiny update-provider allowlist on the stack; a larger eligibility set falls back to the heap.
private const int MaxStackProviderNames = 32;
// Per-scan EventData field-index memo sentinels: a schema not yet looked up, and a schema that lacks the field.
private const int SchemaFieldAbsent = -1;
private const int SchemaFieldUnresolved = -2;
Expand Down Expand Up @@ -278,6 +280,69 @@ internal void BucketTimeTicksByEventData(
}
}

internal void BucketTimeTicksByEventDataHResult(
ReadOnlySpan<int> rankByPhysical,
long minTicks,
long bucketSpanTicks,
int bucketCount,
string fieldName,
IReadOnlyCollection<string> eligibleProviders,
long[] targetCodes,
int slotCount,
int[] slotCounts,
CancellationToken cancellationToken)
{
int otherSlot = targetCodes.Length;
Span<int> eligibleBuffer = eligibleProviders.Count <= MaxStackProviderNames
? stackalloc int[eligibleProviders.Count]
: new int[eligibleProviders.Count];
ReadOnlySpan<int> eligible = ResolveEligibleSourceIndices(eligibleProviders, eligibleBuffer);
int[] fieldIndexBySchema = NewSchemaFieldMemo();
int offset = 0;

foreach (EventColumnChunk chunk in _sealedChunks)
{
cancellationToken.ThrowIfCancellationRequested();

ReadOnlySpan<long> timeColumn = chunk.TimeTicksColumn;
ReadOnlySpan<int> sourceColumn = chunk.PoolIndexColumn(EventColumnField.Source);

for (int row = 0; row < timeColumn.Length; row++)
{
if (rankByPhysical[offset + row] < 0) { continue; }

// Ineligible provider, absent field, and zero/undecodable code all contribute to no slot (omit), so "Other"
// holds only real failure codes beyond the top-N rather than the non-failure population.
if (eligible.BinarySearch(sourceColumn[row]) < 0) { continue; }

if (!TryGetSealedEventDataHResult(chunk, row, fieldName, fieldIndexBySchema, out long code)) { continue; }

int bucket = ToBucket(timeColumn[row], minTicks, bucketSpanTicks, bucketCount);
slotCounts[(bucket * slotCount) + SlotForCode(code, targetCodes, otherSlot)]++;
}

offset += chunk.RowCount;
}

if (_sealedCount >= Count) { return; }

IReadOnlySet<string> eligibleNames = AsOrdinalSet(eligibleProviders);

for (int index = _sealedCount; index < Count; index++)
{
if (rankByPhysical[index] < 0) { continue; }

ResolvedEvent pending = Pending(index);

if (!eligibleNames.Contains(pending.Source)) { continue; }

if (!TryGetPendingEventDataHResult(pending, fieldName, out long code)) { continue; }

int bucket = ToBucket(pending.TimeCreated.Ticks, minTicks, bucketSpanTicks, bucketCount);
slotCounts[(bucket * slotCount) + SlotForCode(code, targetCodes, otherSlot)]++;
}
}

internal void BucketTimeTicksByEventId(
ReadOnlySpan<int> rankByPhysical,
long minTicks,
Expand Down Expand Up @@ -597,6 +662,61 @@ internal void CopyTimeTicks(long[] values, bool[] hasValue)
}
}

internal void CountEventDataHResults(
ReadOnlySpan<int> rankByPhysical,
string fieldName,
IReadOnlyCollection<string> eligibleProviders,
IDictionary<long, int> counts,
CancellationToken cancellationToken)
{
Span<int> eligibleBuffer = eligibleProviders.Count <= MaxStackProviderNames
? stackalloc int[eligibleProviders.Count]
: new int[eligibleProviders.Count];

ReadOnlySpan<int> eligible = ResolveEligibleSourceIndices(eligibleProviders, eligibleBuffer);
int[] fieldIndexBySchema = NewSchemaFieldMemo();
int offset = 0;

foreach (EventColumnChunk chunk in _sealedChunks)
{
cancellationToken.ThrowIfCancellationRequested();

ReadOnlySpan<int> sourceColumn = chunk.PoolIndexColumn(EventColumnField.Source);

for (int row = 0; row < sourceColumn.Length; row++)
{
if (rankByPhysical[offset + row] < 0) { continue; }

if (eligible.BinarySearch(sourceColumn[row]) < 0) { continue; }

if (TryGetSealedEventDataHResult(chunk, row, fieldName, fieldIndexBySchema, out long code))
{
counts[code] = counts.TryGetValue(code, out int existing) ? existing + 1 : 1;
}
}

offset += chunk.RowCount;
}

if (_sealedCount >= Count) { return; }

IReadOnlySet<string> eligibleNames = AsOrdinalSet(eligibleProviders);

for (int index = _sealedCount; index < Count; index++)
{
if (rankByPhysical[index] < 0) { continue; }

ResolvedEvent pending = Pending(index);

if (!eligibleNames.Contains(pending.Source)) { continue; }

if (TryGetPendingEventDataHResult(pending, fieldName, out long code))
{
counts[code] = counts.TryGetValue(code, out int existing) ? existing + 1 : 1;
}
}
}

internal void CountEventDataValues(ReadOnlySpan<int> rankByPhysical, string fieldName, IDictionary<long, int> counts, CancellationToken cancellationToken)
{
int[] fieldIndexBySchema = NewSchemaFieldMemo();
Expand Down Expand Up @@ -1060,6 +1180,11 @@ internal bool TryGetTimeTicksRange(
internal EventColumnStore WithReloadGeneration(IReadOnlyList<ResolvedEvent> batch) =>
Build(batch, Generation + 1, ContentVersion + 1);

// A pending row's Source is a raw string (not yet pooled), so match it against a scan-local set built once per scan.
// Always normalize to Ordinal so pending matching stays byte-identical to the sealed pool's ordinal lookup, regardless
// of the caller collection's own comparer.
private static HashSet<string> AsOrdinalSet(IReadOnlyCollection<string> providers) => new(providers, StringComparer.Ordinal);

private static (EventColumnChunk Chunk, EventColumnPool Pool, ImmutableArray<int[]> Schemas) BuildChunk(
IReadOnlyList<ResolvedEvent> events,
int start,
Expand Down Expand Up @@ -1192,6 +1317,20 @@ private static bool TryGetPendingEventDataCode(ResolvedEvent pending, string fie
return false;
}

private static bool TryGetPendingEventDataHResult(ResolvedEvent pending, string fieldName, out long code)
{
if (pending.EventData.TryGetValue(fieldName, out EventFieldValue value) && value.TryGetHResult32(out uint hresult) && hresult != 0)
{
code = hresult;

return true;
}

code = 0;

return false;
}

private (EventColumnChunk Chunk, int Row) Locate(int index)
{
int[] prefix = SealedPrefix();
Expand Down Expand Up @@ -1323,6 +1462,23 @@ private ImmutableArray<UserDataField> ReconstructUserData(int index)
return fields.MoveToImmutable();
}

// The pool dedups, so each provider name resolves to at most one index; sort the resolved set for O(log n) per-row
// membership. Names absent from this store's pool are dropped (no row here can carry them).
private ReadOnlySpan<int> ResolveEligibleSourceIndices(IReadOnlyCollection<string> eligibleProviders, Span<int> buffer)
{
int count = 0;

foreach (string provider in eligibleProviders)
{
if (count < buffer.Length && _pool.TryGetIndex(provider, out int index)) { buffer[count++] = index; }
}

Span<int> resolved = buffer[..count];
resolved.Sort();

return resolved;
}

private int ResolveSchemaFieldIndex(int schemaId, string fieldName)
{
int[] nameIndices = _schemas[schemaId];
Expand Down Expand Up @@ -1352,6 +1508,61 @@ private int[] SealedPrefix()
return prefix;
}

private bool TryGetHResult32FromRawField(in RawEventDataField field, out long code)
{
switch (field.Kind)
{
case StoredFieldKind.SByte:
case StoredFieldKind.Int16:
case StoredFieldKind.Int32:
case StoredFieldKind.Int64:
// A win:HexInt32 failure code (high bit set) sign-extends to a negative Int64; reinterpret the low 32 bits.
if (field.Bits is >= int.MinValue and <= uint.MaxValue)
{
code = unchecked((uint)field.Bits);

return true;
}

code = 0;

return false;
case StoredFieldKind.Byte:
case StoredFieldKind.UInt16:
case StoredFieldKind.UInt32:
case StoredFieldKind.UInt64:
case StoredFieldKind.SizeT:
ulong unsigned = unchecked((ulong)field.Bits);

if (unsigned <= uint.MaxValue)
{
code = unchecked((uint)unsigned);

return true;
}
Comment thread
jschick04 marked this conversation as resolved.

code = 0;

return false;
case StoredFieldKind.String:
case StoredFieldKind.StringForm:
if (EventFieldValue.TryParseHResult32(PoolGet(field.RefIndex), out uint parsed))
{
code = parsed;

return true;
}

code = 0;

return false;
default:
code = 0;

return false;
}
}

private bool TryGetSealedEventDataCode(EventColumnChunk chunk, int row, string fieldName, int[] fieldIndexBySchema, out long code)
{
int schemaId = chunk.RowEventDataSchemaId(row);
Expand All @@ -1376,6 +1587,31 @@ private bool TryGetSealedEventDataCode(EventColumnChunk chunk, int row, string f
return TryGetWholeNumberFromRawField(chunk.RowEventDataField(row, fieldIndex), out code);
}

private bool TryGetSealedEventDataHResult(EventColumnChunk chunk, int row, string fieldName, int[] fieldIndexBySchema, out long code)
{
int schemaId = chunk.RowEventDataSchemaId(row);

if ((uint)schemaId >= (uint)fieldIndexBySchema.Length) { code = 0; return false; }

int fieldIndex = fieldIndexBySchema[schemaId];

if (fieldIndex == SchemaFieldUnresolved)
{
fieldIndex = ResolveSchemaFieldIndex(schemaId, fieldName);
fieldIndexBySchema[schemaId] = fieldIndex;
}

if (fieldIndex < 0 || fieldIndex >= chunk.RowEventDataCount(row))
{
code = 0;

return false;
}

// A zero code is success (S_OK); the ErrorCode dimension charts failures only, so it is dropped like an absent field.
return TryGetHResult32FromRawField(chunk.RowEventDataField(row, fieldIndex), out code) && code != 0;
}

private bool TryGetWholeNumberFromRawField(in RawEventDataField field, out long code)
{
switch (field.Kind)
Expand All @@ -1394,7 +1630,12 @@ private bool TryGetWholeNumberFromRawField(in RawEventDataField field, out long
case StoredFieldKind.SizeT:
ulong unsigned = unchecked((ulong)field.Bits);

if (unsigned <= long.MaxValue) { code = (long)unsigned; return true; }
if (unsigned <= long.MaxValue)
{
code = (long)unsigned;

return true;
}

code = 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,31 @@ public void BucketTimeTicksByEventData(
_store.BucketTimeTicksByEventData(rankByPhysical, minTicks, bucketSpanTicks, bucketCount, fieldName, targetCodes, slotCount, slotCounts, cancellationToken);
}

public void BucketTimeTicksByEventDataHResult(
ReadOnlySpan<int> rankByPhysical,
long minTicks,
long bucketSpanTicks,
int bucketCount,
string fieldName,
IReadOnlyCollection<string> eligibleProviders,
long[] targetCodes,
int[] slotCounts,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(fieldName);
ArgumentNullException.ThrowIfNull(eligibleProviders);
ArgumentNullException.ThrowIfNull(targetCodes);
ArgumentNullException.ThrowIfNull(slotCounts);
ArgumentOutOfRangeException.ThrowIfNotEqual(rankByPhysical.Length, Count);
ArgumentOutOfRangeException.ThrowIfLessThan(bucketSpanTicks, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(bucketCount, 1);

int slotCount = targetCodes.Length + 1;
ArgumentOutOfRangeException.ThrowIfLessThan(slotCounts.Length, bucketCount * slotCount);

_store.BucketTimeTicksByEventDataHResult(rankByPhysical, minTicks, bucketSpanTicks, bucketCount, fieldName, eligibleProviders, targetCodes, slotCount, slotCounts, cancellationToken);
}

public void BucketTimeTicksByEventId(
ReadOnlySpan<int> rankByPhysical,
long minTicks,
Expand Down Expand Up @@ -182,6 +207,21 @@ public void CopyPoolIndexColumn(EventFieldId field, int[] poolIndices)
}
}

public void CountEventDataHResults(
ReadOnlySpan<int> rankByPhysical,
string fieldName,
IReadOnlyCollection<string> eligibleProviders,
IDictionary<long, int> counts,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(fieldName);
ArgumentNullException.ThrowIfNull(eligibleProviders);
ArgumentNullException.ThrowIfNull(counts);
ArgumentOutOfRangeException.ThrowIfNotEqual(rankByPhysical.Length, Count);

_store.CountEventDataHResults(rankByPhysical, fieldName, eligibleProviders, counts, cancellationToken);
}

public void CountEventDataValues(ReadOnlySpan<int> rankByPhysical, string fieldName, IDictionary<long, int> counts, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(fieldName);
Expand Down
Loading
Loading