Skip to content

Commit 6a37f25

Browse files
authored
Merge pull request #70 from microsoft/user/trgibeau/bugFix3
Fix issue with same module names
2 parents 13ec35c + 6a78d5e commit 6a37f25

12 files changed

Lines changed: 261 additions & 52 deletions

src/ProfileExplorer.Profiling.Tests/Integration/FunctionProfilerEndToEndTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void FullPipeline_PdbAndDll_ProducesAnnotatedAssembly() {
7575
// We'll test the lower-level components directly since we have local files.
7676
var ipResolver = new Profiling.IpResolver();
7777
ipResolver.AddImage(TestDataHelper.MsoModuleName, moduleBase, 0x1000000);
78-
ipResolver.SetFunctions(TestDataHelper.MsoModuleName, allFunctions);
78+
ipResolver.SetFunctions(moduleBase, allFunctions);
7979

8080
var aggregator = new Profiling.SampleAggregator(ipResolver);
8181
aggregator.AddSamples(samples);
@@ -183,7 +183,7 @@ public void MultipleModules_IndependentProfiles() {
183183

184184
var ipResolver = new Profiling.IpResolver();
185185
ipResolver.AddImage(TestDataHelper.MsoModuleName, moduleBase, 0x1000000);
186-
ipResolver.SetFunctions(TestDataHelper.MsoModuleName, functions);
186+
ipResolver.SetFunctions(moduleBase, functions);
187187

188188
var aggregator = new Profiling.SampleAggregator(ipResolver);
189189
aggregator.AddSamples([
@@ -290,7 +290,7 @@ private static (ProfileFunctionId FuncId, FunctionProfileData? Profile, int DllT
290290

291291
var ipResolver = new Profiling.IpResolver();
292292
ipResolver.AddImage(TestDataHelper.MsoModuleName, LocalBinaryModuleBase, 0x1000000);
293-
ipResolver.SetFunctions(TestDataHelper.MsoModuleName, functions);
293+
ipResolver.SetFunctions(LocalBinaryModuleBase, functions);
294294

295295
var aggregator = new Profiling.SampleAggregator(ipResolver);
296296
var samples = new List<IProfileSample>();

src/ProfileExplorer.Profiling.Tests/Integration/IpResolverRealProviderTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ private static IpResolver BuildResolver(PdbSymbolProvider provider, List<Functio
4848
int size = (int)Math.Min(maxEndRva + 0x10000, int.MaxValue);
4949
var resolver = new IpResolver();
5050
resolver.AddImage(TestDataHelper.MsoModuleName, ModuleBase, size);
51-
resolver.SetFunctions(TestDataHelper.MsoModuleName, functions, withProvider ? provider : null);
51+
resolver.SetFunctions(ModuleBase, functions, withProvider ? provider : null);
5252
return resolver;
5353
}
5454

src/ProfileExplorer.Profiling.Tests/Unit/AggregatorSemanticsCharacterizationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void CallerInstructionAttribution_LibraryVsCore_Matches() {
5353
// ---- LIBRARY: real SampleAggregator over a leaf-first stack [leaf, caller-return] ----
5454
var ipResolver = new IpResolver();
5555
ipResolver.AddImage(Module, ModuleBase, ModuleSize);
56-
ipResolver.SetFunctions(Module, new List<FunctionDebugInfo> {
56+
ipResolver.SetFunctions(ModuleBase, new List<FunctionDebugInfo> {
5757
new FunctionDebugInfo("Leaf", LeafRva, 0x100),
5858
new FunctionDebugInfo("Caller", CallerRva, 0x100)
5959
});

src/ProfileExplorer.Profiling.Tests/Unit/CallTreeBuilderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ public class CallTreeBuilderTests {
1818
resolver.AddImage(module, moduleBase, moduleSize);
1919
}
2020

21-
var grouped = functions.GroupBy(f => f.module);
21+
var grouped = functions.GroupBy(f => (f.module, f.moduleBase));
2222
foreach (var group in grouped) {
2323
var funcList = group.Select(f => new FunctionDebugInfo(f.funcName, f.funcRva, f.funcSize)).ToList();
24-
resolver.SetFunctions(group.Key, funcList);
24+
resolver.SetFunctions(group.Key.moduleBase, funcList);
2525
}
2626

2727
return (resolver, new CallTreeBuilder(resolver));

src/ProfileExplorer.Profiling.Tests/Unit/CounterAggregatorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ private class TestCounterEvent : IPerformanceCounterEvent {
2222
private (IpResolver resolver, CounterAggregator aggregator) Setup() {
2323
var resolver = new IpResolver();
2424
resolver.AddImage("test.dll", 0x1000, 0x10000);
25-
resolver.SetFunctions("test.dll", [
25+
resolver.SetFunctions(0x1000, [
2626
new FunctionDebugInfo("HotFunc", 0x100, 0x50)
2727
]);
2828
return (resolver, new CounterAggregator(resolver));

src/ProfileExplorer.Profiling.Tests/Unit/FunctionProfilerInjectionTests.cs

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void AddResolvedFunctions_ResolvesAndAggregates_WithoutSymbolDownload() {
4242
using var profiler = new FunctionProfiler(options, new NoSymbolLocator());
4343

4444
profiler.AddImages(SyntheticSampleBuilder.CreateImages((module, baseAddr, size)));
45-
profiler.AddResolvedFunctions(module, new List<FunctionDebugInfo> {
45+
profiler.AddResolvedFunctions(module, baseAddr, new List<FunctionDebugInfo> {
4646
new("Main", 0x1000, 0x800),
4747
new("Foo", 0x2000, 0x800),
4848
new("Bar", 0x3000, 0x800)
@@ -77,14 +77,116 @@ public void AddResolvedFunctions_ResolvesAndAggregates_WithoutSymbolDownload() {
7777
}
7878

7979
[TestMethod]
80-
public void AddResolvedFunctions_NullArguments_Throw() {
80+
public void AddResolvedFunctions_InvalidArguments_Throw() {
81+
const string module = "app.dll";
82+
const long baseAddr = 0x140000000;
83+
const int size = 0x100000;
8184
var options = new ProfilerOptions { SymbolPaths = new[] { "srv*https://symbols.invalid" } };
8285
using var profiler = new FunctionProfiler(options, new NoSymbolLocator());
86+
profiler.AddImages(SyntheticSampleBuilder.CreateImages((module, baseAddr, size)));
8387

84-
Assert.ThrowsException<ArgumentException>(() =>
85-
profiler.AddResolvedFunctions("", new List<FunctionDebugInfo>()));
88+
// Null function list.
8689
Assert.ThrowsException<ArgumentNullException>(() =>
87-
profiler.AddResolvedFunctions("m.dll", null!));
90+
profiler.AddResolvedFunctions(module, baseAddr, null!));
91+
// Empty module name.
92+
Assert.ThrowsException<ArgumentException>(() =>
93+
profiler.AddResolvedFunctions("", baseAddr, new List<FunctionDebugInfo>()));
94+
// Base address was never registered via AddImages.
95+
Assert.ThrowsException<ArgumentException>(() =>
96+
profiler.AddResolvedFunctions(module, 0xDEADBEEF, new List<FunctionDebugInfo>()));
97+
// Module name does not match the image registered at that base (wrong-module guard).
98+
Assert.ThrowsException<ArgumentException>(() =>
99+
profiler.AddResolvedFunctions("wrong.dll", baseAddr, new List<FunctionDebugInfo>()));
100+
}
101+
102+
[TestMethod]
103+
public void AddResolvedFunctions_ModuleNameCaseMismatch_Throws() {
104+
const string module = "App.DLL";
105+
const long baseAddr = 0x140000000;
106+
const int size = 0x100000;
107+
var options = new ProfilerOptions {
108+
SymbolPaths = new[] { "srv*https://symbols.invalid" },
109+
IncludeManagedCode = false,
110+
IncludePerformanceCounters = false
111+
};
112+
using var profiler = new FunctionProfiler(options, new NoSymbolLocator());
113+
profiler.AddImages(SyntheticSampleBuilder.CreateImages((module, baseAddr, size)));
114+
115+
// Module identity is case-sensitive (Ordinal) throughout the library: a case-only difference denotes
116+
// a different binary (the WinUI vs UWP xaml pair), so the wrong-module guard rejects it.
117+
Assert.ThrowsException<ArgumentException>(() =>
118+
profiler.AddResolvedFunctions("app.dll", baseAddr,
119+
new List<FunctionDebugInfo> { new("Foo", 0x1000, 0x800) }));
120+
}
121+
122+
[TestMethod]
123+
public void AddImages_DuplicateSameImage_IsIdempotent() {
124+
const string module = "app.dll";
125+
const long baseAddr = 0x140000000;
126+
const int size = 0x100000;
127+
var options = new ProfilerOptions {
128+
SymbolPaths = new[] { "srv*https://symbols.invalid" },
129+
IncludeManagedCode = false,
130+
IncludePerformanceCounters = false
131+
};
132+
using var profiler = new FunctionProfiler(options, new NoSymbolLocator());
133+
134+
// The same image reported twice (e.g. an ImageDCStart rundown followed by its ImageLoad) is
135+
// deduped, matching old Core — resolution still works normally afterward.
136+
profiler.AddImages(SyntheticSampleBuilder.CreateImages((module, baseAddr, size)));
137+
profiler.AddImages(SyntheticSampleBuilder.CreateImages((module, baseAddr, size)));
138+
139+
profiler.AddResolvedFunctions(module, baseAddr,
140+
new List<FunctionDebugInfo> { new("Foo", 0x1000, 0x800) });
141+
profiler.AddSamples(new IProfileSample[] {
142+
new SyntheticSample(baseAddr + 0x1000 + 0x10, TimeSpan.FromMilliseconds(5), 1, 1, module, baseAddr)
143+
});
144+
145+
var report = profiler.GetReport();
146+
Assert.AreEqual(TimeSpan.FromMilliseconds(5),
147+
report.Functions[new ProfileFunctionId(module, "Foo")].ExclusiveWeight);
148+
}
149+
150+
[TestMethod]
151+
public void AddImages_SameBaseDifferentImage_ThrowsByDefault() {
152+
const long baseAddr = 0x140000000;
153+
const int size = 0x100000;
154+
var options = new ProfilerOptions {
155+
SymbolPaths = new[] { "srv*https://symbols.invalid" },
156+
IncludeManagedCode = false,
157+
IncludePerformanceCounters = false
158+
};
159+
using var profiler = new FunctionProfiler(options, new NoSymbolLocator());
160+
161+
// Two DIFFERENT binaries at the same base means the caller didn't de-dupe per sampling window;
162+
// silently keeping one would mis-attribute the other's samples, so this throws by default.
163+
profiler.AddImages(SyntheticSampleBuilder.CreateImages(("first.dll", baseAddr, size)));
164+
Assert.ThrowsException<InvalidOperationException>(() =>
165+
profiler.AddImages(SyntheticSampleBuilder.CreateImages(("second.dll", baseAddr, size))));
166+
}
167+
168+
[TestMethod]
169+
public void AddImages_SameBaseDifferentImage_LenientMode_LastWins() {
170+
const long baseAddr = 0x140000000;
171+
const int size = 0x100000;
172+
var options = new ProfilerOptions {
173+
SymbolPaths = new[] { "srv*https://symbols.invalid" },
174+
IncludeManagedCode = false,
175+
IncludePerformanceCounters = false,
176+
ThrowOnImageBaseCollision = false
177+
};
178+
using var profiler = new FunctionProfiler(options, new NoSymbolLocator());
179+
180+
// Opt-in lenient mode: the base-keyed resolver can hold only one, so the latest registration wins
181+
// (warned, non-fatal). Attaching functions to the displaced image is then rejected by the guard.
182+
profiler.AddImages(SyntheticSampleBuilder.CreateImages(("first.dll", baseAddr, size)));
183+
profiler.AddImages(SyntheticSampleBuilder.CreateImages(("second.dll", baseAddr, size)));
184+
185+
profiler.AddResolvedFunctions("second.dll", baseAddr,
186+
new List<FunctionDebugInfo> { new("Bar", 0x1000, 0x800) });
187+
Assert.ThrowsException<ArgumentException>(() =>
188+
profiler.AddResolvedFunctions("first.dll", baseAddr,
189+
new List<FunctionDebugInfo> { new("Foo", 0x1000, 0x800) }));
88190
}
89191

90192
[TestMethod]
@@ -101,7 +203,7 @@ public void AddSamples_WithInstancePath_FocusesOnMatchingStacks() {
101203

102204
using var profiler = new FunctionProfiler(options, new NoSymbolLocator());
103205
profiler.AddImages(SyntheticSampleBuilder.CreateImages((module, baseAddr, size)));
104-
profiler.AddResolvedFunctions(module, new List<FunctionDebugInfo> {
206+
profiler.AddResolvedFunctions(module, baseAddr, new List<FunctionDebugInfo> {
105207
new("Main", 0x1000, 0x800),
106208
new("Foo", 0x2000, 0x800),
107209
new("Bar", 0x3000, 0x800),

src/ProfileExplorer.Profiling.Tests/Unit/IpResolverProviderTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ private static IpResolver CreateResolver(ISymbolDebugInfo? provider,
6060
resolver.AddImage("mod.dll", ModuleBase, ModuleSize);
6161
// The contiguous list would resolve SampleIp to listFuncName; a registered provider must win.
6262
var functions = new List<FunctionDebugInfo> { new(listFuncName, FuncRva, FuncSize) };
63-
resolver.SetFunctions("mod.dll", functions, provider);
63+
resolver.SetFunctions(ModuleBase, functions, provider);
6464
return resolver;
6565
}
6666

@@ -145,7 +145,7 @@ public void SplitChunkAddress_MissingFromContiguousList_ResolvesToParentViaProvi
145145
// Parent's primary chunk is the only entry in the contiguous list; the cold chunk is absent.
146146
var withProvider = new IpResolver();
147147
withProvider.AddImage("mod.dll", ModuleBase, ModuleSize);
148-
withProvider.SetFunctions("mod.dll", new List<FunctionDebugInfo> { parentFunc }, stub);
148+
withProvider.SetFunctions(ModuleBase, new List<FunctionDebugInfo> { parentFunc }, stub);
149149

150150
var resolved = withProvider.Resolve(ColdChunkIp);
151151
Assert.IsNotNull(resolved);
@@ -157,7 +157,7 @@ public void SplitChunkAddress_MissingFromContiguousList_ResolvesToParentViaProvi
157157
// the exact regression the provider path fixes.
158158
var listOnly = new IpResolver();
159159
listOnly.AddImage("mod.dll", ModuleBase, ModuleSize);
160-
listOnly.SetFunctions("mod.dll", new List<FunctionDebugInfo> { parentFunc });
160+
listOnly.SetFunctions(ModuleBase, new List<FunctionDebugInfo> { parentFunc });
161161

162162
var listOnlyResolved = listOnly.Resolve(ColdChunkIp);
163163
Assert.IsNotNull(listOnlyResolved);

src/ProfileExplorer.Profiling.Tests/Unit/SampleAggregatorTests.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ private IpResolver CreateResolverWithFunction(string module, long moduleBase, in
1818
var functions = new List<FunctionDebugInfo> {
1919
new(funcName, funcRva, funcSize)
2020
};
21-
resolver.SetFunctions(module, functions);
21+
resolver.SetFunctions(moduleBase, functions);
2222
return resolver;
2323
}
2424

@@ -54,7 +54,7 @@ public void MultipleSamples_SameFunction_AggregatesWeight() {
5454
public void MultipleSamples_DifferentFunctions_SeparateProfiles() {
5555
var resolver = new IpResolver();
5656
resolver.AddImage("test.dll", 0x1000, 0x10000);
57-
resolver.SetFunctions("test.dll", [
57+
resolver.SetFunctions(0x1000, [
5858
new FunctionDebugInfo("Foo", 0x100, 0x50),
5959
new FunctionDebugInfo("Bar", 0x200, 0x50),
6060
new FunctionDebugInfo("Baz", 0x300, 0x50)
@@ -72,6 +72,29 @@ public void MultipleSamples_DifferentFunctions_SeparateProfiles() {
7272
Assert.AreEqual(3, profiles.Count);
7373
}
7474

75+
[TestMethod]
76+
public void SameFileName_DifferentBases_ResolveIndependently() {
77+
// Two different binaries both named "foo.dll" at different bases, with DIFFERENT function layouts.
78+
// Each base's samples must resolve to that binary's OWN function (symbols are keyed by base), not
79+
// both resolve through whichever function list registered last for the shared name.
80+
var resolver = new IpResolver();
81+
resolver.AddImage("foo.dll", 0x10000, 0x1000);
82+
resolver.AddImage("foo.dll", 0x20000, 0x1000);
83+
resolver.SetFunctions(0x10000, [new FunctionDebugInfo("Alpha", 0x100, 0x50)]);
84+
resolver.SetFunctions(0x20000, [new FunctionDebugInfo("Beta", 0x100, 0x50)]);
85+
86+
var aggregator = new SampleAggregator(resolver);
87+
var w = TimeSpan.FromMilliseconds(1);
88+
aggregator.AddSamples([
89+
new SyntheticSample(0x10110, w, 1, 1, "foo.dll", 0x10000), // base 0x10000 -> Alpha
90+
new SyntheticSample(0x20110, w, 1, 1, "foo.dll", 0x20000) // base 0x20000 -> Beta
91+
]);
92+
93+
var names = aggregator.Build().Select(p => p.Key.FunctionName).OrderBy(n => n).ToArray();
94+
CollectionAssert.AreEqual(new[] { "Alpha", "Beta" }, names,
95+
"Each same-named binary's samples must resolve to its own function, not merge.");
96+
}
97+
7598
[TestMethod]
7699
public void InstructionWeights_AggregatesPerOffset() {
77100
var resolver = CreateResolverWithFunction("test.dll", 0x1000, 0x10000, "Foo", 0x100, 0x50);
@@ -198,7 +221,7 @@ public void UserImagelessLeaf_ResolvedCallersStillCreditedInclusive() {
198221
// receive inclusive weight (the leaf's `continue` used to drop the whole caller walk).
199222
var resolver = new IpResolver();
200223
resolver.AddImage("test.dll", 0x1000, 0x10000);
201-
resolver.SetFunctions("test.dll", [new FunctionDebugInfo("Bar", 0x200, 0x50)]);
224+
resolver.SetFunctions(0x1000, [new FunctionDebugInfo("Bar", 0x200, 0x50)]);
202225

203226
var aggregator = new SampleAggregator(resolver);
204227
var weight = TimeSpan.FromMilliseconds(1);
@@ -227,7 +250,7 @@ public void KernelImagelessLeaf_ResolvedCallersStillCreditedInclusive() {
227250
// get inclusive weight. Only the leaf is dropped — not the whole stack.
228251
var resolver = new IpResolver();
229252
resolver.AddImage("test.dll", 0x1000, 0x10000);
230-
resolver.SetFunctions("test.dll", [new FunctionDebugInfo("Bar", 0x200, 0x50)]);
253+
resolver.SetFunctions(0x1000, [new FunctionDebugInfo("Bar", 0x200, 0x50)]);
231254

232255
var aggregator = new SampleAggregator(resolver);
233256
var weight = TimeSpan.FromMilliseconds(1);
@@ -251,7 +274,7 @@ public void KernelImagelessLeaf_ResolvedCallersStillCreditedInclusive() {
251274
public void PercentCalculation_RelativeToTotalWeight() {
252275
var resolver = new IpResolver();
253276
resolver.AddImage("test.dll", 0x1000, 0x10000);
254-
resolver.SetFunctions("test.dll", [
277+
resolver.SetFunctions(0x1000, [
255278
new FunctionDebugInfo("Foo", 0x100, 0x50),
256279
new FunctionDebugInfo("Bar", 0x200, 0x50)
257280
]);

0 commit comments

Comments
 (0)