From b76b8252f3f263d49dcce8ab9270ff453ccfce5a Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 23 May 2026 07:26:25 -0400 Subject: [PATCH 1/2] Adjust JIT stage timing heuristic to bail out earlier for long-running benchmarks. --- src/BenchmarkDotNet/Engines/EngineJitStage.cs | 58 +++++++--------- .../Engine/EnumerateStagesTests.cs | 68 +++++++++++++++++++ 2 files changed, 91 insertions(+), 35 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/EngineJitStage.cs b/src/BenchmarkDotNet/Engines/EngineJitStage.cs index c514a8b39e..3289f6ca1a 100644 --- a/src/BenchmarkDotNet/Engines/EngineJitStage.cs +++ b/src/BenchmarkDotNet/Engines/EngineJitStage.cs @@ -11,9 +11,6 @@ namespace BenchmarkDotNet.Engines; // the following stages (Pilot and Warmup) will likely take it the rest of the way. Long-running benchmarks may never fully reach tier1. internal sealed class EngineJitStage : EngineStage { - // It is not worth spending a long time in jit stage for macro-benchmarks. - private static readonly TimeInterval MaxTieringTime = TimeInterval.FromSeconds(10); - // Jit call counting delay is only for when the app starts up. We don't need to wait for every benchmark if multiple benchmarks are ran in-process. private static TimeSpan s_tieredDelay = JitInfo.TieredDelay; @@ -88,49 +85,40 @@ private IEnumerator EnumerateIterations() // Don't make the next jit stage wait if it's ran in the same process. s_tieredDelay = TimeSpan.Zero; - // Attempt to promote methods to tier1, but don't spend too much time in jit stage. - StartedClock startedClock = parameters.TargetJob.ResolveValue(InfrastructureMode.ClockCharacteristic, parameters.Resolver)!.Start(); + // If the first iteration suggests a long-running benchmark (a single invocation already + // takes ~2/3 of IterationTime or more), run one confirmation iteration and bail out if + // it agrees. Same cutoff value that pilot stage uses. + // We do not bail out immediately if the first iteration is long-running because it could + // be due to cctors or other lazy initialization that won't be hit in steady-state. #2004 + TimeInterval iterationTime = parameters.TargetJob.ResolveValue(RunMode.IterationTimeCharacteristic, parameters.Resolver); + long remainingCalls = JitInfo.TieredCallCountThreshold; + if (iterationTime.Nanoseconds / (lastMeasurement.Nanoseconds / (double)userInvokeCount) < 1.5) + { + ++iterationIndex; + yield return GetWorkloadIterationData(userInvokeCount); + if (iterationTime.Nanoseconds / (lastMeasurement.Nanoseconds / (double)userInvokeCount) < 1.5) + { + didStopEarly = true; + yield break; + } + remainingCalls -= userInvokeCount; + } - int remainingTiers = JitInfo.MaxTierPromotions; - long lastInvokeCount = userInvokeCount; - while (remainingTiers > 0) + // Promote methods to tier1. + for (int remainingTiers = JitInfo.MaxTierPromotions; remainingTiers > 0; --remainingTiers) { - --remainingTiers; - long remainingCalls = JitInfo.TieredCallCountThreshold; while (remainingCalls > 0) { - long invokeCount; - if (hasUserInvocationCount) - { - invokeCount = userInvokeCount; - } - else - { - // If we can run a batch of calls within the time limit (based on the last measurement), do that instead of multiple single-invocation iterations. - // For long-running benchmarks where even a small batch wouldn't fit, fall back to one invocation per iteration. - var remainingTimeLimit = MaxTieringTime.ToNanoseconds() - startedClock.GetElapsed().GetNanoseconds(); - var lastMeasurementSingleInvocationTime = lastMeasurement.Nanoseconds / lastInvokeCount; - long allowedCallsWithinTimeLimit = (long)Math.Floor(remainingTimeLimit / lastMeasurementSingleInvocationTime); - invokeCount = allowedCallsWithinTimeLimit > 0 - ? Math.Min(remainingCalls, allowedCallsWithinTimeLimit) - : 1; - } - lastInvokeCount = invokeCount; - + // Run the whole tier's call budget in a single iteration unless the user pinned InvocationCount. + long invokeCount = hasUserInvocationCount ? userInvokeCount : remainingCalls; remainingCalls -= invokeCount; ++iterationIndex; // The generated __Overhead method is aggressively optimized, so we don't need to run it again. yield return GetWorkloadIterationData(invokeCount); - - if ((remainingTiers > 0 || remainingCalls > 0) - && startedClock.GetElapsed().GetTimeValue() >= MaxTieringTime) - { - didStopEarly = true; - yield break; - } } Engine.SleepIfPositive(JitInfo.BackgroundCompilationDelay); + remainingCalls = JitInfo.TieredCallCountThreshold; } // Empirical evidence shows that the first call after the method is tiered up may take longer, diff --git a/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs b/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs index 5a7a470beb..a1687bf5be 100644 --- a/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs +++ b/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Engines; using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Portability; using BenchmarkDotNet.Reports; using JetBrains.Annotations; using Perfolizer.Horology; @@ -163,6 +164,73 @@ public void JobWithExplicitInvocationCount(long invocationCount) } } + [Fact] + public void LongRunningBenchmarksExitJitStageEarly() + { + // #3114: a benchmark whose single invocation exceeds IterationTime shouldn't drag + // the JIT stage through the full tier-promotion loop. The pre-loop iteration + // absorbs cctors/lazy-init; a confirmation iteration trips the long-running + // heuristic and bails. + var slowMeasurement = TimeInterval.FromSeconds(4); // ~8x default IterationTime of 500ms + var job = Job.Default.WithInvocationCount(1).WithUnrollFactor(1); + var engineParameters = CreateEngineParameters(job); + + int jitWorkloadCount = 0; + foreach (var stage in EngineStage.EnumerateStages(engineParameters)) + { + var stageMeasurements = stage.GetMeasurementList(); + while (stage.GetShouldRunIteration(stageMeasurements, out var iterationData)) + { + if (stage is EngineJitStage && iterationData.mode == IterationMode.Workload) + { + jitWorkloadCount++; + } + var measurement = new Measurement(0, iterationData.mode, iterationData.stage, iterationData.index, iterationData.invokeCount, slowMeasurement.Nanoseconds); + stageMeasurements.Add(measurement); + } + + if (stage is EngineJitStage) break; + } + + // Non-tiered runtimes yield exactly one iteration; tiered runtimes yield two + // (the pre-loop one, then a single confirmation before bailing). + Assert.Equal(JitInfo.IsTiered ? 2 : 1, jitWorkloadCount); + } + + [Fact] + public void SlowFirstIterationButFastSteadyStateDoesNotExitJitStageEarly() + { + // If only the first iteration looks long-running (e.g. expensive cctor / lazy init), + // the confirmation iteration disagrees and the tiering loop continues as before. + if (!JitInfo.IsTiered) return; // Tier-promotion loop is skipped entirely on non-tiered runtimes. + + var slowFirst = TimeInterval.FromSeconds(4).Nanoseconds; + var fastRest = TimeInterval.FromMicroseconds(1).Nanoseconds; + var engineParameters = CreateEngineParameters(Job.Default.WithInvocationCount(1).WithUnrollFactor(1)); + + int jitWorkloadCount = 0; + foreach (var stage in EngineStage.EnumerateStages(engineParameters)) + { + var stageMeasurements = stage.GetMeasurementList(); + while (stage.GetShouldRunIteration(stageMeasurements, out var iterationData)) + { + if (stage is EngineJitStage && iterationData.mode == IterationMode.Workload) + { + jitWorkloadCount++; + } + var ns = jitWorkloadCount == 1 && stage is EngineJitStage ? slowFirst : fastRest; + stageMeasurements.Add(new Measurement(0, iterationData.mode, iterationData.stage, iterationData.index, iterationData.invokeCount, ns)); + } + + if (stage is EngineJitStage) break; + } + + // Pre-loop iter + confirmation + full tiering loop (one yield per tier since the user + // pinned InvocationCount=1, matching JitInfo.MaxTierPromotions * TieredCallCountThreshold) + // + one stabilization iteration. Just assert it ran the full tiering loop rather than bailing. + Assert.True(jitWorkloadCount > 2, $"Expected the tiering loop to run after confirmation disagreed, got {jitWorkloadCount} jitting iterations."); + } + [Fact] public void MediumTimeConsumingBenchmarksStartPilotFrom2AndIncrementItWithEveryStep() { From 05ce8f5cd33e31ea993e2f4b0b12b520adb19b4d Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Wed, 3 Jun 2026 17:27:38 -0400 Subject: [PATCH 2/2] Add JitTieringMode. --- docs/articles/guides/console-args.md | 1 + .../Mutators/JitTieringAttribute.cs | 9 +++ .../ConsoleArguments/CommandLineOptions.cs | 3 + .../ConsoleArguments/ConfigParser.cs | 2 + src/BenchmarkDotNet/Engines/EngineJitStage.cs | 9 ++- src/BenchmarkDotNet/Engines/EngineResolver.cs | 1 + src/BenchmarkDotNet/Engines/JitTieringMode.cs | 25 +++++++ src/BenchmarkDotNet/Jobs/JobExtensions.cs | 3 + src/BenchmarkDotNet/Jobs/RunMode.cs | 15 ++++ .../Engine/EnumerateStagesTests.cs | 75 +++++++++++++++++++ 10 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/BenchmarkDotNet/Attributes/Mutators/JitTieringAttribute.cs create mode 100644 src/BenchmarkDotNet/Engines/JitTieringMode.cs diff --git a/docs/articles/guides/console-args.md b/docs/articles/guides/console-args.md index 93e4958bf6..fea44c692a 100644 --- a/docs/articles/guides/console-args.md +++ b/docs/articles/guides/console-args.md @@ -313,6 +313,7 @@ dotnet run -c Release -- --filter * --runtimes net6.0 net8.0 --statisticalTest 5 * `--maxWidth` Max parameter column width, the default is 20. * `--envVars` Colon separated environment variables (key:value) * `--memoryRandomization` Specifies whether Engine should allocate some random-sized memory between iterations. It makes [GlobalCleanup] and [GlobalSetup] methods to be executed after every iteration. +* `--jitTieringMode` (Default: Auto) Controls the behavior of the JIT stage when tiering is enabled. Auto/Force/Skip. * `--wasmEngine` (Default: v8) Specifies the executable (in PATH) or full path to a java script engine used to run the benchmarks, used by Wasm toolchain. * `--wasmArgs` (Default: --expose_wasm) Arguments for the javascript engine used by Wasm toolchain. * `--customRuntimePack` Path to a custom runtime pack. Only used for wasm/MonoAotLLVM currently. diff --git a/src/BenchmarkDotNet/Attributes/Mutators/JitTieringAttribute.cs b/src/BenchmarkDotNet/Attributes/Mutators/JitTieringAttribute.cs new file mode 100644 index 0000000000..a48d64ebaf --- /dev/null +++ b/src/BenchmarkDotNet/Attributes/Mutators/JitTieringAttribute.cs @@ -0,0 +1,9 @@ +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Jobs; + +namespace BenchmarkDotNet.Attributes; + +/// +public class JitTieringAttribute(JitTieringMode mode) : JobMutatorConfigBaseAttribute(Job.Default.WithJitTieringMode(mode)) +{ +} diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index 543e1306ac..9bc6b35cb4 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs @@ -198,6 +198,9 @@ public bool UseDisassemblyDiagnoser [Option("memoryRandomization", Required = false, HelpText = "Specifies whether Engine should allocate some random-sized memory between iterations. It makes [GlobalCleanup] and [GlobalSetup] methods to be executed after every iteration.")] public bool MemoryRandomization { get; set; } + [Option("jitTieringMode", Required = false, Default = JitTieringMode.Auto, HelpText = "Controls the behavior of the JIT stage when tiering is enabled. Auto/Force/Skip.")] + public JitTieringMode JitTieringMode { get; set; } + [Option("wasmEngine", Required = false, HelpText = "Specifies the executable (in PATH) or full path to a java script engine used to run the benchmarks, used by Wasm toolchain.", Default = "v8")] public string? WasmJavaScriptEngine { get; set; } = "v8"; diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index db52b3abc3..876af056d3 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -455,6 +455,8 @@ private static Job GetBaseJob(CommandLineOptions options, IConfig? globalConfig) baseJob = baseJob.RunOncePerIteration(); if (options.MemoryRandomization) baseJob = baseJob.WithMemoryRandomization(); + if (options.JitTieringMode != Engines.JitTieringMode.Auto) + baseJob = baseJob.WithJitTieringMode(options.JitTieringMode); if (options.NoForcedGCs) baseJob = baseJob.WithGcForce(false); if (options.EvaluateOverhead is bool evaluateOverhead) diff --git a/src/BenchmarkDotNet/Engines/EngineJitStage.cs b/src/BenchmarkDotNet/Engines/EngineJitStage.cs index 3289f6ca1a..1e61e8315e 100644 --- a/src/BenchmarkDotNet/Engines/EngineJitStage.cs +++ b/src/BenchmarkDotNet/Engines/EngineJitStage.cs @@ -74,8 +74,9 @@ private IEnumerator EnumerateIterations() } yield return GetWorkloadIterationData(userInvokeCount); - // If the jit is not tiered, we're done. - if (!JitInfo.IsTiered) + JitTieringMode jitTieringMode = parameters.TargetJob.ResolveValue(RunMode.JitTieringModeCharacteristic, parameters.Resolver); + // If the jit is not tiered, or the user wants to skip, we're done. + if (!JitInfo.IsTiered || jitTieringMode == JitTieringMode.Skip) { yield break; } @@ -90,9 +91,11 @@ private IEnumerator EnumerateIterations() // it agrees. Same cutoff value that pilot stage uses. // We do not bail out immediately if the first iteration is long-running because it could // be due to cctors or other lazy initialization that won't be hit in steady-state. #2004 + // JitTieringMode.Force opts out of this heuristic and always promotes through every tier. TimeInterval iterationTime = parameters.TargetJob.ResolveValue(RunMode.IterationTimeCharacteristic, parameters.Resolver); long remainingCalls = JitInfo.TieredCallCountThreshold; - if (iterationTime.Nanoseconds / (lastMeasurement.Nanoseconds / (double)userInvokeCount) < 1.5) + if (jitTieringMode == JitTieringMode.Auto + && iterationTime.Nanoseconds / (lastMeasurement.Nanoseconds / (double)userInvokeCount) < 1.5) { ++iterationIndex; yield return GetWorkloadIterationData(userInvokeCount); diff --git a/src/BenchmarkDotNet/Engines/EngineResolver.cs b/src/BenchmarkDotNet/Engines/EngineResolver.cs index a5f823592d..d21606067c 100644 --- a/src/BenchmarkDotNet/Engines/EngineResolver.cs +++ b/src/BenchmarkDotNet/Engines/EngineResolver.cs @@ -33,6 +33,7 @@ private EngineResolver() Register(AccuracyMode.MinInvokeCountCharacteristic, () => 4); Register(AccuracyMode.EvaluateOverheadCharacteristic, () => false); Register(RunMode.MemoryRandomizationCharacteristic, () => false); + Register(RunMode.JitTieringModeCharacteristic, () => JitTieringMode.Auto); Register(AccuracyMode.OutlierModeCharacteristic, job => { // if Memory Randomization was enabled and the benchmark is truly multimodal diff --git a/src/BenchmarkDotNet/Engines/JitTieringMode.cs b/src/BenchmarkDotNet/Engines/JitTieringMode.cs new file mode 100644 index 0000000000..c34ff731e1 --- /dev/null +++ b/src/BenchmarkDotNet/Engines/JitTieringMode.cs @@ -0,0 +1,25 @@ +namespace BenchmarkDotNet.Engines; + +/// +/// Controls the behavior of the JIT stage when tiering is enabled. +/// +public enum JitTieringMode +{ + /// + /// Default. + /// If the benchmark is long-running, tiering is skipped and the benchmark method is left at the tier following the initial invoke (usually tier0). + /// Otherwise, the JIT stage attempts to force the benchmark method and its callees to be promoted to their final tier by calling it repeatedly before measurements begin. + /// + Auto, + + /// + /// Forces the JIT stage to attempt to force the benchmark method and its callees to be promoted to their final tier by calling it repeatedly before measurements begin. + /// Useful when you want the most stable tier1 measurements even for longer-running benchmarks. + /// + Force, + + /// + /// Forces the JIT stage to skip tier promotion entirely, leaving the benchmark method at the tier following the initial invoke (usually tier0). + /// + Skip +} diff --git a/src/BenchmarkDotNet/Jobs/JobExtensions.cs b/src/BenchmarkDotNet/Jobs/JobExtensions.cs index 2842c8d694..9ed798e3c2 100644 --- a/src/BenchmarkDotNet/Jobs/JobExtensions.cs +++ b/src/BenchmarkDotNet/Jobs/JobExtensions.cs @@ -199,6 +199,9 @@ public static Job WithHeapAffinitizeMask(this Job job, int heapAffinitizeMask) = /// public static Job WithMemoryRandomization(this Job job, bool enable = true) => job.WithCore(j => j.Run.MemoryRandomization = enable); + /// + public static Job WithJitTieringMode(this Job job, JitTieringMode mode) => job.WithCore(j => j.Run.JitTieringMode = mode); + // Infrastructure public static Job WithToolchain(this Job job, IToolchain toolchain) => job.WithCore(j => j.Infrastructure.Toolchain = toolchain); diff --git a/src/BenchmarkDotNet/Jobs/RunMode.cs b/src/BenchmarkDotNet/Jobs/RunMode.cs index f15c625907..e290cc9507 100644 --- a/src/BenchmarkDotNet/Jobs/RunMode.cs +++ b/src/BenchmarkDotNet/Jobs/RunMode.cs @@ -25,6 +25,7 @@ public sealed class RunMode : JobMode public static readonly Characteristic MinWarmupIterationCountCharacteristic = CreateCharacteristic(nameof(MinWarmupIterationCount)); public static readonly Characteristic MaxWarmupIterationCountCharacteristic = CreateCharacteristic(nameof(MaxWarmupIterationCount)); public static readonly Characteristic MemoryRandomizationCharacteristic = CreateCharacteristic(nameof(MemoryRandomization)); + public static readonly Characteristic JitTieringModeCharacteristic = CreateCharacteristic(nameof(JitTieringMode)); public static readonly RunMode Dry = new RunMode(nameof(Dry)) { @@ -190,6 +191,20 @@ public bool MemoryRandomization set => MemoryRandomizationCharacteristic[this] = value; } + /// + /// Controls the behavior of the JIT stage when tiering is enabled. + /// + /// (default): Promote through every tier for short-running benchmarks only. + /// : Always promote through every tier, regardless of invocation time. + /// : Skip tier promotion entirely. + /// + /// + public JitTieringMode JitTieringMode + { + get => JitTieringModeCharacteristic[this]; + set => JitTieringModeCharacteristic[this] = value; + } + internal BdnExecution ToPerfonar() => new() { LaunchCount = HasValue(LaunchCountCharacteristic) ? LaunchCount : null, diff --git a/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs b/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs index a1687bf5be..569889ba10 100644 --- a/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs +++ b/tests/BenchmarkDotNet.Tests/Engine/EnumerateStagesTests.cs @@ -2,6 +2,7 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Portability; using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Tests.XUnit; using JetBrains.Annotations; using Perfolizer.Horology; @@ -231,6 +232,80 @@ public void SlowFirstIterationButFastSteadyStateDoesNotExitJitStageEarly() Assert.True(jitWorkloadCount > 2, $"Expected the tiering loop to run after confirmation disagreed, got {jitWorkloadCount} jitting iterations."); } + [FactEnvSpecific("Requires tiered JIT", EnvRequirement.DotNetCoreOnly)] + public void ForceJitTieringModeRunsFullTieringLoopEvenForLongRunningBenchmarks() + { + // Tier-promotion loop is skipped entirely on non-tiered runtimes, so there is nothing to force. + if (!JitInfo.IsTiered) return; + + // A benchmark whose single invocation far exceeds IterationTime would normally trip the + // long-running heuristic and bail (see LongRunningBenchmarksExitJitStageEarly). + // JitTieringMode.Force opts out of that heuristic and always promotes through every tier. + var slowMeasurement = TimeInterval.FromSeconds(4); // ~8x default IterationTime of 500ms + var job = Job.Default.WithInvocationCount(1).WithUnrollFactor(1).WithJitTieringMode(JitTieringMode.Force); + var engineParameters = CreateEngineParameters(job); + + int jitWorkloadCount = 0; + bool didStopEarly = false; + foreach (var stage in EngineStage.EnumerateStages(engineParameters)) + { + var stageMeasurements = stage.GetMeasurementList(); + while (stage.GetShouldRunIteration(stageMeasurements, out var iterationData)) + { + if (stage is EngineJitStage && iterationData.mode == IterationMode.Workload) + { + jitWorkloadCount++; + } + stageMeasurements.Add(new Measurement(0, iterationData.mode, iterationData.stage, iterationData.index, iterationData.invokeCount, slowMeasurement.Nanoseconds)); + } + + if (stage is EngineJitStage jitStage) + { + didStopEarly = jitStage.didStopEarly; + break; + } + } + + Assert.False(didStopEarly, "Force mode should never bail out of the JIT stage early."); + Assert.True(jitWorkloadCount > 2, $"Expected the full tiering loop to run under Force mode, got {jitWorkloadCount} jitting iterations."); + } + + [Fact] + public void SkipJitTieringModeSkipsTierPromotion() + { + // JitTieringMode.Skip runs only the initial workload iteration and leaves tier promotion to the + // following Pilot/Warmup stages (as on non-tiered runtimes). Unlike the long-running bail-out, + // it does not flag the benchmark as long-running, so the Pilot stage still runs normally. + var fastMeasurement = TimeInterval.FromMicroseconds(1); // would normally run the full tiering loop + var job = Job.Default.WithInvocationCount(1).WithUnrollFactor(1).WithJitTieringMode(JitTieringMode.Skip); + var engineParameters = CreateEngineParameters(job); + + int jitWorkloadCount = 0; + bool didStopEarly = false; + foreach (var stage in EngineStage.EnumerateStages(engineParameters)) + { + var stageMeasurements = stage.GetMeasurementList(); + while (stage.GetShouldRunIteration(stageMeasurements, out var iterationData)) + { + if (stage is EngineJitStage && iterationData.mode == IterationMode.Workload) + { + jitWorkloadCount++; + } + stageMeasurements.Add(new Measurement(0, iterationData.mode, iterationData.stage, iterationData.index, iterationData.invokeCount, fastMeasurement.Nanoseconds)); + } + + if (stage is EngineJitStage jitStage) + { + didStopEarly = jitStage.didStopEarly; + break; + } + } + + // Only the single pre-loop workload iteration runs; the tier-promotion loop is skipped. + Assert.Equal(1, jitWorkloadCount); + Assert.False(didStopEarly, "Skip mode should not flag the benchmark as long-running."); + } + [Fact] public void MediumTimeConsumingBenchmarksStartPilotFrom2AndIncrementItWithEveryStep() {