TH1/Unity/Assets/Editor/ProfilerAutoInstrumentWindow.cs
2026-04-21 20:10:01 +08:00

678 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 DefaultFolder = "Assets/Scripts/TH1_Logic";
private const string MarkerBegin = "// TH1_AUTO_PROFILE_BEGIN";
private const string MarkerEnd = "// TH1_AUTO_PROFILE_END";
private const string IndentUnit = " ";
private static readonly HashSet<string> NonMethodKeywords = new HashSet<string>(StringComparer.Ordinal)
{
"if", "for", "foreach", "while", "switch", "catch", "lock", "using", "nameof", "typeof", "sizeof", "default"
};
private string _targetFolder = DefaultFolder;
private Vector2 _scrollPos;
private string _lastReport = 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 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(720, 420);
}
private void OnGUI()
{
EditorGUILayout.LabelField("批量自动打点Profiler Begin/End", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"会在目录下所有 .cs 文件的方法体内自动插入 Profiler.BeginSample/EndSample。\n" +
"默认目录Assets/Scripts/TH1_Logic\n" +
$"已插入的代码会带标记:{MarkerBegin}",
MessageType.Info);
EditorGUILayout.Space(6);
EditorGUILayout.BeginHorizontal();
_targetFolder = EditorGUILayout.TextField("目标目录", _targetFolder);
if (GUILayout.Button("选择...", GUILayout.Width(80)))
{
SelectFolder();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
if (GUILayout.Button("一键自动打点", GUILayout.Height(34)))
{
RunAutoInstrument();
}
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("执行结果", EditorStyles.boldLabel);
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
EditorGUILayout.TextArea(string.IsNullOrEmpty(_lastReport) ? "尚未执行。" : _lastReport, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
private void SelectFolder()
{
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
var selected = EditorUtility.OpenFolderPanel("选择要自动打点的目录", projectRoot, string.Empty);
if (string.IsNullOrEmpty(selected))
{
return;
}
selected = Path.GetFullPath(selected);
if (!selected.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
{
EditorUtility.DisplayDialog("目录无效", "请选择项目目录内的文件夹。", "确定");
return;
}
var relative = selected.Substring(projectRoot.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
_targetFolder = relative.Replace("\\", "/");
}
private void RunAutoInstrument()
{
var folder = NormalizeFolderPath(_targetFolder);
if (!AssetDatabase.IsValidFolder(folder))
{
EditorUtility.DisplayDialog("目录不存在", $"找不到目录:{folder}", "确定");
return;
}
var folderAbsolute = AssetPathToAbsolutePath(folder);
var files = Directory.GetFiles(folderAbsolute, "*.cs", SearchOption.AllDirectories);
int changed = 0;
int already = 0;
int noMethod = 0;
int failed = 0;
var details = new StringBuilder();
for (int i = 0; i < files.Length; i++)
{
var fullPath = files[i];
var assetPath = AbsolutePathToAssetPath(fullPath);
var result = InstrumentSingleFile(fullPath, assetPath, 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 =
$"目录:{folder}\n" +
$"扫描文件:{files.Length}\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, 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);
if (original.Contains(MarkerBegin))
{
return InstrumentResult.AlreadyInstrumented;
}
var methods = FindMethodBlocks(original);
if (methods.Count == 0)
{
return InstrumentResult.NoMethod;
}
var instrumented = BuildInstrumentedContent(original, methods, assetPath);
if (instrumented == original)
{
return InstrumentResult.NoMethod;
}
File.WriteAllText(fullPath, instrumented, new UTF8Encoding(hasUtf8Bom));
return InstrumentResult.Changed;
}
catch (Exception ex)
{
detail = ex.Message;
return InstrumentResult.Failed;
}
}
private static List<MethodBlock> FindMethodBlocks(string content)
{
var codeOnly = BuildCodeOnlyBuffer(content);
var methods = new List<MethodBlock>();
var stack = new Stack<BlockInfo>();
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;
typeDepth++;
}
else if (typeDepth > 0 && methodDepth == 0 && TryResolveMethodName(codeOnly, i, out var methodName))
{
var method = new MethodBlock
{
OpenBraceIndex = i,
CloseBraceIndex = -1,
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);
}
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 assetPath)
{
string newline = DetectNewLine(content);
var output = new StringBuilder(content.Length + methods.Count * 220);
int cursor = 0;
for (int i = 0; i < methods.Count; i++)
{
var method = methods[i];
if (method.OpenBraceIndex < cursor || method.CloseBraceIndex <= method.OpenBraceIndex)
{
continue;
}
output.Append(content, cursor, method.OpenBraceIndex + 1 - cursor);
string braceIndent = GetLineIndent(content, method.OpenBraceIndex);
string innerIndent = braceIndent + IndentUnit;
string sampleName = BuildSampleName(assetPath, 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;
}
if (cursor < content.Length)
{
output.Append(content, cursor, content.Length - cursor);
}
return output.ToString();
}
private static string BuildSampleName(string assetPath, string methodName)
{
var normalizedPath = assetPath.Replace("\\", "/");
var sample = $"TH1Auto/{normalizedPath}::{methodName}";
return sample.Replace("\\", "\\\\").Replace("\"", "\\\"");
}
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;
}
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;
}
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(";") || header.Contains("="))
{
return false;
}
if (Regex.IsMatch(header, @"\b(return|throw)\b"))
{
return false;
}
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 DefaultFolder;
}
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("\\", "/");
}
}