77using System ;
88using System . Collections . Concurrent ;
99using System . Collections . Generic ;
10+ using System . Runtime . CompilerServices ;
1011using System . Threading ;
1112using System . Threading . Tasks ;
1213using Datadog . Trace . Agent . DiscoveryService ;
@@ -27,6 +28,13 @@ namespace Datadog.Trace.DataStreamsMonitoring;
2728/// </summary>
2829internal sealed class DataStreamsManager
2930{
31+ /// <summary>
32+ /// Maximum number of distinct keys stored in a single per-type edge-tag cache.
33+ /// When the limit is reached, new keys are computed on the fly without caching to
34+ /// prevent unbounded memory growth caused by high-cardinality identifiers.
35+ /// </summary>
36+ internal const int MaxEdgeTagCacheSize = 1000 ;
37+
3038 private static readonly IDatadogLogger Log = DatadogLogging . GetLoggerFor < DataStreamsManager > ( ) ;
3139 private static readonly AsyncLocal < PathwayContext ? > LastConsumePathway = new ( ) ; // saves the context on consume checkpointing only
3240 private readonly object _nodeHashUpdateLock = new ( ) ;
@@ -36,6 +44,7 @@ internal sealed class DataStreamsManager
3644 private readonly IDisposable _updateSubscription ;
3745 private readonly bool _isLegacyDsmHeadersEnabled ;
3846 private readonly bool _isInDefaultState ;
47+ private readonly ConditionalWeakTable < string [ ] , NodeHashCacheEntry > _nodeHashCache = new ( ) ;
3948 private long _nodeHashBase ; // note that this actually represents a `ulong` that we have done an unsafe cast for
4049 private MutableSettings _previousMutableSettings ;
4150 private string ? _previousContainerTagsHash ;
@@ -303,7 +312,24 @@ public void InjectPathwayContextAsBase64String<TCarrier>(PathwayContext? context
303312
304313 // Don't blame me, blame the fact we can't do Volatile.Read with a ulong in .NET FX...
305314 var nodeHashBase = new NodeHashBase ( unchecked ( ( ulong ) Volatile . Read ( ref _nodeHashBase ) ) ) ;
306- var nodeHash = HashHelper . CalculateNodeHash ( nodeHashBase , edgeTags ) ;
315+ var cacheEntry = _nodeHashCache . GetOrCreateValue ( edgeTags ) ;
316+ NodeHash nodeHash ;
317+
318+ // Fast lock-free path: snapshot is an immutable object published via a volatile field.
319+ // If the base still matches we avoid taking any lock on the hot path.
320+ if ( ! cacheEntry . TryGetNodeHash ( nodeHashBase , out nodeHash ) )
321+ {
322+ lock ( cacheEntry )
323+ {
324+ // Double-check under lock in case another thread raced to update
325+ if ( ! cacheEntry . TryGetNodeHash ( nodeHashBase , out nodeHash ) )
326+ {
327+ nodeHash = HashHelper . CalculateNodeHash ( nodeHashBase , edgeTags ) ;
328+ cacheEntry . Store ( nodeHashBase , nodeHash ) ;
329+ }
330+ }
331+ }
332+
307333 var parentHash = previousContext ? . Hash ?? default ;
308334 var pathwayHash = HashHelper . CalculatePathwayHash ( nodeHash , parentHash ) ;
309335
@@ -351,6 +377,34 @@ public void InjectPathwayContextAsBase64String<TCarrier>(PathwayContext? context
351377 }
352378 }
353379
380+ /// <summary>
381+ /// Returns a cached edge-tag array for the given key, creating and caching it on first use.
382+ /// On cache hits, zero heap allocations occur. The factory is only invoked on the first call
383+ /// per unique key, making this safe to use on high-throughput hot paths.
384+ /// Once the cache reaches <see cref="MaxEdgeTagCacheSize"/> entries the result is computed
385+ /// fresh each time (no caching) to bound memory usage for high-cardinality key spaces.
386+ /// </summary>
387+ /// <typeparam name="TKey">A value type (struct) used as the cache key — no boxing.</typeparam>
388+ /// <param name="key">The cache key derived from the caller's natural identifiers.</param>
389+ /// <param name="factory">A static factory that builds the edge-tag array from the key on cache miss.</param>
390+ public string [ ] GetOrCreateEdgeTags < TKey > ( TKey key , Func < TKey , string [ ] > factory )
391+ where TKey : notnull , IEquatable < TKey >
392+ {
393+ var cache = EdgeTagCache < TKey > . Cache ;
394+ if ( cache . TryGetValue ( key , out var existing ) )
395+ {
396+ return existing ;
397+ }
398+
399+ if ( cache . Count >= MaxEdgeTagCacheSize )
400+ {
401+ // High-cardinality key space — bypass cache to prevent unbounded memory growth
402+ return factory ( key ) ;
403+ }
404+
405+ return cache . GetOrAdd ( key , factory ) ;
406+ }
407+
354408 /// <summary>
355409 /// Make sure we only extract the schema (a costly operation) on select occasions
356410 /// </summary>
@@ -371,4 +425,58 @@ public bool ShouldExtractSchema(Span span, string operation, out int weight)
371425 weight = 0 ;
372426 return false ;
373427 }
428+
429+ /// <summary>
430+ /// Memoized NodeHash associated with a specific edge-tag array instance and nodeHashBase value.
431+ /// The volatile <see cref="_snapshot"/> field enables a lock-free fast path: callers read the
432+ /// snapshot without a lock, and only acquire the lock when the base has changed or is missing.
433+ /// </summary>
434+ private sealed class NodeHashCacheEntry
435+ {
436+ // Immutable snapshot published via volatile write; null until first computation.
437+ private volatile NodeHashSnapshot ? _snapshot ;
438+
439+ /// <summary>
440+ /// Tries to return the cached <see cref="NodeHash"/> for <paramref name="nodeHashBase"/>
441+ /// without acquiring any lock (lock-free read via volatile field).
442+ /// </summary>
443+ public bool TryGetNodeHash ( NodeHashBase nodeHashBase , out NodeHash nodeHash )
444+ {
445+ var snap = _snapshot ; // volatile read — acts as a load-acquire barrier
446+ if ( snap is not null && snap . Base == nodeHashBase . Value )
447+ {
448+ nodeHash = snap . Hash ;
449+ return true ;
450+ }
451+
452+ nodeHash = default ;
453+ return false ;
454+ }
455+
456+ /// <summary>
457+ /// Stores a newly-computed <see cref="NodeHash"/>. Must be called under a lock held by the caller.
458+ /// The volatile write ensures the snapshot is visible to all threads before the lock is released.
459+ /// </summary>
460+ public void Store ( NodeHashBase nodeHashBase , NodeHash nodeHash )
461+ {
462+ _snapshot = new NodeHashSnapshot ( nodeHashBase . Value , nodeHash ) ; // volatile write
463+ }
464+
465+ /// <summary>Immutable payload published atomically via the volatile <see cref="_snapshot"/> field.</summary>
466+ private sealed class NodeHashSnapshot
467+ {
468+ private readonly ulong _base ;
469+ private readonly NodeHash _hash ;
470+
471+ internal NodeHashSnapshot ( ulong @base , NodeHash hash )
472+ {
473+ _base = @base ;
474+ _hash = hash ;
475+ }
476+
477+ internal ulong Base => _base ;
478+
479+ internal NodeHash Hash => _hash ;
480+ }
481+ }
374482}
0 commit comments