TH1/Unity/Assets/Editor/ProfilerAutoInstrumentWindow.cs
2026-04-24 16:08:07 +08:00

1450 lines
45 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
public class ProfilerAutoInstrumentWindow : EditorWindow
{
private const string AssetRoot = "Assets";
private const string PersistKey = "TH1.ProfilerAutoInstrument.Selection.v2";
private const string MarkerBegin = "// TH1_AUTO_PROFILE_BEGIN";
private const string MarkerEnd = "// TH1_AUTO_PROFILE_END";
private const string IndentUnit = " ";
private const int MaxRenderFoldersWithoutSearch = 400;
private const int MaxRenderFilesWithoutSearch = 500;
private const int MaxRenderFunctionsWithoutSearch = 300;
private const int MaxRenderItemsWithSearch = 1200;
private static readonly HashSet<string> NonMethodKeywords = new HashSet<string>(StringComparer.Ordinal)
{
"if", "for", "foreach", "while", "switch", "catch", "lock", "using", "nameof", "typeof", "sizeof", "default"
};
[Serializable]
private class PersistSelection
{
public List<string> folders = new List<string>();
public List<string> files = new List<string>();
public List<string> functions = new List<string>();
}
private class FunctionItem
{
public string Key;
public string AssetPath;
public string DeclaringTypeName;
public string MethodName;
public int Line;
public string DisplayText;
}
private class FileSelectionTarget
{
public bool InstrumentAll;
public HashSet<string> FunctionKeys;
}
private Vector2 _folderScrollPos;
private Vector2 _fileScrollPos;
private Vector2 _functionScrollPos;
private Vector2 _reportScrollPos;
private string _folderSearch = string.Empty;
private string _fileSearch = string.Empty;
private string _functionSearch = string.Empty;
private string _lastReport = string.Empty;
private bool _indexBuilt;
private readonly List<string> _folderCandidates = new List<string>();
private readonly List<string> _fileCandidates = new List<string>();
private readonly List<FunctionItem> _functionCandidates = new List<FunctionItem>();
private readonly Dictionary<string, FunctionItem> _functionMap = new Dictionary<string, FunctionItem>(StringComparer.Ordinal);
private readonly HashSet<string> _selectedFolders = new HashSet<string>(StringComparer.Ordinal);
private readonly HashSet<string> _selectedFiles = new HashSet<string>(StringComparer.Ordinal);
private readonly HashSet<string> _selectedFunctionKeys = new HashSet<string>(StringComparer.Ordinal);
private readonly List<string> _filteredFolders = new List<string>();
private readonly List<string> _filteredFiles = new List<string>();
private readonly List<FunctionItem> _filteredFunctions = new List<FunctionItem>();
private string _folderSearchCache = string.Empty;
private string _fileSearchCache = string.Empty;
private string _functionSearchCache = string.Empty;
private enum BlockKind
{
Other,
Type,
Method
}
private struct BlockInfo
{
public BlockKind Kind;
public int MethodIndex;
}
private struct MethodBlock
{
public int OpenBraceIndex;
public int CloseBraceIndex;
public string DeclaringTypeName;
public string MethodName;
}
private enum InstrumentResult
{
Changed,
AlreadyInstrumented,
NoMethod,
Failed
}
[MenuItem("Tools/TH1/Profiler Auto Instrument")]
public static void ShowWindow()
{
var window = GetWindow<ProfilerAutoInstrumentWindow>("Profiler Auto Instrument");
window.minSize = new Vector2(1080, 560);
}
private void OnEnable()
{
LoadSelection();
}
private void OnGUI()
{
EditorGUILayout.LabelField("自动打点(按文件夹 / 文件 / 函数)", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"支持按文件夹、文件、函数进行精准勾选打点;可搜索并保存本地勾选记录。\n" +
$"已插入的代码会带标记:{MarkerBegin}",
MessageType.Info);
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("刷新索引", GUILayout.Width(100)))
{
BuildIndex();
}
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField(
$"已选:文件夹 {_selectedFolders.Count} / 文件 {_selectedFiles.Count} / 函数 {_selectedFunctionKeys.Count}",
EditorStyles.miniLabel,
GUILayout.Width(360));
EditorGUILayout.EndHorizontal();
if (!_indexBuilt)
{
EditorGUILayout.HelpBox("索引尚未构建,请点击“刷新索引”。", MessageType.Warning);
return;
}
EditorGUILayout.Space(6);
EditorGUILayout.BeginHorizontal();
DrawFolderPanel();
DrawFilePanel();
DrawFunctionPanel();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
if (GUILayout.Button("开始自动打点", GUILayout.Height(34)))
{
RunAutoInstrument();
}
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("执行结果", EditorStyles.boldLabel);
_reportScrollPos = EditorGUILayout.BeginScrollView(_reportScrollPos, GUILayout.Height(150));
EditorGUILayout.TextArea(string.IsNullOrEmpty(_lastReport) ? "尚未执行。" : _lastReport, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
private void DrawFolderPanel()
{
EditorGUILayout.BeginVertical("box", GUILayout.Width(position.width / 3f - 8), GUILayout.ExpandHeight(true));
EditorGUILayout.LabelField($"文件夹 ({_folderCandidates.Count})", EditorStyles.boldLabel);
_folderSearch = EditorGUILayout.TextField("搜索", _folderSearch);
RebuildFilteredFoldersIfNeeded();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("全选筛选项"))
{
bool changed = false;
foreach (var folder in _filteredFolders)
{
changed |= _selectedFolders.Add(folder);
}
if (changed) SaveSelection();
}
if (GUILayout.Button("取消筛选项"))
{
bool changed = false;
var toRemove = new List<string>();
foreach (var folder in _selectedFolders)
{
if (_filteredFolders.Contains(folder)) toRemove.Add(folder);
}
foreach (var folder in toRemove) changed |= _selectedFolders.Remove(folder);
if (changed) SaveSelection();
}
EditorGUILayout.EndHorizontal();
_folderScrollPos = EditorGUILayout.BeginScrollView(_folderScrollPos);
int renderLimit = string.IsNullOrWhiteSpace(_folderSearch) ? MaxRenderFoldersWithoutSearch : MaxRenderItemsWithSearch;
int renderCount = Math.Min(renderLimit, _filteredFolders.Count);
if (_filteredFolders.Count > renderCount)
{
EditorGUILayout.HelpBox($"结果过多,仅渲染前 {renderCount} 条,请继续搜索缩小范围。", MessageType.None);
}
bool anyChanged = false;
for (int i = 0; i < renderCount; i++)
{
var folder = _filteredFolders[i];
bool selected = _selectedFolders.Contains(folder);
bool newSelected = EditorGUILayout.ToggleLeft(folder, selected);
if (newSelected == selected) continue;
anyChanged = true;
if (newSelected) _selectedFolders.Add(folder);
else _selectedFolders.Remove(folder);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
if (anyChanged) SaveSelection();
}
private void DrawFilePanel()
{
EditorGUILayout.BeginVertical("box", GUILayout.Width(position.width / 3f - 8), GUILayout.ExpandHeight(true));
EditorGUILayout.LabelField($"文件 ({_fileCandidates.Count})", EditorStyles.boldLabel);
_fileSearch = EditorGUILayout.TextField("搜索", _fileSearch);
RebuildFilteredFilesIfNeeded();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("全选筛选项"))
{
bool changed = false;
foreach (var file in _filteredFiles)
{
changed |= _selectedFiles.Add(file);
}
if (changed) SaveSelection();
}
if (GUILayout.Button("取消筛选项"))
{
bool changed = false;
var toRemove = new List<string>();
foreach (var file in _selectedFiles)
{
if (_filteredFiles.Contains(file)) toRemove.Add(file);
}
foreach (var file in toRemove) changed |= _selectedFiles.Remove(file);
if (changed) SaveSelection();
}
EditorGUILayout.EndHorizontal();
_fileScrollPos = EditorGUILayout.BeginScrollView(_fileScrollPos);
int renderLimit = string.IsNullOrWhiteSpace(_fileSearch) ? MaxRenderFilesWithoutSearch : MaxRenderItemsWithSearch;
int renderCount = Math.Min(renderLimit, _filteredFiles.Count);
if (_filteredFiles.Count > renderCount)
{
EditorGUILayout.HelpBox($"结果过多,仅渲染前 {renderCount} 条,请继续搜索缩小范围。", MessageType.None);
}
bool anyChanged = false;
for (int i = 0; i < renderCount; i++)
{
var file = _filteredFiles[i];
bool selected = _selectedFiles.Contains(file);
bool newSelected = EditorGUILayout.ToggleLeft(file, selected);
if (newSelected == selected) continue;
anyChanged = true;
if (newSelected) _selectedFiles.Add(file);
else _selectedFiles.Remove(file);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
if (anyChanged) SaveSelection();
}
private void DrawFunctionPanel()
{
EditorGUILayout.BeginVertical("box", GUILayout.Width(position.width / 3f - 8), GUILayout.ExpandHeight(true));
EditorGUILayout.LabelField($"函数 ({_functionCandidates.Count})", EditorStyles.boldLabel);
_functionSearch = EditorGUILayout.TextField("搜索", _functionSearch);
RebuildFilteredFunctionsIfNeeded();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("全选筛选项"))
{
bool changed = false;
foreach (var func in _filteredFunctions)
{
changed |= _selectedFunctionKeys.Add(func.Key);
}
if (changed) SaveSelection();
}
if (GUILayout.Button("取消筛选项"))
{
bool changed = false;
var toRemove = new List<string>();
foreach (var funcKey in _selectedFunctionKeys)
{
if (!_functionMap.TryGetValue(funcKey, out var func)) continue;
if (_filteredFunctions.Contains(func)) toRemove.Add(funcKey);
}
foreach (var key in toRemove) changed |= _selectedFunctionKeys.Remove(key);
if (changed) SaveSelection();
}
EditorGUILayout.EndHorizontal();
_functionScrollPos = EditorGUILayout.BeginScrollView(_functionScrollPos);
int renderLimit = string.IsNullOrWhiteSpace(_functionSearch) ? MaxRenderFunctionsWithoutSearch : MaxRenderItemsWithSearch;
int renderCount = Math.Min(renderLimit, _filteredFunctions.Count);
if (_filteredFunctions.Count > renderCount)
{
EditorGUILayout.HelpBox($"结果过多,仅渲染前 {renderCount} 条,请继续搜索缩小范围。", MessageType.None);
}
bool anyChanged = false;
for (int i = 0; i < renderCount; i++)
{
var func = _filteredFunctions[i];
bool selected = _selectedFunctionKeys.Contains(func.Key);
bool newSelected = EditorGUILayout.ToggleLeft(func.DisplayText, selected);
if (newSelected == selected) continue;
anyChanged = true;
if (newSelected) _selectedFunctionKeys.Add(func.Key);
else _selectedFunctionKeys.Remove(func.Key);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
if (anyChanged) SaveSelection();
}
private void BuildIndex()
{
_folderCandidates.Clear();
_fileCandidates.Clear();
_functionCandidates.Clear();
_functionMap.Clear();
var folders = new HashSet<string>(StringComparer.Ordinal);
var csFiles = Directory.GetFiles(Application.dataPath, "*.cs", SearchOption.AllDirectories);
Array.Sort(csFiles, StringComparer.Ordinal);
foreach (var fullPath in csFiles)
{
var assetPath = AbsolutePathToAssetPath(fullPath);
if (!assetPath.StartsWith(AssetRoot, StringComparison.Ordinal)) continue;
_fileCandidates.Add(assetPath);
CollectParentFolders(assetPath, folders);
string content;
try
{
content = File.ReadAllText(fullPath);
}
catch
{
continue;
}
var methods = FindMethodBlocks(content);
if (methods.Count == 0) continue;
var occurrenceMap = new Dictionary<string, int>(StringComparer.Ordinal);
for (int i = 0; i < methods.Count; i++)
{
var method = methods[i];
occurrenceMap.TryGetValue(method.MethodName, out int occurrence);
occurrence++;
occurrenceMap[method.MethodName] = occurrence;
var line = GetLineNumber(content, method.OpenBraceIndex);
var item = new FunctionItem
{
AssetPath = assetPath,
DeclaringTypeName = method.DeclaringTypeName,
MethodName = method.MethodName,
Line = line,
Key = BuildFunctionKey(assetPath, method.MethodName, occurrence),
DisplayText = $"{method.DeclaringTypeName}.{method.MethodName} ({assetPath}:{line})"
};
_functionCandidates.Add(item);
_functionMap[item.Key] = item;
}
}
_folderCandidates.AddRange(folders);
_folderCandidates.Sort(StringComparer.Ordinal);
_fileCandidates.Sort(StringComparer.Ordinal);
_functionCandidates.Sort((a, b) =>
{
int pathCmp = string.CompareOrdinal(a.AssetPath, b.AssetPath);
if (pathCmp != 0) return pathCmp;
int nameCmp = string.CompareOrdinal(a.MethodName, b.MethodName);
if (nameCmp != 0) return nameCmp;
return a.Line.CompareTo(b.Line);
});
PruneSelections();
InvalidateFilteredCaches();
_indexBuilt = true;
SaveSelection();
}
private void InvalidateFilteredCaches()
{
_folderSearchCache = null;
_fileSearchCache = null;
_functionSearchCache = null;
_filteredFolders.Clear();
_filteredFiles.Clear();
_filteredFunctions.Clear();
}
private void RebuildFilteredFoldersIfNeeded()
{
if (string.Equals(_folderSearchCache, _folderSearch, StringComparison.Ordinal)) return;
_folderSearchCache = _folderSearch;
_filteredFolders.Clear();
if (string.IsNullOrWhiteSpace(_folderSearch))
{
foreach (var folder in _folderCandidates)
{
if (!_selectedFolders.Contains(folder)) continue;
_filteredFolders.Add(folder);
}
return;
}
foreach (var folder in _folderCandidates)
{
if (!IsMatch(folder, _folderSearch)) continue;
_filteredFolders.Add(folder);
}
}
private void RebuildFilteredFilesIfNeeded()
{
if (string.Equals(_fileSearchCache, _fileSearch, StringComparison.Ordinal)) return;
_fileSearchCache = _fileSearch;
_filteredFiles.Clear();
if (string.IsNullOrWhiteSpace(_fileSearch))
{
foreach (var file in _fileCandidates)
{
if (!_selectedFiles.Contains(file)) continue;
_filteredFiles.Add(file);
}
return;
}
foreach (var file in _fileCandidates)
{
if (!IsMatch(file, _fileSearch)) continue;
_filteredFiles.Add(file);
}
}
private void RebuildFilteredFunctionsIfNeeded()
{
if (string.Equals(_functionSearchCache, _functionSearch, StringComparison.Ordinal)) return;
_functionSearchCache = _functionSearch;
_filteredFunctions.Clear();
if (string.IsNullOrWhiteSpace(_functionSearch))
{
foreach (var func in _functionCandidates)
{
if (!_selectedFunctionKeys.Contains(func.Key)) continue;
_filteredFunctions.Add(func);
}
return;
}
foreach (var func in _functionCandidates)
{
if (!IsFunctionMatch(func, _functionSearch)) continue;
_filteredFunctions.Add(func);
}
}
private void CollectParentFolders(string assetPath, HashSet<string> folders)
{
var folder = Path.GetDirectoryName(assetPath)?.Replace("\\", "/");
while (!string.IsNullOrEmpty(folder) && folder.StartsWith(AssetRoot, StringComparison.Ordinal))
{
folders.Add(folder);
int slash = folder.LastIndexOf('/');
if (slash <= 0) break;
folder = folder.Substring(0, slash);
}
}
private void PruneSelections()
{
_selectedFolders.RemoveWhere(path => !_folderCandidates.Contains(path));
_selectedFiles.RemoveWhere(path => !_fileCandidates.Contains(path));
_selectedFunctionKeys.RemoveWhere(key => !_functionMap.ContainsKey(key));
}
private void LoadSelection()
{
_selectedFolders.Clear();
_selectedFiles.Clear();
_selectedFunctionKeys.Clear();
if (!EditorPrefs.HasKey(PersistKey)) return;
var json = EditorPrefs.GetString(PersistKey, string.Empty);
if (string.IsNullOrEmpty(json)) return;
PersistSelection state;
try
{
state = JsonUtility.FromJson<PersistSelection>(json);
}
catch
{
return;
}
if (state == null) return;
if (state.folders != null) foreach (var item in state.folders) _selectedFolders.Add(item);
if (state.files != null) foreach (var item in state.files) _selectedFiles.Add(item);
if (state.functions != null) foreach (var item in state.functions) _selectedFunctionKeys.Add(item);
}
private void SaveSelection()
{
var state = new PersistSelection
{
folders = new List<string>(_selectedFolders),
files = new List<string>(_selectedFiles),
functions = new List<string>(_selectedFunctionKeys)
};
EditorPrefs.SetString(PersistKey, JsonUtility.ToJson(state));
InvalidateFilteredCaches();
}
private static bool IsMatch(string text, string query)
{
return string.IsNullOrWhiteSpace(query) ||
text.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool IsFunctionMatch(FunctionItem item, string query)
{
if (string.IsNullOrWhiteSpace(query)) return true;
return item.MethodName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0 ||
item.AssetPath.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static string BuildFunctionKey(string assetPath, string methodName, int occurrence)
{
return $"{assetPath}|{methodName}|{occurrence}";
}
private static int GetLineNumber(string content, int index)
{
int line = 1;
int max = Mathf.Clamp(index, 0, Math.Max(0, content.Length));
for (int i = 0; i < max; i++)
{
if (content[i] == '\n') line++;
}
return line;
}
private Dictionary<string, FileSelectionTarget> BuildInstrumentTargets()
{
var targets = new Dictionary<string, FileSelectionTarget>(StringComparer.Ordinal);
foreach (var folder in _selectedFolders)
{
string prefix = folder.EndsWith("/", StringComparison.Ordinal) ? folder : folder + "/";
foreach (var assetPath in _fileCandidates)
{
if (!assetPath.StartsWith(prefix, StringComparison.Ordinal)) continue;
if (!targets.TryGetValue(assetPath, out var target))
{
target = new FileSelectionTarget();
targets[assetPath] = target;
}
target.InstrumentAll = true;
target.FunctionKeys = null;
}
}
foreach (var file in _selectedFiles)
{
if (!targets.TryGetValue(file, out var target))
{
target = new FileSelectionTarget();
targets[file] = target;
}
target.InstrumentAll = true;
target.FunctionKeys = null;
}
foreach (var key in _selectedFunctionKeys)
{
if (!_functionMap.TryGetValue(key, out var function)) continue;
if (!targets.TryGetValue(function.AssetPath, out var target))
{
target = new FileSelectionTarget();
targets[function.AssetPath] = target;
}
if (target.InstrumentAll) continue;
target.FunctionKeys ??= new HashSet<string>(StringComparer.Ordinal);
target.FunctionKeys.Add(key);
}
return targets;
}
private void RunAutoInstrument()
{
var targets = BuildInstrumentTargets();
if (targets.Count == 0)
{
EditorUtility.DisplayDialog("未选择内容", "请至少勾选一个文件夹、文件或函数。", "确定");
return;
}
var assetPaths = new List<string>(targets.Keys);
assetPaths.Sort(StringComparer.Ordinal);
int changed = 0;
int already = 0;
int noMethod = 0;
int failed = 0;
var details = new StringBuilder();
for (int i = 0; i < assetPaths.Count; i++)
{
var assetPath = assetPaths[i];
var fullPath = AssetPathToAbsolutePath(assetPath);
var target = targets[assetPath];
var result = InstrumentSingleFile(
fullPath,
assetPath,
target.InstrumentAll ? null : target.FunctionKeys,
out var detail);
switch (result)
{
case InstrumentResult.Changed:
changed++;
details.AppendLine($"[Changed] {assetPath}");
break;
case InstrumentResult.AlreadyInstrumented:
already++;
break;
case InstrumentResult.NoMethod:
noMethod++;
break;
case InstrumentResult.Failed:
failed++;
details.AppendLine($"[Failed] {assetPath} -> {detail}");
break;
default:
throw new ArgumentOutOfRangeException();
}
}
AssetDatabase.Refresh();
_lastReport =
$"目标文件:{assetPaths.Count}\n" +
$"已打点:{changed}\n" +
$"已存在打点:{already}\n" +
$"无可打点方法:{noMethod}\n" +
$"失败:{failed}\n\n" +
details;
Debug.Log($"[ProfilerAutoInstrument] 完成。Changed={changed}, Already={already}, NoMethod={noMethod}, Failed={failed}");
}
private static InstrumentResult InstrumentSingleFile(
string fullPath,
string assetPath,
HashSet<string> selectedFunctionKeys,
out string detail)
{
detail = string.Empty;
try
{
var rawBytes = File.ReadAllBytes(fullPath);
bool hasUtf8Bom = rawBytes.Length >= 3 &&
rawBytes[0] == 0xEF &&
rawBytes[1] == 0xBB &&
rawBytes[2] == 0xBF;
var original = File.ReadAllText(fullPath);
var methods = FindMethodBlocks(original);
if (methods.Count == 0)
{
return InstrumentResult.NoMethod;
}
var targetMethods = ResolveTargetMethods(original, methods, assetPath, selectedFunctionKeys);
if (targetMethods.Count == 0)
{
return InstrumentResult.NoMethod;
}
bool allAlreadyInstrumented = true;
for (int i = 0; i < targetMethods.Count; i++)
{
if (IsMethodAlreadyInstrumented(original, targetMethods[i])) continue;
allAlreadyInstrumented = false;
break;
}
if (allAlreadyInstrumented)
{
return InstrumentResult.AlreadyInstrumented;
}
var instrumented = BuildInstrumentedContent(original, targetMethods);
if (instrumented == original)
{
return InstrumentResult.AlreadyInstrumented;
}
File.WriteAllText(fullPath, instrumented, new UTF8Encoding(hasUtf8Bom));
return InstrumentResult.Changed;
}
catch (Exception ex)
{
detail = ex.Message;
return InstrumentResult.Failed;
}
}
private static List<MethodBlock> ResolveTargetMethods(
string content,
List<MethodBlock> methods,
string assetPath,
HashSet<string> selectedFunctionKeys)
{
if (selectedFunctionKeys == null || selectedFunctionKeys.Count == 0)
{
return methods;
}
var result = new List<MethodBlock>();
var occurrenceMap = new Dictionary<string, int>(StringComparer.Ordinal);
for (int i = 0; i < methods.Count; i++)
{
var method = methods[i];
occurrenceMap.TryGetValue(method.MethodName, out int occurrence);
occurrence++;
occurrenceMap[method.MethodName] = occurrence;
var key = BuildFunctionKey(assetPath, method.MethodName, occurrence);
if (!selectedFunctionKeys.Contains(key)) continue;
result.Add(method);
}
return result;
}
private static List<MethodBlock> FindMethodBlocks(string content)
{
var codeOnly = BuildCodeOnlyBuffer(content);
var methods = new List<MethodBlock>();
var stack = new Stack<BlockInfo>();
var typeNameStack = new List<string>();
int typeDepth = 0;
int methodDepth = 0;
for (int i = 0; i < codeOnly.Length; i++)
{
char c = codeOnly[i];
if (c == '{')
{
var block = new BlockInfo { Kind = BlockKind.Other, MethodIndex = -1 };
if (LooksLikeTypeBlock(codeOnly, i))
{
block.Kind = BlockKind.Type;
if (!TryResolveTypeName(codeOnly, i, out var typeName))
{
typeName = "UnknownType";
}
typeNameStack.Add(typeName);
typeDepth++;
}
else if (typeDepth > 0 && methodDepth == 0 && TryResolveMethodName(codeOnly, i, out var methodName))
{
var method = new MethodBlock
{
OpenBraceIndex = i,
CloseBraceIndex = -1,
DeclaringTypeName = BuildDeclaringTypeName(typeNameStack),
MethodName = methodName
};
methods.Add(method);
block.Kind = BlockKind.Method;
block.MethodIndex = methods.Count - 1;
methodDepth++;
}
stack.Push(block);
continue;
}
if (c != '}' || stack.Count == 0)
{
continue;
}
var closedBlock = stack.Pop();
if (closedBlock.Kind == BlockKind.Type)
{
typeDepth = Math.Max(0, typeDepth - 1);
if (typeNameStack.Count > 0)
{
typeNameStack.RemoveAt(typeNameStack.Count - 1);
}
}
else if (closedBlock.Kind == BlockKind.Method)
{
methodDepth = Math.Max(0, methodDepth - 1);
if (closedBlock.MethodIndex >= 0 && closedBlock.MethodIndex < methods.Count)
{
var method = methods[closedBlock.MethodIndex];
method.CloseBraceIndex = i;
methods[closedBlock.MethodIndex] = method;
}
}
}
methods.RemoveAll(m => m.CloseBraceIndex <= m.OpenBraceIndex);
return methods;
}
private static string BuildInstrumentedContent(string content, List<MethodBlock> methods)
{
string newline = DetectNewLine(content);
var output = new StringBuilder(content.Length + methods.Count * 220);
int cursor = 0;
bool changed = false;
for (int i = 0; i < methods.Count; i++)
{
var method = methods[i];
if (method.OpenBraceIndex < cursor || method.CloseBraceIndex <= method.OpenBraceIndex)
{
continue;
}
if (IsMethodAlreadyInstrumented(content, method))
{
continue;
}
output.Append(content, cursor, method.OpenBraceIndex + 1 - cursor);
string braceIndent = GetLineIndent(content, method.OpenBraceIndex);
string innerIndent = braceIndent + IndentUnit;
string sampleName = BuildSampleName(method.DeclaringTypeName, method.MethodName);
output.Append(newline);
output.Append(innerIndent).Append(MarkerBegin).Append(newline);
output.Append(innerIndent).Append("UnityEngine.Profiling.Profiler.BeginSample(\"").Append(sampleName).Append("\");").Append(newline);
output.Append(innerIndent).Append("try").Append(newline);
output.Append(innerIndent).Append("{").Append(newline);
int bodyStart = method.OpenBraceIndex + 1;
output.Append(content, bodyStart, method.CloseBraceIndex - bodyStart);
output.Append(newline);
output.Append(innerIndent).Append("}").Append(newline);
output.Append(innerIndent).Append("finally").Append(newline);
output.Append(innerIndent).Append("{").Append(newline);
output.Append(innerIndent).Append(IndentUnit).Append("UnityEngine.Profiling.Profiler.EndSample();").Append(newline);
output.Append(innerIndent).Append("}").Append(newline);
output.Append(innerIndent).Append(MarkerEnd).Append(newline);
output.Append(braceIndent);
cursor = method.CloseBraceIndex;
changed = true;
}
if (!changed)
{
return content;
}
if (cursor < content.Length)
{
output.Append(content, cursor, content.Length - cursor);
}
return output.ToString();
}
private static string BuildSampleName(string declaringTypeName, string methodName)
{
string safeTypeName = string.IsNullOrEmpty(declaringTypeName) ? "UnknownType" : declaringTypeName;
var sample = $"TH1Auto/{safeTypeName}.{methodName}";
return sample.Replace("\\", "\\\\").Replace("\"", "\\\"");
}
private static bool TryResolveTypeName(string codeOnly, int braceIndex, out string typeName)
{
typeName = null;
int headerStart = FindHeaderStart(codeOnly, braceIndex);
int length = braceIndex - headerStart;
if (length <= 0)
{
return false;
}
string header = codeOnly.Substring(headerStart, length);
var match = Regex.Match(
header,
@"\b(?:class|struct|interface|enum)\s+(@?[A-Za-z_][A-Za-z0-9_]*)|\brecord(?:\s+(?:class|struct))?\s+(@?[A-Za-z_][A-Za-z0-9_]*)");
if (!match.Success)
{
return false;
}
typeName = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value;
if (!string.IsNullOrEmpty(typeName) && typeName[0] == '@')
{
typeName = typeName.Substring(1);
}
return !string.IsNullOrEmpty(typeName);
}
private static string BuildDeclaringTypeName(List<string> typeNameStack)
{
if (typeNameStack == null || typeNameStack.Count == 0)
{
return "UnknownType";
}
if (typeNameStack.Count == 1)
{
return typeNameStack[0];
}
var builder = new StringBuilder(typeNameStack[0]);
for (int i = 1; i < typeNameStack.Count; i++)
{
builder.Append('.').Append(typeNameStack[i]);
}
return builder.ToString();
}
private static bool LooksLikeTypeBlock(string codeOnly, int braceIndex)
{
int headerStart = FindHeaderStart(codeOnly, braceIndex);
int length = braceIndex - headerStart;
if (length <= 0)
{
return false;
}
string header = codeOnly.Substring(headerStart, length);
if (Regex.IsMatch(header, @"\bnamespace\b"))
{
return false;
}
return Regex.IsMatch(header, @"\b(class|struct|interface|enum|record)\b");
}
private static bool TryResolveMethodName(string codeOnly, int braceIndex, out string methodName)
{
methodName = null;
int segmentStart = FindHeaderStart(codeOnly, braceIndex);
int closeParen = FindPreviousChar(codeOnly, ')', braceIndex - 1);
while (closeParen >= 0 && closeParen >= segmentStart)
{
int openParen = FindMatchingOpenParen(codeOnly, closeParen);
if (openParen < 0 || openParen < segmentStart)
{
return false;
}
int nameEnd = PreviousNonWhitespace(codeOnly, openParen - 1);
if (nameEnd < 0)
{
return false;
}
nameEnd = SkipGenericTypeParameterListBackward(codeOnly, nameEnd);
if (nameEnd < 0)
{
return false;
}
int nameStart = nameEnd;
while (nameStart >= 0 && IsIdentifierChar(codeOnly[nameStart]))
{
nameStart--;
}
nameStart++;
if (nameStart > nameEnd)
{
return false;
}
methodName = codeOnly.Substring(nameStart, nameEnd - nameStart + 1);
if (string.IsNullOrEmpty(methodName))
{
return false;
}
if (methodName[0] == '@')
{
methodName = methodName.Substring(1);
}
if (methodName == "base" || methodName == "this")
{
closeParen = FindPreviousChar(codeOnly, ')', openParen - 1);
continue;
}
var previousIdentifier = GetPreviousIdentifier(codeOnly, nameStart - 1);
if (previousIdentifier == "new")
{
return false;
}
if (NonMethodKeywords.Contains(methodName))
{
return false;
}
int checkLength = braceIndex - segmentStart;
if (checkLength <= 0)
{
return false;
}
string header = codeOnly.Substring(segmentStart, checkLength);
if (header.Contains("=>") || header.Contains(";"))
{
return false;
}
if (HasAssignmentOutsideParameterList(header))
{
return false;
}
if (Regex.IsMatch(header, @"\b(return|throw)\b"))
{
return false;
}
return true;
}
return false;
}
private static bool IsMethodAlreadyInstrumented(string content, MethodBlock method)
{
int index = method.OpenBraceIndex + 1;
while (index < method.CloseBraceIndex && char.IsWhiteSpace(content[index]))
{
index++;
}
if (index + MarkerBegin.Length > method.CloseBraceIndex)
{
return false;
}
return string.Compare(content, index, MarkerBegin, 0, MarkerBegin.Length, StringComparison.Ordinal) == 0;
}
// Support generic method declarations like Foo<T>(...) when resolving method names.
private static int SkipGenericTypeParameterListBackward(string text, int endIndex)
{
if (endIndex < 0)
{
return -1;
}
if (text[endIndex] != '>')
{
return endIndex;
}
int depth = 0;
for (int i = endIndex; i >= 0; i--)
{
char c = text[i];
if (c == '>')
{
depth++;
continue;
}
if (c != '<')
{
continue;
}
depth--;
if (depth == 0)
{
return PreviousNonWhitespace(text, i - 1);
}
}
return -1;
}
private static string GetPreviousIdentifier(string text, int index)
{
int i = index;
while (i >= 0 && char.IsWhiteSpace(text[i]))
{
i--;
}
if (i < 0 || !IsIdentifierChar(text[i]))
{
return null;
}
int end = i;
while (i >= 0 && IsIdentifierChar(text[i]))
{
i--;
}
int start = i + 1;
var token = text.Substring(start, end - start + 1);
return token.StartsWith("@", StringComparison.Ordinal) ? token.Substring(1) : token;
}
private static bool HasAssignmentOutsideParameterList(string header)
{
int parenDepth = 0;
for (int i = 0; i < header.Length; i++)
{
char c = header[i];
if (c == '(')
{
parenDepth++;
continue;
}
if (c == ')')
{
parenDepth = Math.Max(0, parenDepth - 1);
continue;
}
if (c != '=' || parenDepth != 0)
{
continue;
}
char prev = i > 0 ? header[i - 1] : '\0';
char next = i + 1 < header.Length ? header[i + 1] : '\0';
if (prev == '=' || prev == '!' || prev == '<' || prev == '>' || next == '=')
{
continue;
}
return true;
}
return false;
}
private static string BuildCodeOnlyBuffer(string content)
{
var chars = content.ToCharArray();
bool inSingleLineComment = false;
bool inMultiLineComment = false;
bool inString = false;
bool inChar = false;
bool verbatimString = false;
for (int i = 0; i < chars.Length; i++)
{
char c = chars[i];
char next = i + 1 < chars.Length ? chars[i + 1] : '\0';
if (inSingleLineComment)
{
if (c != '\r' && c != '\n')
{
chars[i] = ' ';
}
else
{
inSingleLineComment = false;
}
continue;
}
if (inMultiLineComment)
{
chars[i] = ' ';
if (c == '*' && next == '/')
{
chars[i + 1] = ' ';
i++;
inMultiLineComment = false;
}
continue;
}
if (inString)
{
chars[i] = ' ';
if (verbatimString)
{
if (c == '"' && next == '"')
{
chars[i + 1] = ' ';
i++;
}
else if (c == '"')
{
inString = false;
verbatimString = false;
}
}
else
{
if (c == '\\' && i + 1 < chars.Length)
{
chars[i + 1] = ' ';
i++;
}
else if (c == '"')
{
inString = false;
}
}
continue;
}
if (inChar)
{
chars[i] = ' ';
if (c == '\\' && i + 1 < chars.Length)
{
chars[i + 1] = ' ';
i++;
}
else if (c == '\'')
{
inChar = false;
}
continue;
}
if (c == '/' && next == '/')
{
chars[i] = ' ';
chars[i + 1] = ' ';
i++;
inSingleLineComment = true;
continue;
}
if (c == '/' && next == '*')
{
chars[i] = ' ';
chars[i + 1] = ' ';
i++;
inMultiLineComment = true;
continue;
}
if (c == '"')
{
chars[i] = ' ';
inString = true;
verbatimString =
(i > 0 && chars[i - 1] == '@') ||
(i > 1 && chars[i - 1] == '$' && chars[i - 2] == '@') ||
(i > 1 && chars[i - 1] == '@' && chars[i - 2] == '$');
continue;
}
if (c == '\'')
{
chars[i] = ' ';
inChar = true;
}
}
return new string(chars);
}
private static int FindMatchingOpenParen(string codeOnly, int closeParenIndex)
{
int depth = 0;
for (int i = closeParenIndex; i >= 0; i--)
{
char c = codeOnly[i];
if (c == ')')
{
depth++;
continue;
}
if (c != '(')
{
continue;
}
depth--;
if (depth == 0)
{
return i;
}
}
return -1;
}
private static int FindHeaderStart(string codeOnly, int index)
{
for (int i = index - 1; i >= 0; i--)
{
char c = codeOnly[i];
if (c == ';' || c == '{' || c == '}')
{
return i + 1;
}
}
return 0;
}
private static int PreviousNonWhitespace(string text, int index)
{
for (int i = index; i >= 0; i--)
{
if (!char.IsWhiteSpace(text[i]))
{
return i;
}
}
return -1;
}
private static int FindPreviousChar(string text, char target, int startIndex)
{
for (int i = startIndex; i >= 0; i--)
{
char c = text[i];
if (c == target)
{
return i;
}
if (c == ';' || c == '{' || c == '}')
{
return -1;
}
}
return -1;
}
private static bool IsIdentifierChar(char c)
{
return char.IsLetterOrDigit(c) || c == '_' || c == '@';
}
private static string DetectNewLine(string content)
{
return content.Contains("\r\n") ? "\r\n" : "\n";
}
private static string GetLineIndent(string content, int index)
{
int lineStart = content.LastIndexOf('\n', Mathf.Clamp(index, 0, Math.Max(0, content.Length - 1)));
lineStart = lineStart < 0 ? 0 : lineStart + 1;
int i = lineStart;
while (i < content.Length && (content[i] == ' ' || content[i] == '\t'))
{
i++;
}
return content.Substring(lineStart, i - lineStart);
}
private static string NormalizeFolderPath(string folder)
{
if (string.IsNullOrWhiteSpace(folder))
{
return AssetRoot;
}
var normalized = folder.Replace("\\", "/").Trim();
if (!normalized.StartsWith("Assets", StringComparison.OrdinalIgnoreCase))
{
normalized = "Assets/" + normalized.TrimStart('/');
}
return normalized.TrimEnd('/');
}
private static string AssetPathToAbsolutePath(string assetPath)
{
return Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath));
}
private static string AbsolutePathToAssetPath(string absolutePath)
{
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
var full = Path.GetFullPath(absolutePath);
var relative = full.Substring(projectRoot.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return relative.Replace("\\", "/");
}
}