自动将UnityEngine.Profiling的 Profiler采样绑定到某个类的每个方法

后端开发 2026-07-10

在使用 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.BeginSampleProfiler.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.Common
  • Mono.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();
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章