diff --git a/src/TraceEvent/EventPipe/EventCache.cs b/src/TraceEvent/EventPipe/EventCache.cs index eaaac5513..6492a41e5 100644 --- a/src/TraceEvent/EventPipe/EventCache.cs +++ b/src/TraceEvent/EventPipe/EventCache.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Runtime.InteropServices; namespace Microsoft.Diagnostics.Tracing.EventPipe @@ -68,6 +67,10 @@ public void ProcessEventBlock(Block block) } else { + if (thread.Events.Count == 0) + { + _activeThreadQueues.Add(thread.Events); + } thread.Events.Enqueue(eventMarker); } @@ -165,49 +168,178 @@ private void CheckForPendingThreadRemoval() private unsafe void SortAndDispatch(long stopTimestamp) { - // This sort could be made faster by using a min-heap but this is a simple place to start - List> threadQueues = new List>(_threads.Values.Select(t => t.Events)); - while(true) + // Build a min-heap from active thread queues (those with pending events) whose + // front event is before stopTimestamp. Using _activeThreadQueues avoids iterating + // all threads in the dictionary — only threads that have had events enqueued are checked. + // This gives O(N * log(T)) merge performance where N is the number of events and + // T is the number of active threads. + _heap.Clear(); + foreach (Queue q in _activeThreadQueues) { - long lowestTimestamp = stopTimestamp; - Queue oldestEventQueue = null; - foreach(Queue threadQueue in threadQueues) + if (q.Count > 0) { - if(threadQueue.Count == 0) + long ts = q.Peek().Header.TimeStamp; + if (ts < stopTimestamp) { - continue; + _heap.Add(ts, q); } - long eventTimestamp = threadQueue.Peek().Header.TimeStamp; - if (eventTimestamp < lowestTimestamp) + } + } + + if (_heap.Count == 0) + { + return; + } + + _heap.Build(); + + // Merge events in timestamp order using the min-heap. + while (_heap.Count > 0) + { + Queue minQueue = _heap.PeekValue; + EventMarker eventMarker = minQueue.Dequeue(); + OnEvent?.Invoke(ref eventMarker.Header); + + if (minQueue.Count > 0) + { + long nextTs = minQueue.Peek().Header.TimeStamp; + if (nextTs < stopTimestamp) { - oldestEventQueue = threadQueue; - lowestTimestamp = eventTimestamp; + // Update the root with the next timestamp and restore the heap property. + _heap.ReplaceRoot(nextTs, minQueue); + } + else + { + _heap.RemoveRoot(); } } - if(oldestEventQueue == null) + else { - break; + _heap.RemoveRoot(); + // Remove from active set and free internal storage to prevent unbounded + // memory growth when the application creates and destroys threads. + _activeThreadQueues.Remove(minQueue); + minQueue.TrimExcess(); + } + } + } + + #region Min-heap Implementation + + /// + /// A min-heap that pairs a long key with a value of type . + /// Entries are ordered by key so the minimum key is always at the root. + /// + internal class MinHeap + { + private struct Entry + { + public long Key; + public TValue Value; + + public Entry(long key, TValue value) + { + Key = key; + Value = value; + } + } + + private readonly List _entries = new List(); + + public int Count => _entries.Count; + + public TValue PeekValue => _entries[0].Value; + + public void Clear() => _entries.Clear(); + + public void Add(long key, TValue value) + { + _entries.Add(new Entry(key, value)); + } + + /// + /// Establishes the heap property over all entries. Call once after adding all + /// entries via Add, before extracting from the heap. + /// + /// + /// Starts from the last non-leaf node (_entries.Count / 2 - 1) and sifts each + /// node down to its correct position. Leaves (the second half of the array) are + /// already trivially valid heaps of size 1, so they are skipped. + /// + public void Build() + { + // Start from the last non-leaf node and work backwards to the root. + // Nodes at indices [Count/2 .. Count-1] are leaves that need no adjustment. + for (int i = _entries.Count / 2 - 1; i >= 0; i--) + { + SiftDown(i); + } + } + + /// + /// Replaces the root entry with a new key/value pair and restores the heap property. + /// Use when the root element has been consumed but its source still has more items. + /// + public void ReplaceRoot(long newKey, TValue value) + { + _entries[0] = new Entry(newKey, value); + SiftDown(0); + } + + /// + /// Removes the root (minimum) entry from the heap and restores the heap property. + /// + public void RemoveRoot() + { + int lastIndex = _entries.Count - 1; + if (lastIndex == 0) + { + _entries.Clear(); } else { - EventMarker eventMarker = oldestEventQueue.Dequeue(); - OnEvent?.Invoke(ref eventMarker.Header); + _entries[0] = _entries[lastIndex]; + _entries.RemoveAt(lastIndex); + SiftDown(0); } } - // If the app creates and destroys threads over time we need to flush old threads - // from the cache or memory usage will grow unbounded. AddThread handles the - // the thread objects but the storage for the queue elements also does not shrink - // below the high water mark unless we free it explicitly. - foreach (Queue q in threadQueues) + /// + /// Restores the min-heap property by moving the element at index i down the tree + /// until it is smaller than both children or reaches a leaf position. + /// + private void SiftDown(int i) { - if(q.Count == 0) + int count = _entries.Count; + while (true) { - q.TrimExcess(); + int smallest = i; + + // In a binary heap stored as an array, the children of node i are at + // indices 2i+1 (left) and 2i+2 (right). + int left = 2 * i + 1; + int right = 2 * i + 2; + + if (left < count && _entries[left].Key < _entries[smallest].Key) + { + smallest = left; + } + if (right < count && _entries[right].Key < _entries[smallest].Key) + { + smallest = right; + } + if (smallest == i) + { + break; + } + (_entries[i], _entries[smallest]) = (_entries[smallest], _entries[i]); + i = smallest; } } } + #endregion + private void FreeOldEventBuffers(long stopTimestamp) { while (_buffers.Count > 0) @@ -277,6 +409,8 @@ public EventBlockBuffer(FixedBuffer buffer, long maxTimestamp) EventPipeEventSource _source; ThreadCache _threads; Queue _buffers = new Queue(); + MinHeap> _heap = new MinHeap>(); + HashSet> _activeThreadQueues = new HashSet>(); } internal class EventMarker diff --git a/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs b/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs index 307da3edd..90a936ea5 100644 --- a/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs +++ b/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs @@ -355,7 +355,20 @@ public sealed class ProcessMappingMetadataTraceData : TraceEvent public string SymbolMetadata {get { return GetShortUTF8StringAt(SkipVarInt(0)); } } - internal ProcessMappingSymbolMetadata ParsedSymbolMetadata { get { return ProcessMappingSymbolMetadataParser.TryParse(SymbolMetadata); } } + internal ProcessMappingSymbolMetadata ParsedSymbolMetadata + { + get + { + if (!_parsedSymbolMetadataCached) + { + _parsedSymbolMetadata = ProcessMappingSymbolMetadataParser.TryParse(SymbolMetadata); + _parsedSymbolMetadataCached = true; + } + return _parsedSymbolMetadata; + } + } + private ProcessMappingSymbolMetadata _parsedSymbolMetadata; + private bool _parsedSymbolMetadataCached; public string VersionMetadata {get {return GetShortUTF8StringAt(SkipShortUTF8String(SkipVarInt(0))); } } @@ -367,9 +380,19 @@ internal ProcessMappingMetadataTraceData(Action } protected internal override void Dispatch() { + _parsedSymbolMetadataCached = false; + _parsedSymbolMetadata = null; Action(this); } + public override unsafe TraceEvent Clone() + { + var clone = (ProcessMappingMetadataTraceData)base.Clone(); + clone._parsedSymbolMetadata = _parsedSymbolMetadata; + clone._parsedSymbolMetadataCached = _parsedSymbolMetadataCached; + return clone; + } + protected internal override Delegate Target { get { return Action; } diff --git a/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs b/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs index 846043bf7..a34cd1107 100644 --- a/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs +++ b/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs @@ -62,7 +62,8 @@ public void BeforeProcess(TraceLog traceLog, TraceEventDispatcher source) } processes.Add(process); - if (!string.IsNullOrEmpty(data.FileName) && data.FileName.StartsWith(DotnetJittedCodeMappingName, StringComparison.Ordinal)) + string fileName = data.FileName; + if (!string.IsNullOrEmpty(fileName) && fileName.StartsWith(DotnetJittedCodeMappingName, StringComparison.Ordinal)) { // Don't create a module for jitted code. // These will be created for each jitted code symbol. @@ -70,7 +71,7 @@ public void BeforeProcess(TraceLog traceLog, TraceEventDispatcher source) } _mappingMetadata.TryGetValue(data.MetadataId, out ProcessMappingMetadataTraceData metadata); - TraceModuleFile moduleFile = process.LoadedModules.UniversalMapping(data, metadata); + TraceModuleFile moduleFile = process.LoadedModules.UniversalMapping(fileName, data.StartAddress, data.EndAddress, data.TimeStampQPC, metadata); }; universalSystemParser.ProcessMappingMetadata += delegate (ProcessMappingMetadataTraceData data) { diff --git a/src/TraceEvent/TraceEvent.Tests/Cache/MinHeapTests.cs b/src/TraceEvent/TraceEvent.Tests/Cache/MinHeapTests.cs new file mode 100644 index 000000000..34283d5a1 --- /dev/null +++ b/src/TraceEvent/TraceEvent.Tests/Cache/MinHeapTests.cs @@ -0,0 +1,230 @@ +using Microsoft.Diagnostics.Tracing.EventPipe; +using System; +using System.Collections.Generic; +using Xunit; + +namespace TraceEventTests +{ + public class MinHeapTests + { + [Fact] + public void EmptyHeap_CountIsZero() + { + var heap = new EventCache.MinHeap(); + Assert.Equal(0, heap.Count); + } + + [Fact] + public void SingleElement_PeekReturnsIt() + { + var heap = new EventCache.MinHeap(); + heap.Add(42, "only"); + heap.Build(); + + Assert.Equal(1, heap.Count); + Assert.Equal("only", heap.PeekValue); + } + + [Fact] + public void Build_EstablishesMinOrder() + { + var heap = new EventCache.MinHeap(); + heap.Add(30, "thirty"); + heap.Add(10, "ten"); + heap.Add(20, "twenty"); + heap.Build(); + + Assert.Equal("ten", heap.PeekValue); + } + + [Fact] + public void RemoveRoot_YieldsAscendingOrder() + { + var heap = new EventCache.MinHeap(); + heap.Add(50, 50); + heap.Add(30, 30); + heap.Add(40, 40); + heap.Add(10, 10); + heap.Add(20, 20); + heap.Build(); + + var result = new List(); + while (heap.Count > 0) + { + result.Add(heap.PeekValue); + heap.RemoveRoot(); + } + + Assert.Equal(new[] { 10, 20, 30, 40, 50 }, result); + } + + [Fact] + public void RemoveRoot_SingleElement_EmptiesHeap() + { + var heap = new EventCache.MinHeap(); + heap.Add(1, "a"); + heap.Build(); + + heap.RemoveRoot(); + Assert.Equal(0, heap.Count); + } + + [Fact] + public void ReplaceRoot_MaintainsHeapOrder() + { + var heap = new EventCache.MinHeap(); + heap.Add(10, "ten"); + heap.Add(20, "twenty"); + heap.Add(30, "thirty"); + heap.Build(); + + Assert.Equal("ten", heap.PeekValue); + + // Replace root (10) with a larger key (25) — "twenty" (20) should become new root. + heap.ReplaceRoot(25, "twenty-five"); + Assert.Equal("twenty", heap.PeekValue); + } + + [Fact] + public void ReplaceRoot_WithSmallestKey_KeepsItAtRoot() + { + var heap = new EventCache.MinHeap(); + heap.Add(10, "ten"); + heap.Add(20, "twenty"); + heap.Add(30, "thirty"); + heap.Build(); + + // Replace root with an even smaller key — it should remain the root. + heap.ReplaceRoot(5, "five"); + Assert.Equal("five", heap.PeekValue); + } + + [Fact] + public void Clear_ResetsHeap() + { + var heap = new EventCache.MinHeap(); + heap.Add(1, "a"); + heap.Add(2, "b"); + heap.Build(); + + heap.Clear(); + Assert.Equal(0, heap.Count); + } + + [Fact] + public void DuplicateKeys_AllElementsPreserved() + { + var heap = new EventCache.MinHeap(); + heap.Add(10, "a"); + heap.Add(10, "b"); + heap.Add(10, "c"); + heap.Build(); + + var result = new List(); + while (heap.Count > 0) + { + result.Add(heap.PeekValue); + heap.RemoveRoot(); + } + + Assert.Equal(3, result.Count); + result.Sort(); + Assert.Equal(new[] { "a", "b", "c" }, result); + } + + [Fact] + public void LargeHeap_ExtractsInOrder() + { + var heap = new EventCache.MinHeap(); + var rng = new Random(12345); + var expected = new List(); + + for (int i = 0; i < 1000; i++) + { + int val = rng.Next(0, 100000); + heap.Add(val, val); + expected.Add(val); + } + + heap.Build(); + expected.Sort(); + + var result = new List(); + while (heap.Count > 0) + { + result.Add(heap.PeekValue); + heap.RemoveRoot(); + } + + Assert.Equal(expected, result); + } + + [Fact] + public void AlreadySorted_ExtractsInOrder() + { + var heap = new EventCache.MinHeap(); + for (int i = 1; i <= 10; i++) + { + heap.Add(i, i); + } + heap.Build(); + + var result = new List(); + while (heap.Count > 0) + { + result.Add(heap.PeekValue); + heap.RemoveRoot(); + } + + Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, result); + } + + [Fact] + public void ReverseSorted_ExtractsInOrder() + { + var heap = new EventCache.MinHeap(); + for (int i = 10; i >= 1; i--) + { + heap.Add(i, i); + } + heap.Build(); + + var result = new List(); + while (heap.Count > 0) + { + result.Add(heap.PeekValue); + heap.RemoveRoot(); + } + + Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, result); + } + + [Fact] + public void MixedOperations_RemoveAndReplace() + { + var heap = new EventCache.MinHeap(); + heap.Add(10, "ten"); + heap.Add(20, "twenty"); + heap.Add(30, "thirty"); + heap.Add(40, "forty"); + heap.Build(); + + // Extract min (10), then replace root with 25. + Assert.Equal("ten", heap.PeekValue); + heap.RemoveRoot(); + Assert.Equal("twenty", heap.PeekValue); + heap.ReplaceRoot(25, "twenty-five"); + + // Now heap has: 25, 30, 40. Min should be 25. + Assert.Equal("twenty-five", heap.PeekValue); + + var result = new List(); + while (heap.Count > 0) + { + result.Add(heap.PeekValue); + heap.RemoveRoot(); + } + Assert.Equal(new[] { "twenty-five", "thirty", "forty" }, result); + } + } +}