Skip to content

Commit a5b0e9a

Browse files
authored
Add glob filters support to disassembler to allow disassembling specific methods (#2072)
* make it possible to specify filters for the disassembler via --disasmFilter and attribute * implement support for ClrMD v2 * implement support for ClrMD v1 * add test
1 parent b79282e commit a5b0e9a

File tree

11 files changed

+136
-24
lines changed

11 files changed

+136
-24
lines changed

src/BenchmarkDotNet.Disassembler.x64/ClrMdV1Disassembler.cs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Text.RegularExpressions;
78

89
namespace BenchmarkDotNet.Disassemblers
910
{
@@ -26,13 +27,20 @@ internal static DisassemblyResult AttachAndDisassemble(Settings settings)
2627

2728
var state = new State(runtime);
2829

29-
var typeWithBenchmark = state.Runtime.Heap.GetTypeByName(settings.TypeName);
30+
if (settings.Filters.Length > 0)
31+
{
32+
FilterAndEnqueue(state, settings);
33+
}
34+
else
35+
{
36+
var typeWithBenchmark = state.Runtime.Heap.GetTypeByName(settings.TypeName);
3037

31-
state.Todo.Enqueue(
32-
new MethodInfo(
33-
// the Disassembler Entry Method is always parameterless, so check by name is enough
34-
typeWithBenchmark.Methods.Single(method => method.IsPublic && method.Name == settings.MethodName),
35-
0));
38+
state.Todo.Enqueue(
39+
new MethodInfo(
40+
// the Disassembler Entry Method is always parameterless, so check by name is enough
41+
typeWithBenchmark.Methods.Single(method => method.IsPublic && method.Name == settings.MethodName),
42+
0));
43+
}
3644

3745
var disassembledMethods = Disassemble(settings, state);
3846

@@ -59,6 +67,28 @@ private static void ConfigureSymbols(DataTarget dataTarget)
5967
control?.Execute(DEBUG_OUTCTL.NOT_LOGGED, ".reload", DEBUG_EXECUTE.NOT_LOGGED);
6068
}
6169

70+
private static void FilterAndEnqueue(State state, Settings settings)
71+
{
72+
Regex[] filters = settings.Filters
73+
.Select(pattern => new Regex(WildcardToRegex(pattern), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToArray();
74+
75+
foreach (ClrModule module in state.Runtime.Modules)
76+
foreach (ClrType type in module.EnumerateTypes())
77+
foreach (ClrMethod method in type.Methods.Where(method => CanBeDisassembled(method) && method.GetFullSignature() != null))
78+
foreach (Regex filter in filters)
79+
{
80+
if (filter.IsMatch(method.GetFullSignature()))
81+
{
82+
state.Todo.Enqueue(new MethodInfo(method,
83+
depth: settings.MaxDepth)); // don't allow for recursive disassembling
84+
break;
85+
}
86+
}
87+
}
88+
89+
// copied from GlobFilter type (this type must not reference BDN)
90+
private static string WildcardToRegex(string pattern) => $"^{Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".")}$";
91+
6292
private static DisassembledMethod[] Disassemble(Settings settings, State state)
6393
{
6494
var result = new List<DisassembledMethod>();
@@ -77,11 +107,14 @@ private static DisassembledMethod[] Disassemble(Settings settings, State state)
77107
return result.ToArray();
78108
}
79109

110+
private static bool CanBeDisassembled(ClrMethod method)
111+
=> !((method.ILOffsetMap is null || method.ILOffsetMap.Length == 0) && (method.HotColdInfo is null || method.HotColdInfo.HotStart == 0 || method.HotColdInfo.HotSize == 0));
112+
80113
private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings)
81114
{
82115
var method = methodInfo.Method;
83116

84-
if ((method.ILOffsetMap is null || method.ILOffsetMap.Length == 0) && (method.HotColdInfo is null || method.HotColdInfo.HotStart == 0 || method.HotColdInfo.HotSize == 0))
117+
if (!CanBeDisassembled(method))
85118
{
86119
if (method.IsPInvoke)
87120
return CreateEmpty(method, "PInvoke method");

src/BenchmarkDotNet.Disassembler.x64/DataContracts.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,23 @@ public static class DisassemblerConstants
101101

102102
internal class Settings
103103
{
104-
internal Settings(int processId, string typeName, string methodName, bool printSource, int maxDepth, string resultsPath)
104+
internal Settings(int processId, string typeName, string methodName, bool printSource, int maxDepth, string resultsPath, string[] filters)
105105
{
106106
ProcessId = processId;
107107
TypeName = typeName;
108108
MethodName = methodName;
109109
PrintSource = printSource;
110110
MaxDepth = methodName == DisassemblerConstants.DisassemblerEntryMethodName && maxDepth != int.MaxValue ? maxDepth + 1 : maxDepth;
111111
ResultsPath = resultsPath;
112+
Filters = filters;
112113
}
113114

114115
internal int ProcessId { get; }
115116
internal string TypeName { get; }
116117
internal string MethodName { get; }
117118
internal bool PrintSource { get; }
118119
internal int MaxDepth { get; }
120+
internal string[] Filters;
119121
internal string ResultsPath { get; }
120122

121123
internal static Settings FromArgs(string[] args)
@@ -125,7 +127,8 @@ internal static Settings FromArgs(string[] args)
125127
methodName: args[2],
126128
printSource: bool.Parse(args[3]),
127129
maxDepth: int.Parse(args[4]),
128-
resultsPath: args[5]
130+
resultsPath: args[5],
131+
filters: args.Skip(6).ToArray()
129132
);
130133
}
131134

src/BenchmarkDotNet/Attributes/DisassemblyDiagnoserAttribute.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,22 @@ public class DisassemblyDiagnoserAttribute : Attribute, IConfigSource
1414
/// <param name="exportHtml">Exports to HTML with clickable links. False by default.</param>
1515
/// <param name="exportCombinedDisassemblyReport">Exports all benchmarks to a single HTML report. Makes it easy to compare different runtimes or methods (each becomes a column in HTML table).</param>
1616
/// <param name="exportDiff">Exports a diff of the assembly code to the Github markdown format. False by default.</param>
17+
/// <param name="filters">Glob patterns applied to full method signatures by the the disassembler.</param>
1718
public DisassemblyDiagnoserAttribute(
1819
int maxDepth = 1,
1920
bool printSource = false,
2021
bool printInstructionAddresses = false,
2122
bool exportGithubMarkdown = true,
2223
bool exportHtml = false,
2324
bool exportCombinedDisassemblyReport = false,
24-
bool exportDiff = false)
25+
bool exportDiff = false,
26+
params string[] filters)
2527
{
2628
Config = ManualConfig.CreateEmpty().AddDiagnoser(
2729
new DisassemblyDiagnoser(
2830
new DisassemblyDiagnoserConfig(
2931
maxDepth: maxDepth,
32+
filters: filters,
3033
printSource: printSource,
3134
printInstructionAddresses: printInstructionAddresses,
3235
exportGithubMarkdown: exportGithubMarkdown,
@@ -35,6 +38,14 @@ public DisassemblyDiagnoserAttribute(
3538
exportDiff: exportDiff)));
3639
}
3740

41+
// CLS-Compliant Code requires a constructor without an array in the argument list
42+
protected DisassemblyDiagnoserAttribute()
43+
{
44+
Config = ManualConfig.CreateEmpty().AddDiagnoser(
45+
new DisassemblyDiagnoser(
46+
new DisassemblyDiagnoserConfig()));
47+
}
48+
3849
public IConfig Config { get; }
3950
}
4051
}

src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ public class CommandLineOptions
148148
[Option("disasmDepth", Required = false, Default = 1, HelpText = "Sets the recursive depth for the disassembler.")]
149149
public int DisassemblerRecursiveDepth { get; set; }
150150

151+
[Option("disasmFilter", Required = false, HelpText = "Glob patterns applied to full method signatures by the the disassembler.")]
152+
public IEnumerable<string> DisassemblerFilters { get; set; }
153+
151154
[Option("disasmDiff", Required = false, Default = false, HelpText = "Generates diff reports for the disassembler.")]
152155
public bool DisassemblerDiff { get; set; }
153156

src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,10 @@ private static IConfig CreateConfig(CommandLineOptions options, IConfig globalCo
214214
if (options.UseThreadingDiagnoser)
215215
config.AddDiagnoser(ThreadingDiagnoser.Default);
216216
if (options.UseDisassemblyDiagnoser)
217-
config.AddDiagnoser(new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig(maxDepth: options.DisassemblerRecursiveDepth, exportDiff: options.DisassemblerDiff)));
217+
config.AddDiagnoser(new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig(
218+
maxDepth: options.DisassemblerRecursiveDepth,
219+
filters: options.DisassemblerFilters.ToArray(),
220+
exportDiff: options.DisassemblerDiff)));
218221
if (!string.IsNullOrEmpty(options.Profiler))
219222
config.AddDiagnoser(DiagnosersLoader.GetImplementation<IProfiler>(profiler => profiler.ShortName.EqualsWithIgnoreCase(options.Profiler)));
220223

