Revit Benchmarking
A Revit benchmark runs inside Revit and supplies the evidence for one production decision about a Revit API hot path.
Nice3point.BenchmarkDotNet.Revit marshals the benchmark onto Revit's thread; running it needs a matching licensed Revit installation.
When to use
- Choosing between viable Revit API implementations on measured evidence.
- Measuring a Revit hot path on a representative model.
When not to use
- The measured operation does not call the Revit API.
Workflow
Step 1: Write the benchmark class
Derive from RevitApiBenchmark.
Open the model in OnGlobalSetup and close it in OnGlobalCleanup — the base binds [GlobalSetup]/[GlobalCleanup] and calls these overrides; never add those attributes directly.
Each [Benchmark] holds one compared operation and returns its result; a returned result keeps the JIT from eliminating it.
Declare the alternatives as sibling [Benchmark] methods in the same class.
A small application-level comparison needs no document:
public class XyzBenchmarks : RevitApiBenchmark
{
[Benchmark]
public XYZ Constructor()
{
return new XYZ(3, 4, 5);
}
[Benchmark]
public XYZ Factory()
{
return Application.Create.NewXYZ(3, 4, 5);
}
}
A benchmark that needs a seeded model opens it once in setup; keep the seeding out of the measured method:
public class CollectorBenchmarks : RevitApiBenchmark
{
private Document _document = null!;
protected sealed override void OnGlobalSetup()
{
_document = Application.NewProjectDocument(UnitSystem.Metric);
using var transaction = new Transaction(_document, "Seed model");
transaction.Start();
var level = Level.Create(_document, 0);
for (var i = 0; i < 1000; i++)
{
Wall.Create(_document, Line.CreateBound(new XYZ(i, 0, 0), new XYZ(i + 1, 0, 0)), level.Id, false);
}
transaction.Commit();
}
protected sealed override void OnGlobalCleanup()
{
_document.Close(false);
}
[Benchmark]
public IList<Element> ToElements()
{
return new FilteredElementCollector(_document)
.WhereElementIsNotElementType()
.ToElements();
}
[Benchmark]
public List<Element> ToList()
{
return new FilteredElementCollector(_document)
.WhereElementIsNotElementType()
.ToList();
}
}
Step 2: Configure the runner with the current build configuration
BenchmarkDotNet builds in Release by default.
The build fails for Revit's multi-version configurations.
Apply WithCurrentConfiguration() to the job; it then builds the active Release.RNN.
var configuration = ManualConfig.Create(DefaultConfig.Instance)
.AddJob(Job.Default.WithCurrentConfiguration())
.AddDiagnoser(MemoryDiagnoser.Default);
BenchmarkRunner.Run<CollectorBenchmarks>(configuration);
Step 3: Run and decide
dotnet run -c Release.RNN
RNN is the target Revit-year configuration, for example Release.R26; it must match the licensed Revit installed on the machine.
Iterate with a dry run first, then measure the final comparison on a quiet machine.
Read the Markdown report; compare time, allocation, and output correctness, and record why the chosen implementation applies to production.
Validation
- The benchmark derives from
RevitApiBenchmark. - The document is opened in
OnGlobalSetupand closed inOnGlobalCleanup, outside the measured method. - The runner job uses
WithCurrentConfiguration(). - The selected
Release.RNNconfiguration matches the installed Revit runtime. - The result drives a concrete implementation decision.
Common Pitfalls
| Pitfall | Correct approach |
|---|---|
Runner uses the default Release job |
Add Job.Default.WithCurrentConfiguration(). |
Seeding the model inside a [Benchmark] method |
Do it in OnGlobalSetup. |
[GlobalSetup]/[GlobalCleanup] added directly |
Override OnGlobalSetup/OnGlobalCleanup; the base binds them. |
Discovery features like [Params]/[ParamsAllValues] using a Revit type |
BenchmarkDotNet reads them during discovery, before Revit initializes, and throws; use primitives or non-Revit types. |
RevitApiBenchmark not found |
The Nice3point.BenchmarkDotNet.Revit package is not referenced. |