@@ -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