src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
using Iced.Intel;
1+
using BenchmarkDotNet.Filters;
2+
using Iced.Intel;
23
using Microsoft.Diagnostics.Runtime;
34
using System;
45
using System.Collections.Generic;
56
using System.Linq;
7+
using System.Text.RegularExpressions;
68

79
namespace BenchmarkDotNet.Disassemblers
810
{
@@ -21,13 +23,20 @@ internal static DisassemblyResult AttachAndDisassemble(Settings settings)
2123

2224
var state = new State(runtime);
2325

24-
var typeWithBenchmark = state.Runtime.EnumerateModules().Select(module => module.GetTypeByName(settings.TypeName)).First(type => type != null);
26+
if (settings.Filters.Length > 0)
27+
{
28+
FilterAndEnqueue(state, settings);
29+
}
30+
else
31+
{
32+
ClrType typeWithBenchmark = state.Runtime.EnumerateModules().Select(module => module.GetTypeByName(settings.TypeName)).First(type => type != null);
2533

26-
state.Todo.Enqueue(
27-
new MethodInfo(
28-
// the Disassembler Entry Method is always parameterless, so check by name is enough
29-
typeWithBenchmark.Methods.Single(method => method.IsPublic && method.Name == settings.MethodName),
30-
0));
34+
state.Todo.Enqueue(
35+
new MethodInfo(
36+
// the Disassembler Entry Method is always parameterless, so check by name is enough
37+
typeWithBenchmark.Methods.Single(method => method.IsPublic && method.Name == settings.MethodName),
38+
0));
39+
}
3140

3241
var disassembledMethods = Disassemble(settings, state);
3342

@@ -51,6 +60,24 @@ private static void ConfigureSymbols(DataTarget dataTarget)
5160
dataTarget.SetSymbolPath("http://msdl.microsoft.com/download/symbols");
5261
}
5362

