Skip to content

Commit dd32a51

Browse files
authored
Fix missing weight due to JITted code (#41)
1 parent de1641f commit dd32a51

5 files changed

Lines changed: 555 additions & 3 deletions

File tree

src/ProfileExplorerCore/Profile/Data/RawProfileData.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public DotNetDebugInfoProvider GetOrAddManagedModuleDebugInfo(int processId, str
271271
}
272272

273273
public void LoadingCompleted() {
274-
// Free objects used while reading the profile.
274+
// Free objects used only during trace event processing.
275275
stacksMap_ = null;
276276
contextsMap_ = null;
277277
imagesMap_ = null;
@@ -283,6 +283,20 @@ public void LoadingCompleted() {
283283
perfCountersEvents_?.Wait();
284284
}
285285

286+
/// <summary>
287+
/// Adds an image directly to the images list and assigns it a sequential ID,
288+
/// bypassing deduplication. Safe to call after LoadingCompleted().
289+
/// </summary>
290+
public void AddImageDirect(ProfileImage image) {
291+
if (imagesMap_ != null) {
292+
throw new InvalidOperationException(
293+
"AddImageDirect can only be called after LoadingCompleted() has been invoked.");
294+
}
295+
296+
images_.Add(image);
297+
image.Id = images_.Count;
298+
}
299+
286300
public void ManagedLoadingCompleted() {
287301
if (procManagedDataMap_ != null) {
288302
foreach (var pair in procManagedDataMap_) {

src/ProfileExplorerCore/Profile/Data/RawProfileModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,8 @@ public ProfileThread(int threadId, int processId, string name) {
630630
public int ProcessId { get; set; }
631631
[ProtoMember(3)]
632632
public string Name { get; set; }
633+
[ProtoMember(4)]
634+
public long Win32StartAddr { get; set; }
633635
public bool HasName => !string.IsNullOrEmpty(Name);
634636

635637
public bool Equals(ProfileThread other) {

src/ProfileExplorerCore/Profile/ETW/ETWEventProcessor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ public RawProfileData ProcessEvents(ProfileLoadProgressHandler progressCallback,
395395
}
396396

397397
var thread = new ProfileThread(data.ThreadID, data.ProcessID, data.ThreadName);
398+
thread.Win32StartAddr = (long)data.Win32StartAddr;
398399
profile.AddThreadToProcess(data.ProcessID, thread);
399400
};
400401

src/ProfileExplorerCore/Profile/ETW/ETWProfileDataProvider.cs

Lines changed: 204 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,102 @@ public sealed class ETWProfileDataProvider : IProfileDataProvider, IDisposable {
5353
private HashSet<ProfileImage> rejectedDebugModules_;
5454
private int currentSampleIndex_;
5555

56+
// Synthetic module for samples whose instruction pointers don't map to any
57+
// known module loaded in the process. This can be dynamically generated code
58+
// (e.g., LUA JIT, Java JIT), code from unloaded modules, or other unmapped regions.
59+
private const string UnknownModuleName = "[Unknown Module]";
60+
61+
// Synthetic IP layout: [base bit48] [PID 16-bit] [TID 32-bit].
62+
// Chosen arbitrarily, just with constraint of being between
63+
// max user VA and IsKernelAddress threshold. See `IsKernelAddress`
64+
// in ETWEventProcessor.cs and https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces
65+
private const ulong SyntheticIpBase = 0x0001_0000_0000_0000UL;
66+
67+
// ProcessId (and ThreadId) is a DWORD (32-bit unsigned)
68+
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocessid
69+
// though with only 16 bits here, as a full 32-bit PID << 32 (in `MakeSyntheticIp`)
70+
// can flow into kernel addr range.
71+
private const ulong ProcessIdMask = 0xFFFFUL;
72+
73+
private sealed class UnknownModuleState(ProfileImage image, ProfileData profileData, int processId) {
74+
private readonly object lock_ = new();
75+
private readonly ConcurrentDictionary<int, (IRTextFunction Function, FunctionDebugInfo DebugInfo)> threadFunctions_ = new();
76+
private readonly ProfileData profileData_ = profileData;
77+
private readonly int processId_ = processId;
78+
private readonly ILoadedDocument document_ = LoadedDocument.CreateDummyDocument(UnknownModuleName);
79+
80+
public ProfileImage Image { get; } = image;
81+
public ILoadedDocument Document => document_;
82+
83+
/// <summary>
84+
/// Gets or creates a synthetic function for samples with unmapped IPs
85+
/// on a specific thread.
86+
/// </summary>
87+
public (IRTextFunction Function, FunctionDebugInfo DebugInfo) GetOrCreateThreadFunction(int threadId) {
88+
// Lock-free fast path for threads we've already seen
89+
(IRTextFunction Function, FunctionDebugInfo DebugInfo) existing;
90+
if (threadFunctions_.TryGetValue(threadId, out existing)) {
91+
return existing;
92+
}
93+
94+
// Serialize creation to ensure AddDummyFunction is called exactly once per threadId
95+
lock (lock_) {
96+
// Check again, in case another thread *just* created the entry since we checked previously
97+
if (threadFunctions_.TryGetValue(threadId, out existing)) {
98+
return existing;
99+
}
100+
101+
ProfileThread thread = profileData_.FindThread(threadId);
102+
string startModule = ResolveStartModule(thread);
103+
string funcName = FormatFuncName(threadId, thread?.Name, startModule);
104+
105+
// RVA = threadId so FunctionDebugInfo.IsUnknown (RVA==0 && Size==0) is false.
106+
// Id = processId_ so FunctionDebugInfo.Equals differentiates across processes
107+
// (Id is normally MethodToken for managed code, repurposed here for dedup keying).
108+
FunctionDebugInfo debugInfo = new FunctionDebugInfo(funcName, threadId, 1, id: processId_);
109+
IRTextFunction func = document_.AddDummyFunction(funcName);
110+
(IRTextFunction func, FunctionDebugInfo debugInfo) result = (func, debugInfo);
111+
threadFunctions_[threadId] = result;
112+
return result;
113+
}
114+
}
115+
116+
/// <summary>
117+
/// Resolves the module containing the thread's Win32 start address.
118+
/// </summary>
119+
private string ResolveStartModule(ProfileThread thread) {
120+
if (thread == null || thread.Win32StartAddr == 0) {
121+
return null;
122+
}
123+
124+
foreach (var module in profileData_.Modules.Values) {
125+
if (module.HasAddress(thread.Win32StartAddr)) {
126+
return module.ModuleName;
127+
}
128+
}
129+
130+
return null;
131+
}
132+
133+
/// <summary>
134+
/// Formats function as [JIT ThreadName (ThreadId) StartModule].
135+
/// </summary>
136+
private static string FormatFuncName(int threadId, string threadName, string startModule) {
137+
// Examples: [JIT WorkerThread (1234) lua51.dll], [JIT Thread 1234]
138+
if (!string.IsNullOrEmpty(threadName)) {
139+
return string.IsNullOrEmpty(startModule)
140+
? $"[JIT {threadName} ({threadId})]"
141+
: $"[JIT {threadName} ({threadId}) {startModule}]";
142+
}
143+
144+
return string.IsNullOrEmpty(startModule)
145+
? $"[JIT Thread {threadId}]"
146+
: $"[JIT Thread {threadId} {startModule}]";
147+
}
148+
}
149+
150+
private readonly ConcurrentDictionary<int, UnknownModuleState> unknownModules_ = new();
151+
56152
// Events for session lifecycle callbacks
57153
public event SetupNewSessionHandler SetupNewSessionRequested;
58154
public event StartNewSessionHandler StartNewSessionRequested;
@@ -199,6 +295,13 @@ public async Task<ProfileData> LoadTraceAsync(RawProfileData rawProfile, List<in
199295
Trace.WriteLine($"LoadTraceAsync: Adding {moduleCount} modules from raw profile");
200296
profileData_.AddModules(rawProfile.Images);
201297

298+
// Pre-create synthetic [Unknown Module] state for each profiled process.
299+
// This must happen before parallel sample processing begins, since
300+
// RawProfileData.AddImageDirect is not thread-safe.
301+
foreach (int procId in processIds) {
302+
PreCreateUnknownModule(rawProfile, procId);
303+
}
304+
202305
string imageName = Utilities.Utils.TryGetFileNameWithoutExtension(mainProcess.ImageFileName);
203306
Trace.WriteLine($"LoadTraceAsync: Main image name: {imageName}");
204307

@@ -540,6 +643,7 @@ private async Task<ResolvedProfileStack> ProcessUnresolvedStackAsync(ProfileStac
540643
int managedFrames = 0;
541644
int unknownFrames = 0;
542645
int resolvedFrames = 0;
646+
bool prevFrameWasUnknownJit = false;
543647

544648
//? TODO: Stacks with >256 frames are truncated, inclusive time computation is not right then
545649
//? for ex it never gets to main. Easy example is a quicksort impl
@@ -570,8 +674,44 @@ private async Task<ResolvedProfileStack> ProcessUnresolvedStackAsync(ProfileStac
570674

571675
if (frameImage == null) {
572676
unknownFrames++;
573-
resolvedStack.AddFrame(null, frameIp, 0, frameIndex, ResolvedProfileStackFrameKey.Unknown, stack,
574-
pointerSize);
677+
678+
// for case when some kernel address used without a named corresponding module,
679+
// we should not label it as JIT immediately
680+
if (ETWEventProcessor.IsKernelAddress((ulong)frameIp, pointerSize)) {
681+
resolvedStack.AddFrame(null, frameIp, 0, frameIndex,
682+
ResolvedProfileStackFrameKey.Unknown, stack, pointerSize);
683+
prevFrameWasUnknownJit = false;
684+
continue;
685+
}
686+
687+
// IP doesn't map to any known module (e.g., JITted). Attribute
688+
// to a synthetic per-thread JIT function, but collapse consecutive
689+
// unknown frames to avoid self-recursive chains in the call tree
690+
if (!prevFrameWasUnknownJit) {
691+
UnknownModuleState? unknownState = GetUnknownModule(context.ProcessId);
692+
693+
if (unknownState != null) {
694+
(IRTextFunction function, FunctionDebugInfo debugInfo) = unknownState.GetOrCreateThreadFunction(context.ThreadId);
695+
ResolvedProfileStackFrameKey unknownFrame = new ResolvedProfileStackFrameKey(debugInfo, unknownState.Image, false);
696+
697+
// Use a synthetic IP keyed by thread ID to prevent cache collisions
698+
// in ResolvedProfileStack.frameInstances_ when multiple threads
699+
// share the same unmapped IP
700+
long syntheticIp = MakeSyntheticIp(context.ProcessId, context.ThreadId);
701+
resolvedStack.AddFrame(function, syntheticIp, debugInfo.RVA,
702+
frameIndex, unknownFrame, stack, pointerSize);
703+
prevFrameWasUnknownJit = true;
704+
continue;
705+
}
706+
707+
// Fallback when no synthetic module state for this process
708+
resolvedStack.AddFrame(null, frameIp, 0, frameIndex,
709+
ResolvedProfileStackFrameKey.Unknown, stack, pointerSize);
710+
continue;
711+
}
712+
713+
// Consecutive unknown frame after a named JIT frame -- skip
714+
// to avoid self-recursive chains in the call tree.
575715
continue;
576716
}
577717
}
@@ -586,6 +726,7 @@ private async Task<ResolvedProfileStack> ProcessUnresolvedStackAsync(ProfileStac
586726
if (profileModuleBuilder == null) {
587727
unknownFrames++;
588728
resolvedStack.AddFrame(null, frameIp, 0, frameIndex, ResolvedProfileStackFrameKey.Unknown, stack, pointerSize);
729+
prevFrameWasUnknownJit = false;
589730
continue;
590731
}
591732

@@ -620,6 +761,7 @@ private async Task<ResolvedProfileStack> ProcessUnresolvedStackAsync(ProfileStac
620761
resolvedStack.AddFrame(funcPair.Function, frameIp, frameRva, frameIndex,
621762
resolvedFrame, stack, pointerSize);
622763
resolvedFrames++;
764+
prevFrameWasUnknownJit = false; // Known frame breaks unknown frame run.
623765
}
624766

625767
var totalTime = sw.Elapsed;
@@ -633,6 +775,55 @@ private async Task<ResolvedProfileStack> ProcessUnresolvedStackAsync(ProfileStac
633775
return resolvedStack;
634776
}
635777

778+
private ProfileImage unknownModuleImage_;
779+
780+
/// <summary>
781+
/// Pre-creates the synthetic [Unknown Module] for a process. Must be called
782+
/// before parallel sample processing begins.
783+
/// </summary>
784+
private void PreCreateUnknownModule(RawProfileData rawProfile, int processId) {
785+
// Share a single image across all processes to match how real modules
786+
// are deduplicated by AddImage.
787+
if (unknownModuleImage_ == null) {
788+
unknownModuleImage_ = new ProfileImage(
789+
filePath: UnknownModuleName,
790+
originalFileName: UnknownModuleName,
791+
baseAddress: 0,
792+
defaultBaseAddress: 0,
793+
size: 0,
794+
timeStamp: 0,
795+
checksum: 0);
796+
797+
// Use AddImageDirect since LoadingCompleted() has already freed imagesMap_.
798+
rawProfile.AddImageDirect(unknownModuleImage_);
799+
profileData_.Modules[unknownModuleImage_.Id] = unknownModuleImage_;
800+
}
801+
802+
var state = new UnknownModuleState(unknownModuleImage_, profileData_, processId);
803+
unknownModules_[processId] = state;
804+
805+
Trace.WriteLine($"Pre-created synthetic {UnknownModuleName} for process {processId}, ImageId={unknownModuleImage_.Id}");
806+
}
807+
808+
/// <summary>
809+
/// Gets the pre-created synthetic module state for a process.
810+
/// Returns null if PreCreateUnknownModule was not called for this process.
811+
/// </summary>
812+
private UnknownModuleState? GetUnknownModule(int processId) {
813+
return unknownModules_.GetValueOrDefault(processId);
814+
}
815+
816+
/// <summary>
817+
/// Creates a synthetic IP that is unique per (processId, threadId).
818+
/// Uses made up x64 address that shouldn't naturally be seen in traces,
819+
/// that stays below the IsKernelAddress threshold.
820+
/// </summary>
821+
private static long MakeSyntheticIp(int processId, int threadId) {
822+
ulong pid = (ulong)processId & ProcessIdMask;
823+
ulong tid = (uint)threadId;
824+
return unchecked((long)(SyntheticIpBase | (pid << 32) | tid));
825+
}
826+
636827
private ILoadedDocument FindSessionDocuments(string imageName, out List<ILoadedDocument> otherDocuments) {
637828
ILoadedDocument exeDocument = null;
638829
otherDocuments = new List<ILoadedDocument>();
@@ -664,6 +855,12 @@ private ILoadedDocument FindSessionDocuments(string imageName, out List<ILoadedD
664855
}
665856
}
666857

858+
// Include synthetic [Unknown Module] documents so the UI can
859+
// navigate to JIT functions via FindLoadedDocument.
860+
foreach (var state in unknownModules_.Values) {
861+
otherDocuments.Add(state.Document);
862+
}
863+
667864
return exeDocument;
668865
}
669866

@@ -1348,6 +1545,11 @@ private void ProcessPerformanceCounters(RawProfileData rawProfile, List<int> pro
13481545
managedBaseAddress = 1;
13491546
}
13501547
}
1548+
1549+
// If real module and managed method lookup both fail, attribute to synthetic "Unknown" to preserve counts
1550+
if (frameImage == null) {
1551+
frameImage = GetUnknownModule(context.ProcessId)?.Image;
1552+
}
13511553
}
13521554

13531555
if (frameImage != null) {

0 commit comments

Comments
 (0)