static unsafe void ReadEtlFile(string etlPath, IEnumerable <string> pdbWhitelist, out ProfilerTrace profTrace, Allocator allocator) { var monoFunctions = new Dictionary <IntPtr, MonoJitInfo>(); var discoveredModules = DiscoveredData <DiscoveredModule> .Make(DiscoveredModule.Invalid); var discoveredStackFrames = DiscoveredData <CallStackIndex> .Make(CallStackIndex.Invalid); var discoveredFunctions = DiscoveredData <DiscoveredFunction> .Make(DiscoveredFunction.Invalid); var discoveredThreads = DiscoveredData <ThreadIndex> .Make(ThreadIndex.Invalid); string additionalSymbolPath = BurstPath; #if UNITY_EDITOR additionalSymbolPath += ";" + Path.GetDirectoryName(EditorApplication.applicationPath); #endif var options = new TraceLogOptions() { AlwaysResolveSymbols = true, LocalSymbolsOnly = false, AllowUnsafeSymbols = true, AdditionalSymbolPath = additionalSymbolPath, ShouldResolveSymbols = path => { path = path.ToLowerInvariant(); return(pdbWhitelist.Any(x => path.EndsWith(x))); } }; profTrace = new ProfilerTrace(); using (var trace = TraceLog.OpenOrConvert(etlPath, options)) { var processIndex = FindUnityProcessIndex(trace.Processes); var processId = trace.Processes[processIndex].ProcessID; profTrace.Header = new ProfilerTraceHeader { SamplingInterval = trace.SampleProfileInterval.TotalMilliseconds, SessionStart = trace.SessionStartTime.Ticks, SessionEnd = trace.SessionEndTime.Ticks, }; var sampleRange = new Dictionary <MethodIndex, ulong>(); using (var samples = new NativeList <SampleData>(Allocator.Temp)) { SampleData sampleData = default; foreach (var evt in trace.Events) { var sample = evt as SampledProfileTraceData; if (sample == null || sample.ProcessID != processId || sample.NonProcess) { continue; } // TODO sample has a count field that we are currently ignoring. should we? sampleData.Address = (long)sample.InstructionPointer; sampleData.ThreadIdx = discoveredThreads.AddData(sample.Thread()?.ThreadIndex ?? ThreadIndex.Invalid); sampleData.TimeStamp = sample.TimeStampRelativeMSec; sampleData.StackTrace = discoveredStackFrames.AddData(sample.CallStackIndex()); var codeAddress = sample.IntructionPointerCodeAddress(); sampleData.Function = GetFunctionIndex(codeAddress); samples.Add(sampleData); if (sampleData.Function >= 0) { var index = discoveredFunctions.Data[sampleData.Function].Index; if (index == MethodIndex.Invalid) { continue; } if (!sampleRange.TryGetValue(index, out ulong previousMax)) { sampleRange[index] = sample.InstructionPointer; } else { sampleRange[index] = sample.InstructionPointer > previousMax ? sample.InstructionPointer : previousMax; } } } profTrace.Samples = samples.ToArray(allocator); } using (var stackFrames = new NativeList <StackFrameData>(Allocator.Temp)) { StackFrameData stackTraceData = default; // N.B. this loop adds more stack frames as it executes for (int idx = 0; idx < discoveredStackFrames.Count; idx++) { var stack = trace.CallStacks[discoveredStackFrames.Data[idx]]; stackTraceData.Address = (long)stack.CodeAddress.Address; stackTraceData.Depth = stack.Depth; stackTraceData.CallerStackFrame = discoveredStackFrames.AddData(stack.Caller?.CallStackIndex ?? CallStackIndex.Invalid); stackTraceData.Function = GetFunctionIndex(stack.CodeAddress); stackFrames.Add(stackTraceData); } profTrace.StackFrames = stackFrames.ToArray(allocator); } using (var functions = new NativeList <FunctionData>(Allocator.Temp)) { var burstModules = new Dictionary <int, bool> { { -1, false } }; var blobData = new NativeList <byte>(Allocator.Temp); FunctionData funcData = default; foreach (var func in discoveredFunctions.Data) { bool copyCode; if (func.Index != MethodIndex.Invalid) { var method = trace.CodeAddresses.Methods[func.Index]; if (method.MethodRva > 0 && method.MethodModuleFile != null) { funcData.BaseAddress = method.MethodModuleFile.ImageBase + (ulong)method.MethodRva; } else { funcData.BaseAddress = 0; } if (funcData.BaseAddress > 0 && method.MethodIndex != MethodIndex.Invalid && sampleRange.TryGetValue(method.MethodIndex, out var maxAddress)) { Debug.Assert(maxAddress >= funcData.BaseAddress); // use the value of the furthest sample to estimate the length of the function funcData.Length = 16 + (int)(maxAddress - funcData.BaseAddress); } else { funcData.Length = 0; } funcData.Module = discoveredModules.AddData(DiscoveredModule.FromIndex(method.MethodModuleFileIndex)); funcData.Name.CopyFrom(method.FullMethodName); if (!burstModules.TryGetValue((int)method.MethodModuleFileIndex, out var isBurst)) { isBurst = method.MethodModuleFile.FilePath.ToLowerInvariant().Contains("burst"); burstModules[(int)method.MethodModuleFileIndex] = isBurst; } copyCode = isBurst && funcData.Length > 0; } else { var jitData = func.MonoMethod; funcData.BaseAddress = (ulong)jitData.CodeStart.ToInt64(); funcData.Length = jitData.CodeSize; funcData.Module = discoveredModules.AddData(DiscoveredModule.FromMonoModule(jitData.Method.Module)); var fullName = MonoFunctionName(jitData.Method); funcData.Name.CopyFrom(fullName); copyCode = true; } if (copyCode) { funcData.CodeBlobOffset = blobData.Length; blobData.AddRange((void *)funcData.BaseAddress, funcData.Length); } else { funcData.CodeBlobOffset = -1; } functions.Add(funcData); } profTrace.Functions = functions.ToArray(allocator); profTrace.BlobData = blobData.ToArray(allocator); } using (var modules = new NativeList <ModuleData>(Allocator.Temp)) { // make sure that all modules of the current process are included. foreach (var module in trace.Processes[processIndex].LoadedModules) { discoveredModules.AddData(new DiscoveredModule { Index = module.ModuleFile.ModuleFileIndex }); } ModuleData moduleData = default; foreach (var dm in discoveredModules.Data) { if (dm.MonoModule != null) { moduleData = default; moduleData.IsMono = true; moduleData.FilePath = dm.MonoModule.Assembly.Location; } else { var module = trace.ModuleFiles[dm.Index]; moduleData.IsMono = false; moduleData.Checksum = module.ImageChecksum; moduleData.ImageBase = module.ImageBase; moduleData.ImageEnd = module.ImageEnd; moduleData.PdbAge = module.PdbAge; moduleData.FilePath.CopyFrom(module.FilePath); moduleData.PdbName.CopyFrom(module.PdbName); var guidBytes = module.PdbSignature.ToByteArray(); fixed(byte *ptr = guidBytes) UnsafeUtility.MemCpy(moduleData.PdbGuid, ptr, 16); } modules.Add(moduleData); } profTrace.Modules = modules.ToArray(allocator); } using (var threads = new NativeList <ThreadData>(Allocator.Temp)) { ThreadData threadData = default; foreach (var t in discoveredThreads.Data) { var thread = trace.Threads[t]; threadData.ThreadName.CopyFrom(thread.ThreadInfo ?? ""); threads.Add(threadData); } profTrace.Threads = threads.ToArray(allocator); } } int GetFunctionIndex(TraceCodeAddress address) { var method = address.Method; if (method == null) { var jit = Mono.GetJitInfoAnyDomain(new IntPtr((long)address.Address), out _); if (jit.Method == null) { return(-1); } if (!monoFunctions.TryGetValue(jit.CodeStart, out var actualJit)) { monoFunctions.Add(jit.CodeStart, jit); actualJit = jit; } return(discoveredFunctions.AddData(DiscoveredFunction.FromMethod(actualJit))); } return(discoveredFunctions.AddData(new DiscoveredFunction { Index = method.MethodIndex })); } string MonoFunctionName(MethodBase method) { if (method == null) { return("???"); } if (method.DeclaringType.DeclaringType == null) { return(method.DeclaringType.Namespace + '.' + method.DeclaringType.Name + '.' + method.Name); } var outerMost = method.DeclaringType.DeclaringType; var outer = method.DeclaringType; return(outerMost.Namespace + '.' + outerMost.Name + '+' + outer.Name + '.' + method.Name); } }