63+
private static void FilterAndEnqueue(State state, Settings settings)
64+
{
65+
Regex[] filters = GlobFilter.ToRegex(settings.Filters);
66+
67+
foreach (ClrModule module in state.Runtime.EnumerateModules())
68+
foreach (ClrType type in module.EnumerateTypeDefToMethodTableMap().Select(map => state.Runtime.GetTypeByMethodTable(map.MethodTable)).Where(type => type is not null))
69+
foreach (ClrMethod method in type.Methods.Where(method => CanBeDisassembled(method) && method.Signature != null))
70+
foreach (Regex filter in filters)
71+
{
72+
if (filter.IsMatch(method.Signature))
73+
{
74+
state.Todo.Enqueue(new MethodInfo(method,
75+
depth: settings.MaxDepth)); // don't allow for recursive disassembling
76+
break;
77+
}
78+
}
79+
}
80+
5481
private static DisassembledMethod[] Disassemble(Settings settings, State state)
5582
{
5683
var result = new List<DisassembledMethod>();
@@ -69,11 +96,14 @@ private static DisassembledMethod[] Disassemble(Settings settings, State state)
6996
return result.ToArray();
7097
}
7198

99+
private static bool CanBeDisassembled(ClrMethod method)
100+
=> !(method.ILOffsetMap.Length == 0 && (method.HotColdInfo.HotStart == 0 || method.HotColdInfo.HotSize == 0));
101+
72102
private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings)
73103
{
74104
var method = methodInfo.Method;
75105

76-
if (method.ILOffsetMap.Length == 0 && (method.HotColdInfo.HotStart == 0 || method.HotColdInfo.HotSize == 0))
106+
if (!CanBeDisassembled(method))
77107
{
78108
if (method.IsPInvoke)
79109
return CreateEmpty(method, "PInvoke method");

src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoserConfig.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
using BenchmarkDotNet.Disassemblers.Exporters;
22
using Iced.Intel;
33
using JetBrains.Annotations;
4+
using System;
45
using System.Collections.Generic;
56

67
namespace BenchmarkDotNet.Diagnosers
78
{
89
public class DisassemblyDiagnoserConfig
910
{
1011
/// <param name="maxDepth">Includes called methods to given level. 1 by default, indexed from 1. To print just the benchmark set it to 0.</param>
12+
/// <param name="filters">Glob patterns applied to full method signatures by the the disassembler.</param>
1113
/// <param name="formatter">Assembly code formatter. If not provided, MasmFormatter with the recommended settings will be used.</param>
1214
/// <param name="printSource">C#|F#|VB source code will be printed. False by default.</param>
1315
/// <param name="printInstructionAddresses">Print instruction addresses. False by default</param>
@@ -18,6 +20,7 @@ public class DisassemblyDiagnoserConfig
1820
[PublicAPI]
1921
public DisassemblyDiagnoserConfig(
2022
int maxDepth = 1,
23+
string[] filters = null,
2124
Formatter formatter = null,
2225
bool printSource = false,
2326
bool printInstructionAddresses = false,
@@ -27,6 +30,7 @@ public DisassemblyDiagnoserConfig(
2730
bool exportDiff = false)
2831
{
2932
MaxDepth = maxDepth;
33+
Filters = filters ?? Array.Empty<string>();
3034
Formatter = formatter ?? CreateDefaultFormatter();
3135
PrintSource = printSource;
3236
PrintInstructionAddresses = printInstructionAddresses;
@@ -39,6 +43,7 @@ public DisassemblyDiagnoserConfig(
3943
public bool PrintSource { get; }
4044
public bool PrintInstructionAddresses { get; }
4145
public int MaxDepth { get; }
46+
public string[] Filters { get; }
4247
public bool ExportGithubMarkdown { get; }
4348
public bool ExportHtml { get; }
4449
public bool ExportCombinedDisassemblyReport { get; }

src/BenchmarkDotNet/Disassemblers/LinuxDisassembler.cs renamed to src/BenchmarkDotNet/Disassemblers/SameArchitectureDisassembler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ private Settings BuildDisassemblerSettings(DiagnoserActionParameters parameters)
1818
methodName: DisassemblerConstants.DisassemblerEntryMethodName,
1919
printSource: config.PrintSource,
2020
maxDepth: config.MaxDepth,
21+
filters: config.Filters,
2122
resultsPath: default
2223
);
2324
}

src/BenchmarkDotNet/Disassemblers/WindowsDisassembler.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,13 @@ private string BuildArguments(DiagnoserActionParameters parameters, string resul
141141
.Append(DisassemblerConstants.DisassemblerEntryMethodName).Append(' ')
142142
.Append(config.PrintSource).Append(' ')
143143
.Append(config.MaxDepth).Append(' ')
144-
.Append($"\"{resultsPath}\"")
144+
.Append(Escape(resultsPath))
145+
.Append(' ')
146+
.Append(string.Join(" ", config.Filters.Select(Escape)))
145147
.ToString();
146148

149+
private static string Escape(string value) => $"\"{value}\"";
150+
147151
// code copied from https://stackoverflow.com/a/33206186/5852046
148152
private static class NativeMethods
149153
{

src/BenchmarkDotNet/Filters/GlobFilter.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,21 @@ namespace BenchmarkDotNet.Filters
99
/// </summary>
1010
public class GlobFilter : IFilter
1111
{
12-
private readonly (string userValue, Regex regex)[] patterns;
12+
private readonly Regex[] patterns;
1313

14-
public GlobFilter(string[] patterns)
15-
=> this.patterns = patterns.Select(pattern => (pattern, new Regex(WildcardToRegex(pattern), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))).ToArray();
14+
public GlobFilter(string[] patterns) => this.patterns = ToRegex(patterns);
1615

1716
public bool Predicate(BenchmarkCase benchmarkCase)
1817
{
1918
var benchmark = benchmarkCase.Descriptor.WorkloadMethod;
2019
string fullBenchmarkName = benchmarkCase.Descriptor.GetFilterName();
2120

22-
return patterns.Any(pattern => pattern.regex.IsMatch(fullBenchmarkName));
21+
return patterns.Any(pattern => pattern.IsMatch(fullBenchmarkName));
2322
}
2423

24+
internal static Regex[] ToRegex(string[] patterns)
25+
=> patterns.Select(pattern => new Regex(WildcardToRegex(pattern), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).ToArray();
26+
2527
// https://stackoverflow.com/a/6907849/5852046 not perfect but should work for all we need
2628
private static string WildcardToRegex(string pattern) => $"^{Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".")}$";
2729
}

0 commit comments

Comments
 (0)