自动将UnityEngine.Profiling的 Profiler采样绑定到某个类的每个方法
在使用 UnityEngine.Profiling 时,我们可以对代码的部分进行采样,以了解它们在运行时消耗的时间和空间,大致如下:
using UnityEngine.Profiling;
public class MyClass
{
void MyMethodA()
{
Profiler.BeginSample("MyClass.MyMethodA");
// Code here
Profiler.EndSample();
}
void MyMethodB()
{
Profiler.BeginSample("MyClass.MyMethodB");
// Code here
Profiler.EndSample();
}
void MyMethodC()
{
Profiler.BeginSample("MyClass.MyMethodC");
// Code here
Profiler.EndSample();
}
// ... and so on.
}
在这个类中,我恰当地把 Profiler 的采样绑定到了每个方法上,以便我能对它们进行分析。
在大量编写方法时,不断写 Profiler.BeginSample 和 Profiler.EndSample,在我看来有点乏味。
在某个特定的类中,我希望让“每一个”方法都按上述格式进行分析。我想知道是否可以只在代码中用几行就指定某些内容,让Unity自动为该类的每个方法创建这样的采样,而无需我逐个写出它们。
这可行吗?
N.B. 这与我此前的另一个问题有关,但并不完全相同,参见此处:this other question。
解决方案
Marker作用域可释放对象(using块)
如前文所述,内置功能中并没有类似的东西。
当然可以启用 Deep Profile,它基本上会实现你想要的效果——但这是针对每一种类型,而不仅仅是某一个类型。如果这对你已经足够,你也可以暂时开启,然后再在后续关闭。Deep Profile 的开销当然相当大,且肯定会影响分析结果,因为它本身也会对如内存、帧率等产生影响。
Profiler故意保持“手动”以便获得更好的控制。
所以使用纯内置/简单工具时,using 范围如你在前一个问题中提到的 是最快、最直接的做法,前提是你偶尔才需要这样做。
IL织入
如果你确实想要更自动化的方式,你可能在寻找一个自定义的 IL Weaver。它们基本上允许在编译时注入生成的代码,而不需要你逐条手写。
你可以看看 MewWeaver(它有点老了),它能给你一个起点。他们的示例用例甚至与为方法添加一个用于被Profile标记包装的属性相关 ;)
它取决于包 com.unity.nuget.mono-cecil
所以基本上你需要在一个 AssemblyDefinition 中实现你自己的 ILPostProcessor,该定义仅在编辑器编译,且至少引用这两个程序集
Unity.CompilationPipeline.CommonMono.Cecil
在那里你就可以基本上扫描该属性并相应地包装该类型的方法。你不会在官方API中找到它们,但它们其实随Unity一起提供。
This 也是一个很好的通用解释。
总之,大致会是这样的
- 按名称安装上述提到的包
com.unity.nuget.mono-cecil - 拥有两个自定义程序集
-
runtime
- 在这里放置一个自定义属性,例如
[AttributeUsage(AttributeTargets.Class)] public class ProfileAttribute : Attribute { }- 以及前面提到的profiler Marker范围,例如``` using System; using Unity.Profiling;
public struct ProfileScope : IDisposable { #if UNITY_EDITOR private ProfilerMarker.AutoScope _scope;
public ProfileScope(string name) { var marker = new ProfilerMarker(name); _scope = marker.Auto(); } public void Dispose() { _scope.Dispose(); }#else public ProfileScope(string name) { } public void Dispose() { } #endif } ``` + editor - 如前所述需要引用这两个隐藏的程序集 - IL Weaver的代码生成——下面这段是我在手机上拼凑出来的,大致如下
``` #if UNITY_EDITOR using System.Linq; using System.IO; using Mono.Cecil; using Mono.Cecil.Cil; using Unity.CompilationPipeline.Common.ILPostProcessing; using Unity.CompilationPipeline.Common.Diagnostics;
public class ProfileILPostProcessor : ILPostProcessor { public override bool WillProcess(ICompiledAssembly compiledAssembly) { // Here you can - and should - pre-filter which assemblies in general shall be processed // by early skipping certain assemblies here - like for instance anything starting with "Unity" // you can boost performance a lot and reduce compile times significantly if(compiledAssembly.Name.StartsWith("Unity")) return false; if(compiledAssembly.Name.EndsWith(".Editor")) return false; if(compiledAssembly.Name.EndsWith(".Tests")) return false;
return true; } public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) { var resolver = new DefaultAssemblyResolver(); foreach (var path in compiledAssembly.References) { resolver.AddSearchDirectory(Path.GetDirectoryName(path)); } var readerParams = new ReaderParameters { AssemblyResolver = resolver }; var assembly = AssemblyDefinition.ReadAssembly( new MemoryStream(compiledAssembly.InMemoryAssembly.PeData), readerParams); var module = assembly.MainModule; bool modified = false; foreach (var type in module.Types) { if (!HasProfileAttribute(type)) continue; foreach (var method in type.Methods) { if (ShouldSkip(method)) continue; if (AlreadyInjected(method)) continue; string sampleName = $"{type.Name}.{method.Name}"; if (InjectScope(method, module, sampleName)) modified = true; } } if (!modified) return null; var peStream = new MemoryStream(); assembly.Write(peStream); return new ILPostProcessResult( new InMemoryAssembly(peStream.ToArray(), compiledAssembly.InMemoryAssembly.PdbData), new DiagnosticMessage[0]); } private bool HasProfileAttribute(TypeDefinition type) { return type.CustomAttributes.Any(a => a.AttributeType.Name == nameof(ProfileAttribute)); } // Further filters out methods like constructors, abstract, external, and empty bodies that should not be wrapped private bool ShouldSkip(MethodDefinition method) { return method.IsConstructor || method.IsAbstract || method.IsPInvokeImpl || !method.HasBody; } // additionally filter out methods that already contain a ProfileScope private bool AlreadyInjected(MethodDefinition method) { return method.Body.Instructions.Any(i => i.OpCode == OpCodes.Newobj && i.Operand is MethodReference mr && mr.DeclaringType.Name == nameof(ProfileScope)); } // Here the actual magic happens and we wrap the original method body with the additional ProfileScope and try-finally blocks private bool InjectScope(MethodDefinition method, ModuleDefinition module, string sampleName) { var il = method.Body.GetILProcessor(); method.Body.SimplifyMacros(); var scopeType = module.ImportReference(typeof(ProfileScope)); var ctor = module.ImportReference( typeof(ProfileScope).GetConstructor(new[] { typeof(string) })); var dispose = module.ImportReference( typeof(System.IDisposable).GetMethod(nameof(System.IDisposable.Dispose))); var first = method.Body.Instructions.First(); var scopeVar = new VariableDefinition(scopeType); method.Body.Variables.Add(scopeVar); method.Body.InitLocals = true; // Create scope var loadName = il.Create(OpCodes.Ldstr, sampleName); var newObj = il.Create(OpCodes.Newobj, ctor); var store = il.Create(OpCodes.Stloc, scopeVar); il.InsertBefore(first, loadName); il.InsertAfter(loadName, newObj); il.InsertAfter(newObj, store); var tryStart = store.Next; // Create END label var endOfMethod = il.Create(OpCodes.Nop); // Replace all returns var retInstructions = method.Body.Instructions .Where(i => i.OpCode == OpCodes.Ret) .ToList(); foreach (var ret in retInstructions) { var leave = il.Create(OpCodes.Leave, endOfMethod); il.Replace(ret, leave); } // FINALLY block var finallyStart = il.Create(OpCodes.Ldloc, scopeVar); il.Append(finallyStart); il.Append(il.Create(OpCodes.Callvirt, dispose)); il.Append(il.Create(OpCodes.Endfinally)); // END label il.Append(endOfMethod); il.Append(il.Create(OpCodes.Ret)); var handler = new ExceptionHandler(ExceptionHandlerType.Finally) { TryStart = tryStart, TryEnd = finallyStart, HandlerStart = finallyStart, HandlerEnd = endOfMethod }; method.Body.ExceptionHandlers.Add(handler); method.Body.OptimizeMacros(); return true; }} #endif ```
类似这样的东西随后就可以简单地用作你类作用域上的属性
[Profile]
public class Example
{
public void ExampleA()
{
// original method body
}
public void ExampleB()
{
// original method body
}
}
而每个方法将简单地被额外包装一次
using (new ProfileScope("ClassName.MethodName"))
或者更准确地说(毕竟 using 只是一个高级语法糖),其实是一个等价的
var scope = new ProfileScope("ClassName.MethodName");
try
{
// original method body
}
finally
{
scope.Dispose();
}