|
1 | 1 | using System; |
2 | 2 | using System.Collections.Generic; |
| 3 | +using System.IO; |
| 4 | +using System.IO.Compression; |
3 | 5 | using System.Linq; |
4 | 6 | using Microsoft.Diagnostics.Tracing; |
| 7 | +using Microsoft.Diagnostics.Tracing.Etlx; |
| 8 | +using Microsoft.Diagnostics.Tracing.EventPipe; |
5 | 9 | using Microsoft.Diagnostics.Tracing.Stacks; |
6 | 10 | using Xunit; |
7 | 11 |
|
@@ -143,6 +147,95 @@ public void ThreeLevelRecursiveCallsShowCorrectMetrics() |
143 | 147 | Assert.True(xCallee.InclusiveMetric > 0, $"Recursive callee 'X' should have positive metric, got {xCallee.InclusiveMetric}"); |
144 | 148 | } |
145 | 149 |
|
| 150 | + /// <summary> |
| 151 | + /// Regression test using MutableTraceEventStackSource to reproduce the exact issue scenario. |
| 152 | + /// This test uses an existing test nettrace file, converts it to a TraceLog, and then uses |
| 153 | + /// MutableTraceEventStackSource to add recursive frames, matching the original issue repro code. |
| 154 | + /// </summary> |
| 155 | + [Fact] |
| 156 | + public void RecursiveCallsWithMutableTraceEventStackSource() |
| 157 | + { |
| 158 | + // Use an existing test nettrace file |
| 159 | + string testDataDir = Path.Combine( |
| 160 | + Path.GetDirectoryName(typeof(RecursiveCallTest).Assembly.Location), |
| 161 | + "..", "..", "..", "inputs"); |
| 162 | + string zipFile = Path.Combine(testDataDir, "eventpipe-dotnetcore6.0-win-x64-executioncheckpoints.nettrace.zip"); |
| 163 | + |
| 164 | + // Skip test if file doesn't exist (CI environments may not have test data) |
| 165 | + if (!File.Exists(zipFile)) |
| 166 | + { |
| 167 | + return; // Skip test |
| 168 | + } |
| 169 | + |
| 170 | + string unzippedFile = Path.Combine(Path.GetTempPath(), $"test_recursive_{Guid.NewGuid()}.nettrace"); |
| 171 | + string tempEtlxFile = null; |
| 172 | + |
| 173 | + try |
| 174 | + { |
| 175 | + // Extract the nettrace file |
| 176 | + using (var archive = System.IO.Compression.ZipFile.OpenRead(zipFile)) |
| 177 | + { |
| 178 | + var entry = archive.Entries.First(e => e.Name.EndsWith(".nettrace")); |
| 179 | + entry.ExtractToFile(unzippedFile, true); |
| 180 | + } |
| 181 | + |
| 182 | + // Create TraceLog from nettrace |
| 183 | + tempEtlxFile = TraceLog.CreateFromEventPipeDataFile(unzippedFile, null, new TraceLogOptions() { ContinueOnError = true }); |
| 184 | + using (var traceLog = new TraceLog(tempEtlxFile)) |
| 185 | + { |
| 186 | + // Create MutableTraceEventStackSource and reproduce the exact issue scenario |
| 187 | + var stackSource = new MutableTraceEventStackSource(traceLog); |
| 188 | + |
| 189 | + // This reproduces the exact code from the issue: |
| 190 | + var sample = new StackSourceSample(stackSource); |
| 191 | + sample.StackIndex = stackSource.Interner.CallStackIntern( |
| 192 | + stackSource.Interner.FrameIntern("X"), sample.StackIndex); |
| 193 | + sample.StackIndex = stackSource.Interner.CallStackIntern( |
| 194 | + stackSource.Interner.FrameIntern("X"), sample.StackIndex); |
| 195 | + sample.TimeRelativeMSec = 1.0; |
| 196 | + sample.Metric = 1.0f; |
| 197 | + sample.Count = 1; |
| 198 | + stackSource.AddSample(sample); |
| 199 | + stackSource.DoneAddingSamples(); |
| 200 | + |
| 201 | + // Build CallTree and verify structure |
| 202 | + var callTree = new CallTree(ScalingPolicyKind.ScaleToData); |
| 203 | + callTree.StackSource = stackSource; |
| 204 | + |
| 205 | + var root = callTree.Root; |
| 206 | + Assert.NotNull(root); |
| 207 | + |
| 208 | + // Test CallerCalleeNode for "X" - this is the main test |
| 209 | + // The CallerCalleeNode should correctly identify recursive relationships |
| 210 | + var callerCalleeNode = new CallerCalleeNode("X", callTree); |
| 211 | + |
| 212 | + // The key assertions: X should appear as both caller and callee |
| 213 | + // This tests that the recursive call (X -> X) is properly represented |
| 214 | + var xCaller = callerCalleeNode.Callers.FirstOrDefault(c => c.Name == "X"); |
| 215 | + Assert.NotNull(xCaller); |
| 216 | + Assert.True(xCaller.InclusiveMetric > 0, |
| 217 | + $"Recursive caller 'X' should have positive metric, got {xCaller.InclusiveMetric}"); |
| 218 | + Assert.False(float.IsNaN(xCaller.InclusiveMetric), |
| 219 | + "Recursive caller 'X' has NaN inclusive metric"); |
| 220 | + |
| 221 | + var xCallee = callerCalleeNode.Callees.FirstOrDefault(c => c.Name == "X"); |
| 222 | + Assert.NotNull(xCallee); |
| 223 | + Assert.True(xCallee.InclusiveMetric > 0, |
| 224 | + $"Recursive callee 'X' should have positive metric, got {xCallee.InclusiveMetric}"); |
| 225 | + Assert.False(float.IsNaN(xCallee.InclusiveMetric), |
| 226 | + "Recursive callee 'X' has NaN inclusive metric"); |
| 227 | + } |
| 228 | + } |
| 229 | + finally |
| 230 | + { |
| 231 | + // Clean up temp files |
| 232 | + if (File.Exists(unzippedFile)) |
| 233 | + File.Delete(unzippedFile); |
| 234 | + if (tempEtlxFile != null && File.Exists(tempEtlxFile)) |
| 235 | + File.Delete(tempEtlxFile); |
| 236 | + } |
| 237 | + } |
| 238 | + |
146 | 239 | /// <summary> |
147 | 240 | /// Simple StackSource implementation for testing recursive call scenarios. |
148 | 241 | /// This minimal implementation allows creating call stacks with interned frames |
|
0 commit comments