问题描述
好吧,我想在 minecraft (https://github.com/lucko/spark) 中做我自己的基准测试系统,比如 spark:
我正在使用 Harmony lib (https://github.com/pardeike/Harmony),它允许我交互/修改方法,并允许我在每次调用时添加前缀/后缀,这将帮助我解决这个堆栈。
基本结构类似于(https://github.com/pardeike/Harmony/issues/355):
[HarmonyPatch]
class MyPatches
{
static IEnumerable<MethodBase> Targetmethods()
{
return Accesstools.GetTypesFromAssembly(Assembly.GetExecutingAssembly())
.SelectMany(type => type.getmethods())
.Where(method => method.ReturnType != typeof(void) && method.Name.StartsWith("Do"));
}
static void Prefix(out Stopwatch __state,MethodBase __originalMethod)
{
__state = Stopwatch.StartNew();
// ...
}
static void Postfix(Stopwatch __state,MethodBase __originalMethod)
{
__state.Stop();
// ....
}
}
这里的问题是 __originalMethod 不关心它是从 A 还是 B 调用的。
例如,我们修补了 string.Join
方法。我们从 A 或 B 调用,其中 A 或 B 是此方法的完整调用堆栈。
所以首先,我们需要为这个调用分配一个ID,我们需要创建一个基于树的结构(以后很难序列化),从这里(https://stackoverflow.com/a/36649069/3286975):
public class TreeModel : Tree<TreeModel>
{
public int ID { get; set; }
public TreeModel() { }
public TreeModel(TreeModel parent) : base(parent) { }
}
public class Tree<T> where T : Tree<T>
{
protected Tree() : this(null) { }
protected Tree(T parent)
{
Parent=parent;
Children=new List<T>();
if(parent!=null)
{
parent.Children.Add(this as T);
}
}
public T Parent { get; set; }
public List<T> Children { get; set; }
public bool IsRoot { get { return Parent==null; } }
public T Root { get { return IsRoot?this as T:Parent.Root; } }
public T RecursiveFind(Predicate<T> check)
{
if(check(this as T)) return this as T;
foreach(var item in Children)
{
var result=item.RecursiveFind(check);
if(result!=null)
{
return result;
}
}
return null;
}
}
现在,只要我们迭代从 Harmony 获得的所有方法和指令,我们就需要填充树。暂时忘掉 Harmony,我将只解释两个事实。
lib 允许您首先通过 IEnumerable<MethodBase> Targetmethods()
获取所有修补的方法,因此,您让 Assembly X 通过反射并过滤了所有允许修补的方法(其中一些破坏了 Unity,所以我决定跳过来自 UnityEngine.、UnityEditor. 和 System.* 命名空间的方法)。
我们还有来自给定 MethodBase 的 ReadMethodBody 方法 (https://harmony.pardeike.net/api/HarmonyLib.PatchProcessor.html#HarmonyLib_PatchProcessor_ReadMethodBody_System_Reflection_MethodBase_),它返回所有 IL 堆栈指令。
所以我们可以开始一遍又一遍地迭代以获取所有指令并填充整个树。这是我昨晚写的:
internal static class BenchmarkEnumerator
{
internal static Dictionary<MethodBase,int> Mappings { get; } = new Dictionary<MethodBase,int>();
internal static Dictionary<int,TreeModel> TreeIDs { get; } = new Dictionary<int,TreeModel>();
internal static Dictionary<MethodBase,BenchmarkTreeModel> TreeMappings { get; } = new Dictionary<MethodBase,BenchmarkTreeModel>();
private static HashSet<int> IDUsed { get; } = new HashSet<int>();
public static int GetID(this MethodBase method)
{
return GetID(method,out _);
}
public static int GetID(this MethodBase method,out bool contains)
{
// A > X = X1
// B > X = X2
if (!Mappings.ContainsKey(method))
{
var id = Mappings.Count;
Mappings.Add(method,Mappings.Count);
IDUsed.Add(id);
contains = false;
return id;
}
contains = true;
return Mappings[method];
}
public static int GetFreeID()
{
int id;
Random rnd = new Random();
do
{
id = rnd.Next();
} while (IDUsed.Contains(id));
IDUsed.Add(id);
return id;
}
public static BenchmarkCall GetCall(int id)
{
return TreeIDs[id]?.Call;
}
public static BenchmarkCall GetCall(this MethodBase method)
{
return TreeIDs[Mappings[method]]?.Call;
}
}
BenchmarkEnumerator
类允许我们区分 A 或 B,但它不关心完整的层次结构,只关心父 MethodBase 本身,所以我需要写一些复杂的东西来处理完整的调用堆栈,我说我有一个问题要理解。
然后我们有 Targetmethods:
private static IEnumerable<MethodBase> Targetmethods()
{
Model = new BenchmarkTreeModel();
var sw = Stopwatch.StartNew();
//int i = 0;
return Filter.GetTargetmethods(method =>
{
try
{
var instructions = PatchProcessor.ReadMethodBody(method);
var i = method.GetID(out var contains);
var tree = new TreeModel
{
ID = i
};
if (contains)
{
//var lastId = i;
i = GetFreeID();
tree.ID = i;
tree.FillMethodName($"{method.getmethodSignature()}_{i}"); // Todo: Check this
tree.Parent = null;
tree.Children = TreeMappings[method].Forest.First().Children; // ??
//DictionaryHelper.AddOrAppend(TreeMappings,method,tree);
TreeMappings[method].Forest.Add(tree);
TreeIDs.Add(i,tree);
Model.Forest.Add(tree);
// UNIT TESTING: All contained methods at this point will have a parent.
// string.Join is being added as a method by a instruction,so when we try to patch it,it will have already a reference on the dictionary
// Here,we check if the method was already added by a instruction CALL
// Logic: If the method is already contained by the mapping dictionary
// then,we will exit adding a new that will have the same childs but a new ID
return false;
}
TreeIDs.Add(i,tree);
tree.FillMethodName($"{method.getmethodSignature()}_{i}"); // Todo: Check this
foreach (var pair in instructions)
{
var opcode = pair.Key;
if (opcode != OpCodes.Call || opcode != OpCodes.Callvirt) continue;
var childMethod = (MethodBase)pair.Value;
var id = childMethod.GetID(out var _contains);
var subTree = new TreeModel(tree)
{
ID = id
};
if (_contains)
{
id = GetFreeID();
subTree.ID = id;
subTree.FillMethodName($"{childMethod.getmethodSignature()}_{id}"); // Todo: Check this
subTree.Parent = TreeIDs[i];
subTree.Children = TreeMappings[childMethod].Forest.First().Children;
TreeIDs.Add(id,subTree);
continue;
}
TreeIDs.Add(id,subTree);
subTree.FillMethodName($"{childMethod.getmethodSignature()}_{id}");
tree.Children.Add(subTree);
TreeMappings.Add(childMethod,new BenchmarkTreeModel());
TreeMappings[childMethod].Forest.Add(subTree);
}
TreeMappings.Add(method,new BenchmarkTreeModel());
TreeMappings[method].Forest.Add(tree);
Model.Forest.Add(tree);
return true;
//var treeModel = new TreeModel();
}
catch (Exception ex)
{
//Debug.LogException(new Exception(method.getmethodSignature(),ex));
return false;
}
},sw);
//return methods;
}
getmethodSignature 类似于:
public static string getmethodSignature(this MethodBase method)
{
if (method == null) return null;
return method.DeclaringType == null ? method.Name : $"{method.DeclaringType.FullName}.{method.Name}";
}
我想我会用 MethodBase.ToString 代替它(你怎么看?)
此外,我们有 BenchmarkCall 类,它允许我们注意调用完成了多少次以及花费了多少时间:
[Serializable]
public class BenchmarkCall
{
public string Method { get; set; }
public double SpentMilliseconds { get; set; }
public long SpentTicks { get; set; }
public double MinSpentMs { get; set; } = double.MaxValue;
public double MaxSpentMs { get; set; } = double.MinValue;
public long MinSpentTicks { get; set; } = long.MaxValue;
public long MaxSpentTicks { get; set; } = long.MinValue;
public double AvgMs => SpentMilliseconds / TimesCalled;
public double AvgTicks => SpentTicks / (double)TimesCalled;
public BenchmarkCall()
{
}
public BenchmarkCall(MethodBase method)
{
Method = method.getmethodSignature();
}
public override string ToString()
{
if (TimesCalled > 0)
return "BenchmarkCall{\n" +
$"Ticks[SpentTicks={SpentTicks},MinTicks={MinSpentTicks},MaxTicks={MaxSpentTicks},AvgTicks={AvgTicks:F2}]\n" +
$"Ms[SpentMs={SpentMilliseconds:F2},MinMs={MinSpentMs:F2},Maxms={MaxSpentMs:F2},AvgMs={AvgMs:F2}]\n" +
"}";
return "BenchmarkCall{}";
}
}
}
所以我认为我的下一个动作是区分从 A 或 B(Xa 或 Xb)调用的 X 方法来处理完整的层次结构(我不知道该怎么做)而不是父方法调用它,也许我写的代码与它有关,但我不确定(昨晚我太累了,所以我没有编写代码来照顾这些事实),建立一个方法列表具有不同ID的签名,然后填充树,ID 1是Xa,ID 2是Xb(我在填充树时也有问题)。
此外,我还需要使用 transpiler 来更改所有代码指令,因此如果一个方法具有:
void method() {
X1();
X2();
}
我们需要添加 2 个方法(如前缀/后缀)来测量每个指令调用:
void method() {
Start(1);
X1();
End(1);
Start(2);
X2();
End(2);
}
这将是一项艰巨的任务,但我希望有人能指导我解决这个问题。
解决方法
暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!
如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。
小编邮箱:dio#foxmail.com (将#修改为@)