Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Core;
|
||||
using StellaOps.Scanner.Reachability.Slices;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for runtime-static graph merging.
|
||||
/// </summary>
|
||||
public sealed record RuntimeStaticMergeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Confidence boost for edges observed at runtime. Default: 1.0 (max).
|
||||
/// </summary>
|
||||
public double ObservedConfidenceBoost { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Base confidence for runtime-only edges (not in static graph). Default: 0.9.
|
||||
/// </summary>
|
||||
public double RuntimeOnlyConfidence { get; init; } = 0.9;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum observation count to include a runtime-only edge. Default: 1.
|
||||
/// </summary>
|
||||
public int MinObservationCount { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum age of observations to consider fresh. Default: 7 days.
|
||||
/// </summary>
|
||||
public TimeSpan FreshnessWindow { get; init; } = TimeSpan.FromDays(7);
|
||||
|
||||
/// <summary>
|
||||
/// Whether to add edges from runtime that don't exist in static graph.
|
||||
/// </summary>
|
||||
public bool AddRuntimeOnlyEdges { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of merging runtime traces with static call graph.
|
||||
/// </summary>
|
||||
public sealed record RuntimeStaticMergeResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Merged graph with runtime annotations.
|
||||
/// </summary>
|
||||
public required CallGraph MergedGraph { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Statistics about the merge operation.
|
||||
/// </summary>
|
||||
public required MergeStatistics Statistics { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Edges that were observed at runtime.
|
||||
/// </summary>
|
||||
public ImmutableArray<ObservedEdge> ObservedEdges { get; init; } = ImmutableArray<ObservedEdge>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Edges added from runtime that weren't in static graph.
|
||||
/// </summary>
|
||||
public ImmutableArray<RuntimeOnlyEdge> RuntimeOnlyEdges { get; init; } = ImmutableArray<RuntimeOnlyEdge>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistics from the merge operation.
|
||||
/// </summary>
|
||||
public sealed record MergeStatistics
|
||||
{
|
||||
public int StaticEdgeCount { get; init; }
|
||||
public int RuntimeEventCount { get; init; }
|
||||
public int MatchedEdgeCount { get; init; }
|
||||
public int RuntimeOnlyEdgeCount { get; init; }
|
||||
public int UnmatchedStaticEdgeCount { get; init; }
|
||||
public double CoverageRatio => StaticEdgeCount > 0
|
||||
? (double)MatchedEdgeCount / StaticEdgeCount
|
||||
: 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An edge that was observed at runtime.
|
||||
/// </summary>
|
||||
public sealed record ObservedEdge
|
||||
{
|
||||
public required string From { get; init; }
|
||||
public required string To { get; init; }
|
||||
public required DateTimeOffset FirstObserved { get; init; }
|
||||
public required DateTimeOffset LastObserved { get; init; }
|
||||
public required int ObservationCount { get; init; }
|
||||
public string? TraceDigest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An edge that only exists in runtime observations (dynamic dispatch, etc).
|
||||
/// </summary>
|
||||
public sealed record RuntimeOnlyEdge
|
||||
{
|
||||
public required string From { get; init; }
|
||||
public required string To { get; init; }
|
||||
public required DateTimeOffset FirstObserved { get; init; }
|
||||
public required DateTimeOffset LastObserved { get; init; }
|
||||
public required int ObservationCount { get; init; }
|
||||
public required string Origin { get; init; } // "runtime", "dynamic_dispatch", etc.
|
||||
public string? TraceDigest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a runtime call event from eBPF/ETW collectors.
|
||||
/// </summary>
|
||||
public sealed record RuntimeCallEvent
|
||||
{
|
||||
public required ulong Timestamp { get; init; }
|
||||
public required uint Pid { get; init; }
|
||||
public required uint Tid { get; init; }
|
||||
public required string CallerSymbol { get; init; }
|
||||
public required string CalleeSymbol { get; init; }
|
||||
public required string BinaryPath { get; init; }
|
||||
public string? TraceDigest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges runtime trace observations with static call graphs.
|
||||
/// </summary>
|
||||
public sealed class RuntimeStaticMerger
|
||||
{
|
||||
private readonly RuntimeStaticMergeOptions _options;
|
||||
private readonly ILogger<RuntimeStaticMerger> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public RuntimeStaticMerger(
|
||||
RuntimeStaticMergeOptions? options = null,
|
||||
ILogger<RuntimeStaticMerger>? logger = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_options = options ?? new RuntimeStaticMergeOptions();
|
||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<RuntimeStaticMerger>.Instance;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge runtime events into a static call graph.
|
||||
/// </summary>
|
||||
public RuntimeStaticMergeResult Merge(
|
||||
CallGraph staticGraph,
|
||||
IEnumerable<RuntimeCallEvent> runtimeEvents)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(staticGraph);
|
||||
ArgumentNullException.ThrowIfNull(runtimeEvents);
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var freshnessThreshold = now - _options.FreshnessWindow;
|
||||
|
||||
// Index static edges for fast lookup
|
||||
var staticEdgeIndex = BuildStaticEdgeIndex(staticGraph);
|
||||
|
||||
// Aggregate runtime events by edge
|
||||
var runtimeEdgeAggregates = AggregateRuntimeEvents(runtimeEvents);
|
||||
|
||||
var observedEdges = new List<ObservedEdge>();
|
||||
var runtimeOnlyEdges = new List<RuntimeOnlyEdge>();
|
||||
var modifiedEdges = new List<CallEdge>();
|
||||
var matchedEdgeKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var (edgeKey, aggregate) in runtimeEdgeAggregates)
|
||||
{
|
||||
// Skip stale observations
|
||||
if (aggregate.LastObserved < freshnessThreshold)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip low observation counts
|
||||
if (aggregate.ObservationCount < _options.MinObservationCount)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (staticEdgeIndex.TryGetValue(edgeKey, out var staticEdge))
|
||||
{
|
||||
// Edge exists in static graph - mark as observed
|
||||
matchedEdgeKeys.Add(edgeKey);
|
||||
|
||||
var observedMetadata = new ObservedEdgeMetadata
|
||||
{
|
||||
FirstObserved = aggregate.FirstObserved,
|
||||
LastObserved = aggregate.LastObserved,
|
||||
ObservationCount = aggregate.ObservationCount,
|
||||
TraceDigest = aggregate.TraceDigest
|
||||
};
|
||||
|
||||
var boostedEdge = staticEdge with
|
||||
{
|
||||
Confidence = _options.ObservedConfidenceBoost,
|
||||
Observed = observedMetadata
|
||||
};
|
||||
|
||||
modifiedEdges.Add(boostedEdge);
|
||||
observedEdges.Add(new ObservedEdge
|
||||
{
|
||||
From = aggregate.From,
|
||||
To = aggregate.To,
|
||||
FirstObserved = aggregate.FirstObserved,
|
||||
LastObserved = aggregate.LastObserved,
|
||||
ObservationCount = aggregate.ObservationCount,
|
||||
TraceDigest = aggregate.TraceDigest
|
||||
});
|
||||
}
|
||||
else if (_options.AddRuntimeOnlyEdges)
|
||||
{
|
||||
// Edge only exists in runtime - add it
|
||||
var runtimeEdge = new CallEdge
|
||||
{
|
||||
From = aggregate.From,
|
||||
To = aggregate.To,
|
||||
Kind = CallEdgeKind.Dynamic,
|
||||
Confidence = ComputeRuntimeOnlyConfidence(aggregate),
|
||||
Evidence = "runtime_observation",
|
||||
Observed = new ObservedEdgeMetadata
|
||||
{
|
||||
FirstObserved = aggregate.FirstObserved,
|
||||
LastObserved = aggregate.LastObserved,
|
||||
ObservationCount = aggregate.ObservationCount,
|
||||
TraceDigest = aggregate.TraceDigest
|
||||
}
|
||||
};
|
||||
|
||||
modifiedEdges.Add(runtimeEdge);
|
||||
runtimeOnlyEdges.Add(new RuntimeOnlyEdge
|
||||
{
|
||||
From = aggregate.From,
|
||||
To = aggregate.To,
|
||||
FirstObserved = aggregate.FirstObserved,
|
||||
LastObserved = aggregate.LastObserved,
|
||||
ObservationCount = aggregate.ObservationCount,
|
||||
Origin = "runtime",
|
||||
TraceDigest = aggregate.TraceDigest
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build merged edge list: unmatched static + modified
|
||||
var mergedEdges = new List<CallEdge>();
|
||||
foreach (var edge in staticGraph.Edges)
|
||||
{
|
||||
var key = BuildEdgeKey(edge.From, edge.To);
|
||||
if (!matchedEdgeKeys.Contains(key))
|
||||
{
|
||||
mergedEdges.Add(edge);
|
||||
}
|
||||
}
|
||||
mergedEdges.AddRange(modifiedEdges);
|
||||
|
||||
var mergedGraph = staticGraph with
|
||||
{
|
||||
Edges = mergedEdges.ToImmutableArray()
|
||||
};
|
||||
|
||||
var statistics = new MergeStatistics
|
||||
{
|
||||
StaticEdgeCount = staticGraph.Edges.Length,
|
||||
RuntimeEventCount = runtimeEdgeAggregates.Count,
|
||||
MatchedEdgeCount = matchedEdgeKeys.Count,
|
||||
RuntimeOnlyEdgeCount = runtimeOnlyEdges.Count,
|
||||
UnmatchedStaticEdgeCount = staticGraph.Edges.Length - matchedEdgeKeys.Count
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"Merged runtime traces: {Matched}/{Static} edges observed ({Coverage:P1}), {RuntimeOnly} runtime-only edges added",
|
||||
statistics.MatchedEdgeCount,
|
||||
statistics.StaticEdgeCount,
|
||||
statistics.CoverageRatio,
|
||||
statistics.RuntimeOnlyEdgeCount);
|
||||
|
||||
return new RuntimeStaticMergeResult
|
||||
{
|
||||
MergedGraph = mergedGraph,
|
||||
Statistics = statistics,
|
||||
ObservedEdges = observedEdges.ToImmutableArray(),
|
||||
RuntimeOnlyEdges = runtimeOnlyEdges.ToImmutableArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, CallEdge> BuildStaticEdgeIndex(CallGraph graph)
|
||||
{
|
||||
var index = new Dictionary<string, CallEdge>(StringComparer.Ordinal);
|
||||
foreach (var edge in graph.Edges)
|
||||
{
|
||||
var key = BuildEdgeKey(edge.From, edge.To);
|
||||
index.TryAdd(key, edge);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private static Dictionary<string, RuntimeEdgeAggregate> AggregateRuntimeEvents(
|
||||
IEnumerable<RuntimeCallEvent> events)
|
||||
{
|
||||
var aggregates = new Dictionary<string, RuntimeEdgeAggregate>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var evt in events)
|
||||
{
|
||||
var key = BuildEdgeKey(evt.CallerSymbol, evt.CalleeSymbol);
|
||||
|
||||
if (aggregates.TryGetValue(key, out var existing))
|
||||
{
|
||||
aggregates[key] = existing with
|
||||
{
|
||||
ObservationCount = existing.ObservationCount + 1,
|
||||
LastObserved = DateTimeOffset.FromUnixTimeMilliseconds((long)(evt.Timestamp / 1_000_000))
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds((long)(evt.Timestamp / 1_000_000));
|
||||
aggregates[key] = new RuntimeEdgeAggregate
|
||||
{
|
||||
From = evt.CallerSymbol,
|
||||
To = evt.CalleeSymbol,
|
||||
FirstObserved = timestamp,
|
||||
LastObserved = timestamp,
|
||||
ObservationCount = 1,
|
||||
TraceDigest = evt.TraceDigest
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return aggregates;
|
||||
}
|
||||
|
||||
private double ComputeRuntimeOnlyConfidence(RuntimeEdgeAggregate aggregate)
|
||||
{
|
||||
// Higher observation count = higher confidence, capped at runtime-only max
|
||||
var countFactor = Math.Min(1.0, aggregate.ObservationCount / 10.0);
|
||||
return _options.RuntimeOnlyConfidence * (0.5 + 0.5 * countFactor);
|
||||
}
|
||||
|
||||
private static string BuildEdgeKey(string from, string to) => $"{from}->{to}";
|
||||
|
||||
private sealed record RuntimeEdgeAggregate
|
||||
{
|
||||
public required string From { get; init; }
|
||||
public required string To { get; init; }
|
||||
public required DateTimeOffset FirstObserved { get; init; }
|
||||
public required DateTimeOffset LastObserved { get; init; }
|
||||
public required int ObservationCount { get; init; }
|
||||
public string? TraceDigest { get; init; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user