using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Unity.Burst.LowLevel; using UnityEditor; using System.Text.RegularExpressions; using UnityEditor.IMGUI.Controls; using UnityEngine; [assembly: InternalsVisibleTo("Unity.Burst.Editor.Tests")] [assembly: InternalsVisibleTo("Unity.Burst.Tester.Editor.Tests")] namespace Unity.Burst.Editor { internal class BurstInspectorGUI : EditorWindow { private static bool Initialized; private static void EnsureInitialized() { if (Initialized) { return; } Initialized = true; BurstLoader.OnBurstShutdown += () => { if (EditorWindow.HasOpenInstances()) { var window = EditorWindow.GetWindow("Burst Inspector"); window.Close(); } }; } private const string FontSizeIndexPref = "BurstInspectorFontSizeIndex"; private static readonly string[] DisassemblyKindNames = { "Assembly", ".NET IL", "LLVM IR (Unoptimized)", "LLVM IR (Optimized)", "LLVM IR Optimisation Diagnostics" }; internal enum AssemblyOptions { PlainWithoutDebugInformation = 0, PlainWithDebugInformation = 1, EnhancedWithMinimalDebugInformation = 2, EnhancedWithFullDebugInformation = 3, ColouredWithMinimalDebugInformation = 4, ColouredWithFullDebugInformation = 5 } internal AssemblyOptions? _assemblyKind = null; private AssemblyOptions? _assemblyKindPrior = null; private AssemblyOptions _oldAssemblyKind; private bool SupportsEnhancedRendering => _disasmKind == DisassemblyKind.Asm || _disasmKind == DisassemblyKind.OptimizedIR || _disasmKind == DisassemblyKind.UnoptimizedIR; private static string[] DisasmOptions; internal static string[] GetDisasmOptions() { if (DisasmOptions == null) { // We can't initialize this in BurstInspectorGUI.cctor because BurstCompilerOptions may not yet // have been initialized by BurstLoader. So we initialize on-demand here. This method doesn't need to // be thread-safe because it's only called from the UI thread. DisasmOptions = new[] { "\n" + BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDump, NativeDumpFlags.Asm), "\n" + BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDump, NativeDumpFlags.IL), "\n" + BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDump, NativeDumpFlags.IR), "\n" + BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDump, NativeDumpFlags.IROptimized), "\n" + BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDump, NativeDumpFlags.IRPassAnalysis) }; } return DisasmOptions; } private static readonly SplitterState TreeViewSplitterState = new SplitterState(new float[] { 30, 70 }, new int[] { 128, 128 }, null); private static readonly string[] TargetCpuNames = Enum.GetNames(typeof(BurstTargetCpu)); private static readonly string[] SIMDSmellTest = { "False", "True" }; private static readonly int[] FontSizes = { 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20 }; private static string[] _fontSizesText; internal const int _scrollbarThickness = 14; internal float _buttonOverlapInspectorView = 0; /// Used because it's not legal to change layout of GUI in a frame without the users input. private float _buttonBarWidth = -1; [NonSerialized] internal readonly BurstDisassembler _burstDisassembler; private const string BurstSettingText = "Inspector Settings/"; [SerializeField] private BurstTargetCpu _targetCpu = BurstTargetCpu.Auto; [SerializeField] private DisassemblyKind _disasmKind = DisassemblyKind.Asm; [SerializeField] private DisassemblyKind _oldDisasmKind = DisassemblyKind.Asm; [NonSerialized] internal GUIStyle fixedFontStyle; [NonSerialized] internal int fontSizeIndex = -1; [SerializeField] private int _previousTargetIndex = -1; [SerializeField] private bool _safetyChecks = false; [SerializeField] private bool _showBranchMarkers = true; [SerializeField] private bool _enhancedDisassembly = true; [SerializeField] private string _searchFilterJobs; [SerializeField] private bool _showUnityNamespaceJobs = false; [SerializeField] private bool _showDOTSGeneratedJobs = false; [SerializeField] private bool _focusTargetJob = true; [SerializeField] private string _searchFilterAssembly = String.Empty; [SerializeField] private bool _sameTargetButDifferentAssemblyKind = false; [SerializeField] internal Vector2 _scrollPos; internal SearchField _searchFieldJobs; internal SearchField _searchFieldAssembly; private bool saveSearchFieldFromEvent = false; [SerializeField] private bool _searchBarVisible = true; [SerializeField] private string _selectedItem; [NonSerialized] private BurstCompileTarget _target; [NonSerialized] private List _targets; // Used as a serialized representation of _targets: [SerializeField] private List targetNames; [NonSerialized] internal LongTextArea _textArea; internal Rect _inspectorView; [NonSerialized] internal Font _font; [NonSerialized] internal BurstMethodTreeView _treeView; // Serialized representation of _treeView: [SerializeField] private TreeViewState treeViewState; [NonSerialized] internal bool _initialized; [NonSerialized] private bool _requiresRepaint; private int FontSize => FontSizes[fontSizeIndex]; private static readonly Regex _rx = new Regex(@"^.*\(\d+,\d+\):\sBurst\serror"); private bool _leftClicked = false; [SerializeField] private bool _isCompileError = false; [SerializeField] private bool _prevWasCompileError; [SerializeField] private bool _smellTest = false; // Caching GUIContent and style options for button bar private readonly GUIContent _contentShowUnityNamespaceJobs = new GUIContent("Show Unity Namespace"); private readonly GUIContent _contentShowDOTSGeneratedJobs = new GUIContent("Show \".Generated\""); private readonly GUIContent _contentDisasm = new GUIContent("Enhanced With Minimal Debug Information"); private readonly GUIContent _contentCollapseToCode = new GUIContent("Focus on Code"); private readonly GUIContent _contentExpandAll = new GUIContent("Expand All"); private readonly GUIContent _contentBranchLines = new GUIContent("Show Branch Flow"); private readonly GUIContent[] _contentsTarget = Enum.GetNames(typeof(BurstTargetCpu)).Select(str => new GUIContent($"Target ({str})")).ToArray(); private readonly GUIContent[] _contentsFontSize = FontSizes.Select(v => new GUIContent($"Font Size ({v})")).ToArray(); private readonly GUIContent[] _contentsSmellTest = { new GUIContent("Highlight SIMD Scalar vs Packed (False)"), new GUIContent("Highlight SIMD Scalar vs Packed (True)") }; // content for button search bar private readonly GUIContent _ignoreCase = new GUIContent("Match Case"); private readonly GUIContent _matchWord = new GUIContent("Whole words"); private readonly GUIContent _regexSearch = new GUIContent("Regex"); private readonly GUILayoutOption[] _toolbarStyleOptions = { GUILayout.ExpandWidth(true), GUILayout.MinWidth(5 * 10) }; private readonly string[] _branchMarkerOptions = { "Hide Branch Flow", "Show Branch Flow" }; private readonly string[] _safetyCheckOptions = { "Safety Check On", "Safety Check Off" }; private enum KeyboardOperation { SelectAll, Copy, MoveLeft, MoveRight, MoveUp, MoveDown, Search, Escape, Enter, } private Dictionary _keyboardEvents; private void FillKeyboardEvent() { if (_keyboardEvents != null) { return; } _keyboardEvents = new Dictionary(); _keyboardEvents.Add(Event.KeyboardEvent("#left"), KeyboardOperation.MoveLeft); _keyboardEvents.Add(Event.KeyboardEvent("#right"), KeyboardOperation.MoveRight); _keyboardEvents.Add(Event.KeyboardEvent("#down"), KeyboardOperation.MoveDown); _keyboardEvents.Add(Event.KeyboardEvent("#up"), KeyboardOperation.MoveUp); _keyboardEvents.Add(Event.KeyboardEvent("escape"), KeyboardOperation.Escape); _keyboardEvents.Add(Event.KeyboardEvent("return"), KeyboardOperation.Enter); _keyboardEvents.Add(Event.KeyboardEvent("#return"), KeyboardOperation.Enter); _keyboardEvents.Add(Event.KeyboardEvent("left"), KeyboardOperation.MoveLeft); _keyboardEvents.Add(Event.KeyboardEvent("right"), KeyboardOperation.MoveRight); _keyboardEvents.Add(Event.KeyboardEvent("up"), KeyboardOperation.MoveUp); _keyboardEvents.Add(Event.KeyboardEvent("down"), KeyboardOperation.MoveDown); if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { _keyboardEvents.Add(Event.KeyboardEvent("%a"), KeyboardOperation.SelectAll); _keyboardEvents.Add(Event.KeyboardEvent("%c"), KeyboardOperation.Copy); _keyboardEvents.Add(Event.KeyboardEvent("%f"), KeyboardOperation.Search); } else { // windows or linux bindings. _keyboardEvents.Add(Event.KeyboardEvent("^a"), KeyboardOperation.SelectAll); _keyboardEvents.Add(Event.KeyboardEvent("^c"), KeyboardOperation.Copy); _keyboardEvents.Add(Event.KeyboardEvent("^f"), KeyboardOperation.Search); } } public BurstInspectorGUI() { _burstDisassembler = new BurstDisassembler(); } private bool DisplayAssemblyKind(Enum assemblyKind) { var assemblyOption = (AssemblyOptions)assemblyKind; if (_disasmKind != DisassemblyKind.Asm || _isCompileError) { return assemblyOption == AssemblyOptions.PlainWithoutDebugInformation; } return true; } public void OnEnable() { EnsureInitialized(); var newTreeState = false; if (treeViewState is null) { treeViewState = new TreeViewState(); newTreeState = true; } _treeView ??= _treeView = new BurstMethodTreeView ( treeViewState, () => _searchFilterJobs, () => (_showUnityNamespaceJobs, _showDOTSGeneratedJobs) ); if (_keyboardEvents == null) FillKeyboardEvent(); var assemblyList = BurstReflection.EditorAssembliesThatCanPossiblyContainJobs; Task.Run( () => { // Do this stuff asynchronously. var result = BurstReflection.FindExecuteMethods(assemblyList, BurstReflectionAssemblyOptions.None); _targets = result.CompileTargets; _targets.Sort((left, right) => string.Compare(left.GetDisplayName(), right.GetDisplayName(), StringComparison.Ordinal)); return result; }) .ContinueWith(t => { // Do this stuff on the main (UI) thread. if (t.Status == TaskStatus.RanToCompletion) { foreach (var logMessage in t.Result.LogMessages) { switch (logMessage.LogType) { case BurstReflection.LogType.Warning: Debug.LogWarning(logMessage.Message); break; case BurstReflection.LogType.Exception: Debug.LogException(logMessage.Exception); break; default: throw new InvalidOperationException(); } } List newNames = _targets.Select(x => x.GetDisplayName()).ToList(); bool identical = !newTreeState && newNames.SequenceEqual(targetNames); targetNames = newNames; _treeView.Initialize(_targets, identical); if (_selectedItem == null || !_treeView.TrySelectByDisplayName(_selectedItem)) { _previousTargetIndex = -1; _scrollPos = Vector2.zero; } _requiresRepaint = true; _initialized = true; } else if (t.Exception != null) { Debug.LogError($"Could not load Inspector: {t.Exception}"); } }); } #if !UNITY_2023_1_OR_NEWER private void CleanupFont() { if (_font != null) { DestroyImmediate(_font, true); _font = null; } } public void OnDisable() { CleanupFont(); } #endif public void Update() { // Need to do this because if we call Repaint from anywhere else, // it doesn't do anything if this window is not currently focused. if (_requiresRepaint) { Repaint(); _requiresRepaint = false; } // Need this because pressing new target, and then not invoking new events, // will leave the assembly unrendered. // This is not included in above, to minimize needed calls. if (_target != null && _target.JustLoaded) { Repaint(); } } /// /// Checks if there is space for given content withs style, and starts new horizontalgroup /// if there is no space on this line. /// private void FlowToNewLine(ref float remainingWidth, float width, Vector2 size) { float sizeX = size.x + _scrollbarThickness / 2; if (sizeX >= remainingWidth) { _buttonOverlapInspectorView += size.y + 2; remainingWidth = width - sizeX; GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); } else { remainingWidth -= sizeX; } } private bool IsRaw(AssemblyOptions kind) { return kind == AssemblyOptions.PlainWithoutDebugInformation || kind == AssemblyOptions.PlainWithDebugInformation; } private bool IsEnhanced(AssemblyOptions kind) { return !IsRaw(kind); } private bool IsColoured(AssemblyOptions kind) { return kind == AssemblyOptions.ColouredWithMinimalDebugInformation || kind == AssemblyOptions.ColouredWithFullDebugInformation; } /// /// Renders buttons bar, and handles saving/loading of _assemblyKind options when changing in inspector settings /// that disable/enables some options for _assemblyKind. /// private void HandleButtonBars(BurstCompileTarget target, bool targetChanged, out int fontIndex, out bool collapse, out bool focusCode) { // We can only make an educated guess for the correct width. if (_buttonBarWidth == -1) { _buttonBarWidth = (position.width * 2) / 3 - _scrollbarThickness; } RenderButtonBars(_buttonBarWidth, target, out fontIndex, out collapse, out focusCode); var disasmKindChanged = _oldDisasmKind != _disasmKind; // Handles saving and loading _assemblyKind option when going between settings, that disable/enable some options for it if ((disasmKindChanged && _oldDisasmKind == DisassemblyKind.Asm && !_isCompileError) || (targetChanged && !_prevWasCompileError && _isCompileError && _disasmKind == DisassemblyKind.Asm)) { // save when _disasmKind changed from Asm WHEN we are not looking at a burst compile error, // or when target changed from non compile error to compile error and current _disasmKind is Asm. _oldAssemblyKind = (AssemblyOptions)_assemblyKind; } else if ((disasmKindChanged && _disasmKind == DisassemblyKind.Asm && !_isCompileError) || (targetChanged && _prevWasCompileError && _disasmKind == DisassemblyKind.Asm)) { // load when _diasmKind changed to Asm and we are not at burst compile error, // or when target changed from a burst compile error while _disasmKind is Asm. _assemblyKind = _oldAssemblyKind; } // if _assemblyKind is something that is not available, force it up to PlainWithoutDebugInformation. if ((_disasmKind != DisassemblyKind.Asm && _assemblyKind != AssemblyOptions.PlainWithoutDebugInformation) || _isCompileError) { _assemblyKind = AssemblyOptions.PlainWithoutDebugInformation; } } private void RenderButtonBars(float width, BurstCompileTarget target, out int fontIndex, out bool collapse, out bool focus) { var remainingWidth = width; GUILayout.BeginHorizontal(); // First button should never call beginHorizontal(). remainingWidth -= (EditorStyles.popup.CalcSize(_contentDisasm).x + _scrollbarThickness / 2f); EditorGUI.BeginDisabledGroup(target.DisassemblyKind == DisassemblyKind.IRPassAnalysis); _assemblyKind = (AssemblyOptions)EditorGUILayout.EnumPopup(GUIContent.none, _assemblyKind, DisplayAssemblyKind, true); EditorGUI.EndDisabledGroup(); FlowToNewLine(ref remainingWidth, width, EditorStyles.popup.CalcSize(_contentBranchLines)); // Reversed "logic" to match the array of options, which has "positive" case on idx 0. _safetyChecks = EditorGUILayout.Popup(_safetyChecks ? 0 : 1, _safetyCheckOptions) == 0; EditorGUI.BeginDisabledGroup(!target.HasRequiredBurstCompileAttributes); GUIContent targetContent = _contentsTarget[(int)_targetCpu]; FlowToNewLine(ref remainingWidth, width, EditorStyles.popup.CalcSize(targetContent)); _targetCpu = (BurstTargetCpu)LabeledPopup.Popup((int)_targetCpu, targetContent, TargetCpuNames); GUIContent fontSizeContent = _contentsFontSize[fontSizeIndex]; FlowToNewLine(ref remainingWidth, width, EditorStyles.popup.CalcSize(fontSizeContent)); fontIndex = LabeledPopup.Popup(fontSizeIndex, fontSizeContent, _fontSizesText); EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup(!IsEnhanced((AssemblyOptions)_assemblyKind) || !SupportsEnhancedRendering || _isCompileError); FlowToNewLine(ref remainingWidth, width, EditorStyles.miniButton.CalcSize(_contentCollapseToCode)); focus = GUILayout.Button(_contentCollapseToCode, EditorStyles.miniButton); FlowToNewLine(ref remainingWidth, width, EditorStyles.miniButton.CalcSize(_contentExpandAll)); collapse = GUILayout.Button(_contentExpandAll, EditorStyles.miniButton); FlowToNewLine(ref remainingWidth, width, EditorStyles.popup.CalcSize(_contentBranchLines)); _showBranchMarkers = EditorGUILayout.Popup(Convert.ToInt32(_showBranchMarkers), _branchMarkerOptions) == 1; int smellTestIdx = Convert.ToInt32(_smellTest); GUIContent smellTestContent = _contentsSmellTest[smellTestIdx]; FlowToNewLine(ref remainingWidth, width, EditorStyles.popup.CalcSize(smellTestContent)); _smellTest = LabeledPopup.Popup(smellTestIdx, smellTestContent, SIMDSmellTest) == 1; EditorGUI.EndDisabledGroup(); GUILayout.EndHorizontal(); _oldDisasmKind = _disasmKind; _disasmKind = (DisassemblyKind)GUILayout.Toolbar((int)_disasmKind, DisassemblyKindNames, _toolbarStyleOptions); } /// /// Handles mouse events for selecting text. /// /// /// Must be called after Render(...), as it uses the mouse events, and Render(...) /// need mouse events for buttons etc. /// private void HandleMouseEventForSelection(Rect workingArea, int controlID, bool showBranchMarkers) { var evt = Event.current; var mousePos = evt.mousePosition; if (_textArea.MouseOutsideView(workingArea, mousePos, controlID)) { return; } switch (evt.type) { case EventType.MouseDown: // button 0 is left and 1 is right if (evt.button == 0) { _textArea.MouseClicked(showBranchMarkers, evt.shift, mousePos, controlID); } else { _leftClicked = true; } evt.Use(); break; case EventType.MouseDrag: _textArea.DragMouse(mousePos, showBranchMarkers); evt.Use(); break; case EventType.MouseUp: _textArea.MouseReleased(); evt.Use(); break; case EventType.ScrollWheel: _textArea.DoScroll(workingArea, evt.delta.y); // we cannot Use() (consume) scrollWheel events, as they are still needed in EndScrollView. break; } } private bool AssemblyFocused() => !((_treeView != null && _treeView.HasFocus()) || (_searchFieldAssembly != null && _searchFieldAssembly.HasFocus())); private void HandleKeyboardEventAssemblyView(Rect workingArea, KeyboardOperation op, Event evt, bool showBranchMarkers) { switch (op) { case KeyboardOperation.SelectAll: _textArea.SelectAll(); evt.Use(); break; case KeyboardOperation.Copy: _textArea.DoSelectionCopy(); evt.Use(); break; case KeyboardOperation.MoveLeft: if (evt.shift) { if (_textArea.HasSelection) _textArea.MoveSelectionLeft(workingArea, showBranchMarkers); } else { _textArea.MoveView(LongTextArea.Direction.Left, workingArea); } evt.Use(); break; case KeyboardOperation.MoveRight: if (evt.shift) { if (_textArea.HasSelection) _textArea.MoveSelectionRight(workingArea, showBranchMarkers); } else { _textArea.MoveView(LongTextArea.Direction.Right, workingArea); } evt.Use(); break; case KeyboardOperation.MoveUp: if (evt.shift) { if (_textArea.HasSelection) _textArea.MoveSelectionUp(workingArea, showBranchMarkers); } else { _textArea.MoveView(LongTextArea.Direction.Up, workingArea); } evt.Use(); break; case KeyboardOperation.MoveDown: if (evt.shift) { if (_textArea.HasSelection) _textArea.MoveSelectionDown(workingArea, showBranchMarkers); } else { _textArea.MoveView(LongTextArea.Direction.Down, workingArea); } evt.Use(); break; case KeyboardOperation.Search: _searchBarVisible = true; _searchFieldAssembly?.SetFocus(); evt.Use(); break; } } /// /// Must be called after Render(...) because of depenency on LongTextArea.finalAreaSize. /// private void HandleKeyboardEventForSelection(Rect workingArea, bool showBranchMarkers) { var evt = Event.current; if (!_keyboardEvents.TryGetValue(evt, out var op)) { return; } if (AssemblyFocused()) { // Do input handling for assembly view. HandleKeyboardEventAssemblyView(workingArea, op, evt, showBranchMarkers); } else { // This amounts to logic for all else. switch (op) { case KeyboardOperation.Escape: if (_searchFieldAssembly != null && _searchFieldAssembly.HasFocus() && _searchFilterAssembly == "") { _searchBarVisible = false; evt.Use(); } break; case KeyboardOperation.Enter: if (_searchFieldAssembly != null && _searchFieldAssembly.HasFocus()) { _textArea.NextSearchHit(evt.shift, workingArea); saveSearchFieldFromEvent = true; evt.Use(); } break; } } } private void RenderCompileTargetsFilters(float width) { GUILayout.BeginHorizontal(); // Handle and render filtering toggles: var newShowUnityTests = GUILayout.Toggle(_showUnityNamespaceJobs, _contentShowUnityNamespaceJobs); FlowToNewLine(ref width, width, EditorStyles.toggle.CalcSize(_contentShowDOTSGeneratedJobs)); var newShowDOTSGeneratedJobs = GUILayout.Toggle(_showDOTSGeneratedJobs, _contentShowDOTSGeneratedJobs); GUILayout.EndHorizontal(); if (newShowUnityTests != _showUnityNamespaceJobs || newShowDOTSGeneratedJobs != _showDOTSGeneratedJobs) { _showDOTSGeneratedJobs = newShowDOTSGeneratedJobs; _showUnityNamespaceJobs = newShowUnityTests; _treeView.Reload(); } // Handle and render search filter: var newFilter = _searchFieldJobs.OnGUI(_searchFilterJobs); if (newFilter != _searchFilterJobs) { _searchFilterJobs = newFilter; _treeView.Reload(); } } private void CompileNewTarget(BurstCompileTarget target, BurstCompilerOptions targetOptions) { if (target.IsLoading) { return; } target.IsLoading = true; target.JustLoaded = false; // Setup target and it's compilation options. // This is done here as EditorGUIUtility.isProSkin must be on main thread. target.TargetCpu = _targetCpu; target.DisassemblyKind = _disasmKind; targetOptions.EnableBurstSafetyChecks = _safetyChecks; target.IsDarkMode = EditorGUIUtility.isProSkin; targetOptions.EnableBurstCompileSynchronously = true; // Don't set debug mode, because it disables optimizations. // Instead we set debug level (None, Full, LineOnly) below. targetOptions.EnableBurstDebug = false; Task.Run(() => { var options = new StringBuilder(); if (targetOptions.TryGetOptions(target.IsStaticMethod ? (MemberInfo)target.Method : target.JobType, true, out var defaultOptions)) { options.AppendLine(defaultOptions); // Disables the 2 current warnings generated from code (since they clutter up the inspector display) // BC1370 - throw inside code not guarded with ConditionalSafetyCheck attribute // BC1322 - loop intrinsic on loop that has been optimized away options.AppendLine($"{BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDisableWarnings, "BC1370;BC1322")}"); options.AppendLine($"{BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionTarget, TargetCpuNames[(int)_targetCpu])}"); // For IRPassAnalysis, we always want full debug information. if (_disasmKind != DisassemblyKind.IRPassAnalysis) { switch (_assemblyKind) { case AssemblyOptions.EnhancedWithMinimalDebugInformation: case AssemblyOptions.ColouredWithMinimalDebugInformation: options.AppendLine($"{BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDebug, "2")}"); break; case AssemblyOptions.ColouredWithFullDebugInformation: case AssemblyOptions.EnhancedWithFullDebugInformation: case AssemblyOptions.PlainWithDebugInformation: options.AppendLine($"{BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDebug, "1")}"); break; default: case AssemblyOptions.PlainWithoutDebugInformation: options.AppendLine($"{BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDebug, "0")}"); break; } } else { options.AppendLine($"{BurstCompilerOptions.GetOption(BurstCompilerOptions.OptionDebug, "1")}"); } var baseOptions = options.ToString(); target.RawDisassembly = GetDisassembly(target.Method, baseOptions + GetDisasmOptions()[(int)_disasmKind]); target.FormattedDisassembly = null; target.IsBurstError = IsBurstError(target.RawDisassembly); } target.IsLoading = false; target.JustLoaded = true; }); } private void RenderBurstJobMenu() { float width = position.width / 3; GUILayout.BeginVertical(GUILayout.Width(width)); // Render Treeview showing burst targets: GUILayout.Label("Compile Targets", EditorStyles.boldLabel); RenderCompileTargetsFilters(width); // Does not give proper rect during layout event. _inspectorView = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); _treeView.OnGUI(_inspectorView); GUILayout.EndVertical(); } private void HandleHorizontalFocus(float workingWidth, bool shouldSetupText, bool isTextFormatted) { if (!shouldSetupText || !isTextFormatted || !_burstDisassembler.IsInitialized) { return; } var branchFiller = _textArea.MaxLineDepth * 10; if (branchFiller < workingWidth / 2f) { return; } // Do horizontal padding: _scrollPos.x = _textArea.MaxLineDepth * 10; } private static void RenderLoading() { GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.BeginVertical(); GUILayout.FlexibleSpace(); GUILayout.Label("Loading..."); GUILayout.FlexibleSpace(); GUILayout.EndVertical(); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); } public void OnGUI() { if (!_initialized) { RenderLoading(); return; } // used to give hot control to inspector when a mouseDown event has happened. // This way we can register a mouseUp happening outside inspector. int controlID = GUIUtility.GetControlID(FocusType.Passive); // Make sure that editor options are synchronized BurstEditorOptions.EnsureSynchronized(); if (_fontSizesText == null) { _fontSizesText = new string[FontSizes.Length]; for (var i = 0; i < FontSizes.Length; ++i) _fontSizesText[i] = FontSizes[i].ToString(); } if (fontSizeIndex == -1) { fontSizeIndex = EditorPrefs.GetInt(FontSizeIndexPref, 5); fontSizeIndex = Math.Max(0, fontSizeIndex); fontSizeIndex = Math.Min(fontSizeIndex, FontSizes.Length - 1); } if (fixedFontStyle == null || fixedFontStyle.font == null) // also check .font as it's reset somewhere when going out of play mode. { fixedFontStyle = new GUIStyle(GUI.skin.label); #if UNITY_2023_1_OR_NEWER _font = EditorGUIUtility.Load("Fonts/RobotoMono/RobotoMono-Regular.ttf") as Font; #else string fontName; if (Application.platform == RuntimePlatform.WindowsEditor) { fontName = "Consolas"; } else { fontName = "Courier"; } CleanupFont(); _font = Font.CreateDynamicFontFromOSFont(fontName, FontSize); #endif fixedFontStyle.font = _font; fixedFontStyle.fontSize = FontSize; } if (_searchFieldJobs == null) _searchFieldJobs = new SearchField(); if (_textArea == null) _textArea = new LongTextArea(); GUILayout.BeginHorizontal(); // SplitterGUILayout.BeginHorizontalSplit is internal in Unity but we don't have much choice SplitterGUILayout.BeginHorizontalSplit(TreeViewSplitterState); RenderBurstJobMenu(); GUILayout.BeginVertical(); var selection = _treeView.GetSelection(); if (selection.Count == 1) { var targetIndex = selection[0]; _target = _targets[targetIndex - 1]; var targetOptions = _target.Options; var targetChanged = _previousTargetIndex != targetIndex; _previousTargetIndex = targetIndex; // Stash selected item name to handle domain reloads more gracefully _selectedItem = _target.GetDisplayName(); if (_assemblyKind == null) { if (_enhancedDisassembly) { _assemblyKind = AssemblyOptions.ColouredWithMinimalDebugInformation; } else { _assemblyKind = AssemblyOptions.PlainWithoutDebugInformation; } _oldAssemblyKind = (AssemblyOptions)_assemblyKind; } // We are currently formatting only Asm output var isTextFormatted = IsEnhanced((AssemblyOptions)_assemblyKind) && SupportsEnhancedRendering; // Depending if we are formatted or not, we don't render the same text var textToRender = _target.RawDisassembly?.TrimStart('\n'); // Only refresh if we are switching to a new selection that hasn't been disassembled yet // Or we are changing disassembly settings (safety checks / enhanced disassembly) var targetRefresh = textToRender == null || _target.DisassemblyKind != _disasmKind || targetOptions.EnableBurstSafetyChecks != _safetyChecks || _target.TargetCpu != _targetCpu || _target.IsDarkMode != EditorGUIUtility.isProSkin; if (_assemblyKindPrior != _assemblyKind) { targetRefresh = true; _assemblyKindPrior = _assemblyKind; // Needs to be refreshed, as we need to change disassembly options // If the target did not changed but our assembly kind did, we need to remember this. if (!targetChanged) { _sameTargetButDifferentAssemblyKind = true; } } // If the previous target changed the assembly kind and we have a target change, we need to // refresh the assembly because we'll have cached the previous assembly kinds output rather // than the one requested. if (_sameTargetButDifferentAssemblyKind && targetChanged) { targetRefresh = true; _sameTargetButDifferentAssemblyKind = false; } if (targetRefresh) { CompileNewTarget(_target, targetOptions); } _prevWasCompileError = _isCompileError; _isCompileError = _target.IsBurstError; _buttonOverlapInspectorView = 0; var oldSimdSmellTest = _smellTest; HandleButtonBars(_target, targetChanged, out var fontSize, out var expandAllBlocks, out var focusCode); var simdSmellTestChanged = oldSimdSmellTest != _smellTest; // Guard against _textArea being used, as the assembly isn't ready yet. // Have to test against event so it cannot finish between a Layout event and Repaint event; // this is necessary as we cannot alter GUI between these events. if (_target.HasRequiredBurstCompileAttributes && (_target.IsLoading || (_target.JustLoaded && Event.current.type != EventType.Layout))) { RenderLoading(); // Need to close the splits we opened. GUILayout.EndVertical(); SplitterGUILayout.EndHorizontalSplit(); GUILayout.EndHorizontal(); return; } var justLoaded = _target.JustLoaded; _target.JustLoaded = false; // If ´CompileNewTarget´ finishes before we enter loading screen above `textToRender` might not be set. textToRender ??= _target.RawDisassembly?.TrimStart('\n'); if (!string.IsNullOrEmpty(textToRender)) { // we should only call SetDisassembler(...) the first time assemblyKind is changed with same target. // Otherwise it will kep re-initializing fields such as _folded, meaning we can no longer fold/unfold. var shouldSetupText = !_textArea.IsTextSet(_selectedItem) || justLoaded || simdSmellTestChanged; if (shouldSetupText) { _textArea.SetText( _selectedItem, textToRender, _target.IsDarkMode, _burstDisassembler, isTextFormatted && _burstDisassembler.Initialize( textToRender, FetchAsmKind(_targetCpu, _disasmKind), _target.IsDarkMode, IsColoured((AssemblyOptions)_assemblyKind), _smellTest)); } if (justLoaded) { _scrollPos = Vector2.zero; } HandleHorizontalFocus( _inspectorView.width == 1f ? _buttonBarWidth : _inspectorView.width, shouldSetupText, isTextFormatted ); // Fixing lastRectSize to actually be size of scroll view _inspectorView.position = _scrollPos; _inspectorView.width = position.width - (_inspectorView.width + _scrollbarThickness); _inspectorView.height -= (_buttonOverlapInspectorView + 4); //+4 for alignment. if (_searchBarVisible) _inspectorView.height -= EditorStyles.searchField.CalcHeight(GUIContent.none, 2); // 2 is just arbitrary, as the width does not alter height // repaint indicate end of frame, so we can alter width for menu items to new correct. if (Event.current.type == EventType.Repaint) { _buttonBarWidth = _inspectorView.width - _scrollbarThickness; } // Do search if we did not try and find assembly and we were actually going to do a search. if (_focusTargetJob && TryFocusJobInAssemblyView(ref _inspectorView, shouldSetupText, _target)) { _scrollPos.y = _inspectorView.y - _textArea.fontHeight*10; } _scrollPos = GUILayout.BeginScrollView(_scrollPos, true, true); if (Event.current.type != EventType.Layout) // we always want mouse position feedback { _textArea.Interact(_inspectorView, Event.current.type); } // Set up search information if it is happening. Regex regx = default; SearchCriteria sc = default; var doSearch = _searchBarVisible && _searchFilterAssembly != ""; var wrongRegx = false; if (doSearch) { sc = new SearchCriteria(_searchFilterAssembly, _doIgnoreCase, _doWholeWordMatch, _doRegex); if (_doRegex) { try { var opt = RegexOptions.Compiled | RegexOptions.CultureInvariant; if (!_doIgnoreCase) opt |= RegexOptions.IgnoreCase; var filter = _searchFilterAssembly; if (_doWholeWordMatch) filter = @"\b" + filter + @"\b"; regx = new Regex(filter, opt); } catch (Exception) { // Regex was invalid wrongRegx = true; doSearch = false; } } } var doRepaint = _textArea.Render(fixedFontStyle, _inspectorView, _showBranchMarkers, doSearch, sc, regx); // A change in the underlying textArea has happened, that requires the GUI to be repainted during this frame. if (doRepaint) Repaint(); if (Event.current.type != EventType.Layout) { HandleMouseEventForSelection(_inspectorView, controlID, _showBranchMarkers); HandleKeyboardEventForSelection(_inspectorView, _showBranchMarkers); } if (_leftClicked) { GenericMenu menu = new GenericMenu(); menu.AddItem(EditorGUIUtility.TrTextContent("Copy Selection"), false, _textArea.DoSelectionCopy); menu.AddItem(EditorGUIUtility.TrTextContent("Copy Color Tags"), _textArea.CopyColorTags, _textArea.ChangeCopyMode); menu.AddItem(EditorGUIUtility.TrTextContent("Select All"), false, _textArea.SelectAll); menu.AddItem(EditorGUIUtility.TrTextContent($"Find in {DisassemblyKindNames[(int)_disasmKind]}"), _searchBarVisible, EnableDisableSearchBar); menu.ShowAsContext(); _leftClicked = false; } GUILayout.EndScrollView(); if (_searchBarVisible) { if (_searchFieldAssembly == null) { _searchFieldAssembly = new SearchField(); _searchFieldAssembly.autoSetFocusOnFindCommand = false; } int hitnumbers = _textArea.NrSearchHits > 0 ? _textArea.ActiveSearchNr + 1 : 0; var hitNumberContent = new GUIContent(" " + hitnumbers + " of " + _textArea.NrSearchHits + " hits"); GUILayout.BeginHorizontal(); // Makes sure that on "enter" keyboard event, the focus is not taken away from searchField. if (saveSearchFieldFromEvent) GUI.FocusControl("BurstInspectorGUI"); string newFilterAssembly; if (wrongRegx) { var colb = GUI.contentColor; GUI.contentColor = Color.red; newFilterAssembly = _searchFieldAssembly.OnGUI(_searchFilterAssembly); GUI.contentColor = colb; } else { newFilterAssembly = _searchFieldAssembly.OnGUI(_searchFilterAssembly); } // Give back focus to the searchField, if we took it away. if (saveSearchFieldFromEvent) { _searchFieldAssembly.SetFocus(); saveSearchFieldFromEvent = false; } if (newFilterAssembly != _searchFilterAssembly) { _searchFilterAssembly = newFilterAssembly; _textArea.StopSearching(); } _doIgnoreCase = GUILayout.Toggle(_doIgnoreCase, _ignoreCase); _doWholeWordMatch = GUILayout.Toggle(_doWholeWordMatch, _matchWord); _doRegex = GUILayout.Toggle(_doRegex, _regexSearch); GUILayout.Label(hitNumberContent); if (GUILayout.Button(GUIContent.none, EditorStyles.searchFieldCancelButton)) { _searchBarVisible = false; _textArea.StopSearching(); } GUILayout.EndHorizontal(); } } if (fontSize != fontSizeIndex) { _textArea.Invalidate(); fontSizeIndex = fontSize; EditorPrefs.SetInt(FontSizeIndexPref, fontSize); fixedFontStyle = null; } if (expandAllBlocks) { _textArea.ExpandAllBlocks(); } if (focusCode) { _textArea.FocusCodeBlocks(); } } GUILayout.EndVertical(); SplitterGUILayout.EndHorizontalSplit(); GUILayout.EndHorizontal(); } public static bool IsBurstError(string disassembly) { return _rx.IsMatch(disassembly ?? ""); } /// /// Focuses the view on the active function if a jump is doable. /// /// Current assembly view. /// Whether text was set in . /// Target job to find function in. /// Whether a focus was attempted or not. private bool TryFocusJobInAssemblyView(ref Rect workingArea, bool wasTextSetup, BurstCompileTarget target) { bool TryFindByLabel(ref Rect workingArea) { var regx = default(Regex); var sb = new StringBuilder(); if (target.IsStaticMethod) { // Search for fullname as label // Null reference not a danger, because of target being a static method sb.Append(target.Method.DeclaringType.ToString().Replace("+", ".")); // Generic labels will be sorounded by "", while standard static methods won't var genericArguments = target.JobType.GenericTypeArguments; if (genericArguments.Length > 0) { // Need to alter the generic arguments from [] to <> form // Removing [] form var idx = sb.ToString().LastIndexOf('['); sb.Remove(idx, sb.Length - idx); // Adding <> form sb.Append('<').Append(BurstCompileTarget.Pretty(genericArguments[0])); for (var i = 1; i < genericArguments.Length; i++) { sb.Append(",").Append(BurstCompileTarget.Pretty(genericArguments[i])); } sb.Append('>').Append('.').Append(target.Method.Name); } else { sb.Append('.').Append(target.Method.Name); } const RegexOptions opt = RegexOptions.Compiled | RegexOptions.CultureInvariant; regx = new Regex(@$"{Regex.Escape(sb.ToString())}[^"":]+"":", opt); } else { // Append full method name. Using display name for simpler access var targetName = target.GetDisplayName(); // Remove part that tells about used interface var idx = 0; // If generic the argument part must also be removed, as they won't match if ((idx = targetName.IndexOf('[')) == -1) idx = targetName.IndexOf('-') - 1; targetName = targetName.Remove(idx); sb.Append($@""".*<{targetName}.*"":"); const RegexOptions opt = RegexOptions.Compiled | RegexOptions.CultureInvariant; regx = new Regex(sb.ToString(), opt); } var sc = new SearchCriteria(sb.ToString(), false, false, true); return _textArea.SearchText(sc, regx, ref workingArea, true, true); } var foundTarget = false; // _isTextSetLastEvent used so we call this at the first scroll-able event after text was set. // We cannot scroll during used or layout events, and the order of events are: // 1. Used event: text is set in textArea // 2. Layout event: Cannot do the jump yet // 3. Repaint event: Now jump is doable // Hence _isTextSetLastEvent is only set on layout events (during phase 2) if (wasTextSetup) { // Need to call Layout to setup fontsize before searching _textArea.Layout(fixedFontStyle, _textArea.horizontalPad); foundTarget = TryFindByLabel(ref workingArea); _textArea.StopSearching(); // Clear the internals of _textArea from this search; to avoid highlighting // Clear other possible search, so it won't interfere with this. _searchFilterAssembly = string.Empty; // We need to do a Repaint() in order for the view to actually update immediately. if (foundTarget) Repaint(); } return foundTarget; } private void EnableDisableSearchBar() { _searchBarVisible = !_searchBarVisible; if (_searchBarVisible && _searchFieldAssembly != null) { _searchFieldAssembly.SetFocus(); } else if (!_searchBarVisible) { _textArea.StopSearching(); } } private bool _doIgnoreCase = false; private bool _doWholeWordMatch = false; private bool _doRegex = false; internal static string GetDisassembly(MethodInfo method, string options) { try { var result = BurstCompilerService.GetDisassembly(method, options); if (result.IndexOf('\t') >= 0) { result = result.Replace("\t", " "); } // Workaround to remove timings if (result.Contains("Burst timings")) { var index = result.IndexOf("While compiling", StringComparison.Ordinal); if (index > 0) { result = result.Substring(index); } } return result; } catch (Exception e) { return "Failed to compile:\n" + e.Message; } } private static BurstDisassembler.AsmKind FetchAsmKind(BurstTargetCpu cpu, DisassemblyKind kind) { if (kind == DisassemblyKind.Asm) { switch (cpu) { case BurstTargetCpu.ARMV7A_NEON32: case BurstTargetCpu.ARMV8A_AARCH64: case BurstTargetCpu.ARMV8A_AARCH64_HALFFP: case BurstTargetCpu.THUMB2_NEON32: return BurstDisassembler.AsmKind.ARM; case BurstTargetCpu.WASM32: return BurstDisassembler.AsmKind.Wasm; } return BurstDisassembler.AsmKind.Intel; } else { return BurstDisassembler.AsmKind.LLVMIR; } } } /// /// Important: id for namespaces are negative, and ids for jobs are positive. /// This lets us use the id for a job as an index directy into . /// Hence before going from to index, /// One should check whether current item has any children (Only jobs are leafs). /// internal class BurstMethodTreeView : TreeView { private readonly Func _getFilter; private readonly Func<(bool,bool)> _getJobListFilterToggles; private List _targets; public BurstMethodTreeView(TreeViewState state, Func getFilter, Func<(bool,bool)> getJobListFilterToggles) : base(state) { _getFilter = getFilter; _getJobListFilterToggles = getJobListFilterToggles; showBorder = true; } public void Initialize(List targets, bool identicalTargets) { _targets = targets; Reload(); if (!identicalTargets) { ExpandAll(); } } /// /// Assumes that is derived from . /// i.e. types are separated by '+'. /// /// Given type name string. /// (List of namespaces/types, index of method name in ) internal static (List ns, int nsEndIdx) ExtractNameSpaces(in string str) { if (str is null) { throw new ArgumentNullException(nameof(str)); } var nameSpaces = new List(); int len = str.Length; int scope = 0; int previdx = 0; for (int i = 0; i < len; i++) { bool stop = false; char c = str[i]; switch (c) { case '(': // Jump out as we just found argument list!!! stop = true; break; // We keep looking, as classes might have these in name: case '{': case '<': case '[': scope++; break; case '}': case '>': case ']': scope--; break; case '+' when scope == 0: nameSpaces.Add(new StringSlice(str, previdx, i - previdx)); previdx = i + 1; break; } if (stop) { break; } } return (nameSpaces, previdx); } internal static (int idN, List added, List nameSpace) ProcessNewItem(int idN, int idJ, BurstCompileTarget newTarget, List oldNameSpace) { // Find all namespaces used for new target: string fns = newTarget.JobType.FullName; string dn = newTarget.GetDisplayName(); (List newNameSpaces, int nameSpaceEndIdx) = ExtractNameSpaces(fns); int methodNameIdx = nameSpaceEndIdx; if (newTarget.IsStaticMethod) { // Static method does not have the function name in fns, so fix methodNameIdx. methodNameIdx = dn.IndexOf(newTarget.Method.Name, StringComparison.InvariantCulture); // Add the last namespace: newNameSpaces.Add(new StringSlice(dn, nameSpaceEndIdx, methodNameIdx-1 - nameSpaceEndIdx)); } string methodName = dn.Substring(methodNameIdx); int iNewNs = 0; int lNewNs = newNameSpaces.Count; int iOldNs = 0; int lOldNs = oldNameSpace.Count; var added = new List(lNewNs); int depth = 0; // Skip all namespaces shared by previous but increase depth accordingly: for (; iNewNs < lNewNs && iOldNs < lOldNs && newNameSpaces[iNewNs] == oldNameSpace[iOldNs]; depth++, iNewNs++, iOldNs++) {} // Handle all new namespaces: for (; iNewNs < lNewNs; depth++, iNewNs++) { added.Add(new TreeViewItem { id = --idN, depth = depth, displayName = newNameSpaces[iNewNs].ToString()}); } // Add the function name: added.Add(new TreeViewItem { id = idJ, depth = depth, displayName = methodName }); return (idN, added, newNameSpaces); } protected override TreeViewItem BuildRoot() { var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"}; var allItems = new List(); if (_targets != null) { var filter = _getFilter(); var (showUnityNamespaceJobs, showDOTSGeneratedJobs) = _getJobListFilterToggles(); // Have two separate ids so "jobs ids == jobs index". int idJ = 0; int idN = 0; var oldNameSpace = new List(); foreach (BurstCompileTarget target in _targets) { // idJ used as index into _targets, which means it should also take hidden targets into account! idJ++; string displayName = target.GetDisplayName(); bool filtered = (!string.IsNullOrEmpty(filter) && displayName.IndexOf(filter, 0, displayName.Length, StringComparison.InvariantCultureIgnoreCase) < 0) || (!showUnityNamespaceJobs && displayName.StartsWith("Unity.", StringComparison.InvariantCultureIgnoreCase)) || (!showDOTSGeneratedJobs && displayName.Contains(".Generated")); if (filtered) { continue; } try { var (newIdN, added, nameSpace) = ProcessNewItem(idN, idJ, target, oldNameSpace); allItems.AddRange(added); idN = newIdN; oldNameSpace = nameSpace; } catch (Exception ex) { Debug.Log($"Internal error: Could not add {displayName}\n Because: {ex.Message}"); } } } SetupParentsAndChildrenFromDepths(root, allItems.ToList()); return root; } public new IList GetSelection() { IList selection = base.GetSelection(); // selection == non-leaf node => no job selected if (selection.Any() && selection[0] < 0) { return new List(); } return selection; } internal bool TrySelectByDisplayName(string name) { var id = 1; foreach (var t in _targets) { if (t.GetDisplayName() == name) { try { SetSelection(new[] { id }); FrameItem(id); return true; } catch (ArgumentException) { // When a search is made in the job list, such that the job we search for is filtered away // FrameItem(id) will throw a dictionary error. So we catch this, and tell the caller that // it cannot be selected. return false; } } else { ++id; } } return false; } protected override void RowGUI(RowGUIArgs args) { if (!args.item.hasChildren) { var target = _targets[args.item.id - 1]; var wasEnabled = GUI.enabled; GUI.enabled = target.HasRequiredBurstCompileAttributes; base.RowGUI(args); GUI.enabled = wasEnabled; } else { // Label GUI: base.RowGUI(args); } } protected override void SingleClickedItem(int id) { // If labeled click try and fold/expand: if (id < 0) { SetExpanded(id, !IsExpanded(id)); SetSelection(new List()); } } protected override bool CanMultiSelect(TreeViewItem item) { return false; } } }