using System.Collections.Immutable; using Microsoft.Extensions.Logging; using StellaOps.Scanner.Core; using StellaOps.Scanner.Reachability.Slices; namespace StellaOps.Scanner.Reachability.Runtime; /// /// Configuration for runtime-static graph merging. /// public sealed record RuntimeStaticMergeOptions { /// /// Confidence boost for edges observed at runtime. Default: 1.0 (max). /// public double ObservedConfidenceBoost { get; init; } = 1.0; /// /// Base confidence for runtime-only edges (not in static graph). Default: 0.9. /// public double RuntimeOnlyConfidence { get; init; } = 0.9; /// /// Minimum observation count to include a runtime-only edge. Default: 1. /// public int MinObservationCount { get; init; } = 1; /// /// Maximum age of observations to consider fresh. Default: 7 days. /// public TimeSpan FreshnessWindow { get; init; } = TimeSpan.FromDays(7); /// /// Whether to add edges from runtime that don't exist in static graph. /// public bool AddRuntimeOnlyEdges { get; init; } = true; } /// /// Result of merging runtime traces with static call graph. /// public sealed record RuntimeStaticMergeResult { /// /// Merged graph with runtime annotations. /// public required CallGraph MergedGraph { get; init; } /// /// Statistics about the merge operation. /// public required MergeStatistics Statistics { get; init; } /// /// Edges that were observed at runtime. /// public ImmutableArray ObservedEdges { get; init; } = ImmutableArray.Empty; /// /// Edges added from runtime that weren't in static graph. /// public ImmutableArray RuntimeOnlyEdges { get; init; } = ImmutableArray.Empty; } /// /// Statistics from the merge operation. /// 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; } /// /// An edge that was observed at runtime. /// 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; } } /// /// An edge that only exists in runtime observations (dynamic dispatch, etc). /// 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; } } /// /// Represents a runtime call event from eBPF/ETW collectors. /// 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; } } /// /// Merges runtime trace observations with static call graphs. /// public sealed class RuntimeStaticMerger { private readonly RuntimeStaticMergeOptions _options; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public RuntimeStaticMerger( RuntimeStaticMergeOptions? options = null, ILogger? logger = null, TimeProvider? timeProvider = null) { _options = options ?? new RuntimeStaticMergeOptions(); _logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; _timeProvider = timeProvider ?? TimeProvider.System; } /// /// Merge runtime events into a static call graph. /// public RuntimeStaticMergeResult Merge( CallGraph staticGraph, IEnumerable 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(); var runtimeOnlyEdges = new List(); var modifiedEdges = new List(); var matchedEdgeKeys = new HashSet(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(); 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 BuildStaticEdgeIndex(CallGraph graph) { var index = new Dictionary(StringComparer.Ordinal); foreach (var edge in graph.Edges) { var key = BuildEdgeKey(edge.From, edge.To); index.TryAdd(key, edge); } return index; } private static Dictionary AggregateRuntimeEvents( IEnumerable events) { var aggregates = new Dictionary(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; } } }