skip to content

Stepped Level Builders

One helper per workload model builds a whole staircase in a single chain, so a capacity ramp is one expression rather than a hand-written run of alternating ramps and holds.

on this pageshow

explore

questions

6

In Gatling, which builders declare a whole stepped load profile in one expression, and what does each call in that chain set?

level: juniorimportance: must knowfreq 48%

answer

  1. One builder per workload model
  2. A staircase in a single chain
  3. times then eachLevelLasting are mandatory
  4. separatedByRampsLasting and startingFrom optional
  5. The argument is the per-level increment

basics

~10 s

Gatling has one staircase builder per workload model: incrementUsersPerSec(rateIncrement) and incrementConcurrentUsers(usersIncrement). Both chain .times(levels).eachLevelLasting(duration), plus optional .separatedByRampsLasting(duration) and .startingFrom(x).

solid answer

~40 s

Gatling ships two stepped-level builders, one per workload model: `incrementUsersPerSec(rateIncrement)` for the open model and `incrementConcurrentUsers(usersIncrement)` for the closed one. The argument is the **difference between consecutive levels**, not the load itself. From there the chain is fixed: `.times(levels)` says how many levels, `.eachLevelLasting(duration)` says how long each one holds, and those two are mandatory. Two optional calls follow: `.separatedByRampsLasting(duration)` inserts a linear ramp between levels instead of a hard jump, and `.startingFrom(x)` sets the first level so the staircase does not begin at zero. The step names are the same in Java, Kotlin, Scala, JavaScript and TypeScript; only the registration call differs (`injectOpen`/`injectClosed` versus Scala's `inject`).

code

java · 9 lines
java
setUp(
  scn.injectOpen(
    incrementUsersPerSec(20)
      .times(8)
      .eachLevelLasting(Duration.ofMinutes(2))
      .separatedByRampsLasting(Duration.ofSeconds(30))
      .startingFrom(20)
  )
).protocols(httpProtocol);

go deeper

for a junior

Be ready to name both builders and write the chain in order: the increment, then times, then eachLevelLasting, then the two optional calls.

for a middle

Be ready to explain why the chain's order is fixed — each call returns a different type, and only the last one exposes startingFrom and separatedByRampsLasting.

for a senior

Be ready to say when a staircase belongs in a suite at all, and how you parameterise its increment, level count and level duration rather than hard-coding them.

for a principal

Be ready to own the tradeoff between one readable staircase expression and an explicit chain of steps when a profile needs unequal levels or unequal dwell times.

Gatling calls a stepped load profile a **staircase**, and it ships one builder per workload model so that a capacity profile is a single chained expression rather than a hand-written run of alternating ramps and holds. ## The two entry points | builder | model | what the argument means | type | |---|---|---|---| | `incrementUsersPerSec(rateIncrement)` | open | the difference in **arrivals per second** between consecutive levels | `Double` | | `incrementConcurrentUsers(usersIncrement)` | closed | the difference in **concurrent users** between consecutive levels | `Int` | The commonest misreading is treating that first number as the load itself. It is not. Gatling's own API documentation calls the parameter *"the difference of users per second rate between levels of the stairs"*. So `incrementUsersPerSec(20)` does not ask for twenty arrivals a second — it asks for levels **twenty apart**. The builder you pick fixes which model you are declaring, and every step inside one injection call must be of the **same** model kind, so an `incrementUsersPerSec` staircase cannot sit beside a `constantConcurrentUsers` hold in the same call. (What an open or a closed workload model *is*, and which one your system deserves, is performance-testing theory that this page does not settle.) ## The chain is a type ladder, not a bag of options Each call returns a different type, and that is what enforces the order at compile time: 1. `incrementUsersPerSec(20)` yields a `Stairs` whose **only** method is `times`. 2. `.times(8)` yields a stage whose **only** method is `eachLevelLasting`. 3. `.eachLevelLasting(d)` yields a `Composite` — and this is the first point at which you hold a usable injection step. 4. `.startingFrom(x)` and `.separatedByRampsLasting(d)` are declared on that `Composite`, return another `Composite`, and may be written in either order. Two consequences follow. You cannot skip `times` or `eachLevelLasting` — there is no overload that lets you — and you cannot call `.startingFrom(...)` before `.eachLevelLasting(...)`, because the earlier stages do not declare it. ## What the four calls set * **`times(levels)`** — the number of levels in the staircase. * **`eachLevelLasting(duration)`** — how long every level holds. This is the same for all levels; the builder has no per-level duration. * **`separatedByRampsLasting(duration)`** — *optional*. Inserts a linear ramp of that length between levels. Omit it and the profile jumps from one level straight to the next. * **`startingFrom(x)`** — *optional*. The level the staircase begins at. Omit it and it begins at zero, which is almost never what a capacity run wants. The duration arguments follow the SDK's usual spelling: a bare number means **seconds** everywhere, Java and Kotlin also take a `java.time.Duration`, Scala takes a `scala.concurrent.duration` literal such as `2.minutes`, and the JavaScript/TypeScript SDK takes an object literal such as `{ amount: 2, unit: "minutes" }`. ## A worked profile A capacity staircase of eight levels twenty arrivals per second apart, each level held for two minutes: ```java setUp( scn.injectOpen( incrementUsersPerSec(20) .times(8) .eachLevelLasting(Duration.ofMinutes(2)) .startingFrom(20) ) ).protocols(httpProtocol); ``` That declares levels of 20, 40, 60, 80, 100, 120, 140 and 160 arrivals per second, sixteen minutes of run time, and roughly 86,400 injected users — one expression in place of eight hand-written steps. With no ramps declared, the staircase expands to exactly one `constantUsersPerSec(rate).during(2 min)` block per level, so eight levels means eight blocks. (Adding `separatedByRampsLasting` to this `startingFrom(20)` profile would make it fifteen: eight levels and seven ramps.) ## What the staircase expands into The helper is shorthand. Gatling turns the finished chain into an ordinary list of injection blocks — but which list depends on which branch the expansion takes, and there are two of them. **With no ramps, or with ramps over a non-zero starting level**, it is one constant-rate block per level plus one ramp block per gap (`levels - 1` ramps, and none at all without `separatedByRampsLasting`), and the rate of the level at index `k` is simply: ```text levelRate = k * increment + startingRate // k = 0 .. levels-1 ``` **With ramps and a starting level of zero** — which is what you get when `startingFrom` is omitted — Gatling takes the other branch. It emits `levels` ramps rather than `levels - 1`, because there is a leading ramp out of zero that sits in no gap, and the rates are: ```text levelRate = (k + 1) * increment // k = 0 .. levels-1 ``` so that profile peaks at `levels x increment`, one full increment above what the first formula would predict. Carrying the first formula into the ramped-from-zero case is the commonest arithmetic slip on this builder. Two things follow from the expansion. Everything true of `constantUsersPerSec` and `rampUsersPerSec` is true of the blocks a staircase produces, because they *are* those blocks. And the total run length is derived rather than declared: `levels x levelDuration` with no ramps, plus the ramp time when ramps are present. Nothing in the chain states a total duration, so a reviewer computes it from the level count and the level duration. ## Where it sits in the simulation The finished `Composite` **is** an injection step, so it goes wherever any other step goes: alone inside `injectOpen(...)`, or chained beside other open steps such as a trailing `constantUsersPerSec(...).during(...)`. In Java, Kotlin, JavaScript and TypeScript you pass it to `scn.injectOpen(...)` or `scn.injectClosed(...)`; in Scala you pass it to the single `inject(...)`, which resolves open versus closed from the step type rather than from the method name. One limit worth knowing up front: there is no `.randomized()` on a staircase. That modifier exists on `constantUsersPerSec` and `rampUsersPerSec` only, so the levels of a staircase always inject at regular intervals.

  • Can a staircase step be chained with other injection steps in the same call?
    Yes. The finished chain is an ordinary injection step, so it can be followed by, say, `constantUsersPerSec(160).during(Duration.ofMinutes(10))` inside the same `injectOpen(...)`. The one constraint is that every step in the call must be of the same workload model kind — you cannot mix open and closed steps.
  • Is there a `.randomized()` modifier on a staircase, as there is on `constantUsersPerSec`?
    No. `randomized()` is declared only on the constant-rate and ramp-rate open steps. The staircase builder's final stage exposes just `startingFrom` and `separatedByRampsLasting`, so its levels always inject at regular intervals.

Read it like a flight of stairs specified by its rise and its count: the argument is the height of one step, times is how many steps, eachLevelLasting is how long you stand on each, and startingFrom is the landing you begin on.

saying these in an interview costs you the question

  • Reading the builder argument as the load rather than the increment
  • Thinking times or eachLevelLasting can be omitted
  • Calling startingFrom before eachLevelLasting in the chain
  • Mixing an open staircase with closed steps in one call
open as a page

In Gatling, what load does incrementUsersPerSec(20).times(8).eachLevelLasting(Duration.ofMinutes(2)) actually apply when startingFrom is left off?

level: middleimportance: must knowfreq 38%

basics

~20 s

Levels of 0, 20, 40, 60, 80, 100, 120 and 140 arrivals per second, two minutes each. Omitting startingFrom spends the first two minutes injecting nobody and tops out one increment short, at 140 rather than 160.

open as a page

Why does a Gatling staircase written as incrementUsersPerSec(20).times(8).during(Duration.ofMinutes(2)) fail to compile, and how is the duration spelled instead?

level: middleimportance: should knowfreq 30%

basics

~10 s

The stepped-level builders declare no during method. A staircase spells its durations with eachLevelLasting(d) for each level, and optionally separatedByRampsLasting(d) for the ramps between levels.

open as a page

In a Gatling stepped injection profile, what does separatedByRampsLasting add, and why does the presence of startingFrom change the resulting shape?

level: seniorimportance: should knowfreq 26%

basics

~20 s

It inserts a linear ramp between levels instead of a hard jump. With a non-zero startingFrom the shape is level-then-ramp, ending on a level with no trailing ramp; with it omitted or set to zero the shape is ramp-then-level, opening with a ramp out of zero.

open as a page

When would you stop expressing a Gatling capacity profile with the incrementUsersPerSec staircase and hand-chain the injection steps instead?

level: principalimportance: should knowfreq 24%

basics

~20 s

When the profile stops being uniform. The staircase gives one increment, one level duration and one ramp duration for the whole run, so unequal levels, an uneven progression or a longer dwell at the top all force explicit alternating steps.

open as a page

In Gatling, why does a Kotlin simulation have to write incrementUsersPerSec(20.0) while incrementConcurrentUsers(20) is fine as written?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

The open staircase builder takes a Double and the closed one takes an Int, and startingFrom follows the same split. Kotlin performs no implicit widening from Int to Double, so the open builder needs a decimal literal.

open as a page