From 119b52666c9723ac2c09d679b2a2dc1a2dc31998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 29 Jun 2026 00:17:46 +0200 Subject: [PATCH 1/2] perf: add PropertyBag.FirstOrDefault() to eliminate array allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a zero-allocation FirstOrDefault() method to PropertyBag that walks the internal linked list directly, returning the first match without materialising a TProperty[] array. Previously, callers used: Properties.OfType().FirstOrDefault() PropertyBag.OfType() allocates a TProperty[] even for the common single-element case, and the subsequent LINQ .FirstOrDefault() iterates it. This results in a heap allocation per call that is immediately discarded. The new method: - Returns _testNodeStateProperty directly (O(1), zero alloc) when T is TestNodeStateProperty or a subtype - Walks the linked list with an early-exit on first match for all other types — no intermediate array, no throw on duplicates VideoRecorderSessionHandler had two call sites on the hot path (once per test state-change message): update.TestNode.Properties.OfType().FirstOrDefault() update.TestNode.Properties.OfType().FirstOrDefault() Both are updated to use Properties.FirstOrDefault() directly. Proxy metric: heap allocations eliminated per test update message. GSF principle: Hardware Efficiency — less GC pressure means the CPU spends fewer cycles on collection, reducing energy per functional unit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../VideoRecorderSessionHandler.cs | 4 +- .../Messages/PropertyBag.cs | 38 +++++++++++++++++++ .../PublicAPI/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net/PublicAPI.Unshipped.txt | 1 + 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs b/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs index 2b95051bf4..0e58553feb 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs @@ -125,7 +125,7 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo // A node carries a single state property in practice; use FirstOrDefault rather than // SingleOrDefault so a malformed producer can't throw out of the consumer pump. - TestNodeStateProperty? state = update.TestNode.Properties.OfType().FirstOrDefault(); + TestNodeStateProperty? state = update.TestNode.Properties.FirstOrDefault(); if (state is null) { return Task.CompletedTask; @@ -477,7 +477,7 @@ private static bool OverlapsAnyFailedWindow(VideoSegment segment, double[] faile private (DateTimeOffset Start, DateTimeOffset End) ResolveTiming(TestNodeUpdateMessage update, string testUid) { - TimingProperty? timing = update.TestNode.Properties.OfType().FirstOrDefault(); + TimingProperty? timing = update.TestNode.Properties.FirstOrDefault(); if (timing is not null) { return (timing.GlobalTiming.StartTime, timing.GlobalTiming.EndTime); diff --git a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs index 2027f78d76..6f5caa7ba0 100644 --- a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs +++ b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs @@ -213,6 +213,44 @@ public bool Any() return found; } + /// + /// Returns the first property of the type, or if none is found. + /// Unlike , this method does not throw when multiple properties of the + /// same type are present — it simply returns the first one encountered. + /// + /// The type of the property. + /// The first property of the given type, or if none is found. + public TProperty? FirstOrDefault() + where TProperty : IProperty + { + if (_testNodeStateProperty is TProperty testNodeStateProperty) + { + return testNodeStateProperty; + } + + // We don't want to walk the linked list if we know that we're looking for a TestNodeStateProperty. + if (typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty))) + { + return default; + } + + // Direct linked-list walk: avoids the array allocation from OfType() and the subsequent + // LINQ FirstOrDefault() call. Early-returns on the first match so no duplicate tracking + // is needed (unlike SingleOrDefault()). + Property? current = _property; + while (current is not null) + { + if (current.Current is TProperty match) + { + return match; + } + + current = current.Next; + } + + return default; + } + /// /// Returns the only property of the type, and throws an exception if there is not exactly one element. /// diff --git a/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt index 709927ceb1..6385a4e1fd 100644 --- a/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ #nullable enable [TPEXP]Microsoft.Testing.Platform.Extensions.IBlockingDataConsumer +Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault() -> TProperty? diff --git a/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt index 7dc5c58110..4017897f58 100644 --- a/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault() -> TProperty? From dea66abb776d83d7941f891e09c7a84cf76fb0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 29 Jun 2026 12:12:15 +0200 Subject: [PATCH 2/2] Fix RS0025 duplicate public API entry; align FirstOrDefault docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Testing.Platform/Messages/PropertyBag.cs | 4 ++-- .../PublicAPI/net/PublicAPI.Unshipped.txt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs index 6f5caa7ba0..be855f9bad 100644 --- a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs +++ b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs @@ -214,12 +214,12 @@ public bool Any() } /// - /// Returns the first property of the type, or if none is found. + /// Returns the first property of the type, or default if none is found. /// Unlike , this method does not throw when multiple properties of the /// same type are present — it simply returns the first one encountered. /// /// The type of the property. - /// The first property of the given type, or if none is found. + /// The first property of the given type, or default if none is found. public TProperty? FirstOrDefault() where TProperty : IProperty { diff --git a/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt index 4017897f58..7dc5c58110 100644 --- a/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,2 +1 @@ #nullable enable -Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault() -> TProperty?