This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Benchmark

Detailed breakdown of each component involved in defining a benchmark

In Hyperfoil, defining a benchmark involves structuring scenarios, phases, variables and other components to simulate realistic user behavior and workload patterns. This section provides a detailed breakdown of each component involved in defining a benchmark.

1 - Agents

Entities responsible for executing benchmark and collecting statistics

This section can be omitted in standalone mode.

Agents section forms either a list or map with arbitrary agent names and either an inline or properties-style definition:

agents:
  someAgent: "inline definition"
  otherAgent:
    foo: bar

The definition is passed to an instance of i.h.api.deployment.Deployer which will interpret the definition. Deployer implementation is registred using the java.util.ServiceLoader and selected through the io.hyperfoil.deployer system property. The default implementation is ssh.

Common properties

PropertyDefaultDescription
threadsfrom benchmarkNumber of threads used by the agent (overrides threads in benchmark root).
extrasCustom options passed to the JVM (system properties, JVM options…)

SSH deployer

The user account running Hyperfoil Controller must have a public-key authorization set up on agents’ hosts using key $HOME/.ssh/id_rsa. It also has to be able to copy files into the dir directory using SCP - all the required JARs will be copied there and you will find the logs there as well.

ssh deployer accepts either the [user@]host[:port] inline syntax or these properties:

PropertyDefaultDescription
userCurrent username
hostThis property is mandatory.
port22
sshKeyid_rsaOptionally define a different named key in the $HOME/.ssh directory
dirDirectory set by system property io.hyperfoil.rootdir or /tmp/hyperfoilWorking directory for the agent. This directory can be shared by multiple agents running on the same physical machine.
cpu(all cpus)If set the CPUs where the agent can run is limited using taskset -c <cpu>. Example: 0-2,6

See an example of ssh deployment configuration:

agents:
  agent1: testserver1:22
  agent2: testuser@testserver2
  agent3:
    host: testserver3
    port: 22
    dir: /some/other/path

Kubernetes/Openshift deployer

To activate the kubernetes deployer you should set -Dio.hyperfoil.deployer=k8s; the recommended installation does that automatically.

The agents are configured the same way as with SSH deployment, only the properties differ. Full reference is provided below.

Example:

agents:
  my-agent:
    node: my-worker-node
PropertyDefaultDescription
nodeConfigures the labels for the nodeSelector. If the value does not contain equals sign (=) or comma (,) this sets the desired value of label kubernetes.io/hostname. You can also set multiple custom labels separated by commas, e.g. foo=bar,kubernetes.io/os=linux.
stoptrueBy default the controller stops all agents immediatelly after the run terminates. In case of errors this is not too convenient as you might want to perform further analysis. To prevent automatic agent shutdown set this to false.
logName of config map (e.g. my-config-map) or config map and its entry (e.g. my-config-map/log4j2.xml) that contains the Log4j2 configuration file. Default entry from the config map is log4j2.xml. Hyperfoil will mount this configmap as a volume to this agent.
imagequay.io/hyperfoil/hyperfoil:controller-versionDifferent version of Hyperfoil in the agents
imagePullPolicyAlwaysImage pull policy for agents
fetchLogstrueAutomatically watch agents’ logs and store them in the run directory.

2 - HTTP

This section defines servers that agents contact during benchmarks, allowing configurations for multiple targets with specific connection settings

All servers that Hyperfoil should contact must be declared in this section. Before the benchmark starts Hyperfoil agents will open connections to the target servers; if this connection fails the benchmark is terminated immediatelly.

You can either declare single target server (the default one) within this section or more of them:

http:
  host: http://example.com
  ...
http:
- host: http://example.com
  sharedConnections: 100
- host: http://example.com:8080
  sharedConnections: 50

HTTP configuration has these properties:

PropertyDefaultDescription
protocolEither http or https
hostHostname of the server. For convenience you can use the http[s]://host[:port] inline syntax as shown above
port80 or 443Default is based on the protocol
sharedConnections1Number of connections to open. It is recommended to set this property to a non-default value.
connectionStrategySHARED_POOLConnection pooling model (see details below)
addressesSupply list of IPs or IP:port targets that will be used for the connections instead of resolving the host in DNS and using port as set - host and port will be used only for Host headers and SNI. If this list contains more addresses the connections will be split evenly.
requestTimeout30 secondsDefault request timeout, this can be overridden in each httpRequest.
allowHttp1xtrueAllow HTTP 1.1 for connections (e.g. during ALPN).
allowHttp2xtrueAllow HTTP 2.0 for connections (e.g. during ALPN). If both 1.1 and 2.0 are allowed and https is not used (which would trigger ALPN) Hyperfoil will use HTTP 1.1. If only 2.0 is allowed Hyperfoil will start with HTTP 1.1 and perform protocol upgrade to 2.0.
directHttp2falseStart with H2C HTTP 2.0 without protocol upgrade. Makes sense only for plain text (http) connections. Currently not implemented.
maxHttp2Streams100Maximum number of requests concurrently enqueued on single HTTP 2.0 connection.
pipeliningLimit1Maximum number of requests pipelined on single HTTP 1.1 connection.
rawBytesHandlerstrueEnable or disable using handlers that process HTTP response raw bytes.
keyManagerTLS key manager for setting up client certificates.
trustManagerTLS trust manager for setting up server certificates.
useHttpCachetrueMake use of HTTP cache on client-side. If multiple authorities are involved, disable the HTTP cache for all of them to achieve the desired outcomes. The default is true except for wrk/wrk2 wrappers where it is set to false.

Shared connections

This number is split between all agents and executor threads evenly; if there are too many agents/executors each will get at least 1 connection.

When a scalar value is used for this property the connection pool has fixed size; Hyperfoil opens all connections when the benchmark starts and should a connection be closed throughout the benchmark, another connection is reopened instead. You can change this behaviour by composing the property of these sub-properties:

PropertyDescription
coreNumber of connections that will be opened when the benchmark starts. Number of connections in the pool should never drop below this value (another connection will be opened instead).
maxMaximum number of connections in the pool.
bufferHyperfoil will try to keep at least active + buffer connections in the pool where active is the number of currently used connection (those with at least 1 in-flight request)
keepAliveTimeWhen a connection is not used for more than this value (in milliseconds) it will be closed. Non-positive value means that the connection is never closed because of being idle.

Example:

http:
  host: http://example.com
  sharedConnections:
    core: 10
    buffer: 10
    max: 10
    keepAliveTime: 30000

Connection strategies

This property describes the connection pooling model, you can choose from the options below:

StrategyDescription
SHARED_POOLConnections are created in a pool and then borrowed by the session. When the request is complete the connection is returned to the shared pool.
SESSION_POOLSConnections are created in a shared pool. When the request is completed it is not returned to the shared pool but to a session-local pool. Subsequent requests by this session first try to acquire the connection from this local pool. When the session completes all connections from the session-local pool are returned to the shared pool.
OPEN_ON_REQUESTConnections are created before request or borrowed from a session-local pool. When the request is completed the connection is returned to this pool. When the session completes all connections from the session-local pool are closed.
ALWAYS_NEWAlways create the connection before the request and close it when it is complete. No pooling of connections.

KeyManager configuration

All files are loaded when the benchmark is constructed, e.g. on the machine running CLI. You don’t need to upload any files to controller or agent machines.

PropertyDefaultDescription
storeTypeJKSImplementation of the store.
storeFilePath to a file with the store.
passwordPassword for accessing the store file.
aliasKeystore alias.
certFilePath to a file with the client certificate.
keyFilePath to a file with client’s private key.

TrustManager configuration

All files are loaded when the benchmark is constructed, e.g. on the machine running CLI. You don’t need to upload any files to controller or agent machines.

PropertyDefaultDescription
storeTypeJKSImplementation of the store.
storeFilePath to a file with the store.
passwordPassword for accessing the store file.
certFilePath to a file with the server certificate.

3 - Phases

Defines a unit of workload simulation within a benchmark, representing a specific load pattern or behavior

You might want to simulate several types of workloads at once: e.g. in an eshop users would come browsing or buying products, and operators would restock the virtual warehouse. Also, driving constant load may not be the best way to run the benchmark: often you want to slowly ramp the load up to let the system adjust (scale up, perform JIT, fill pools) and push the full load only after that. When trying to find system limits, you do the same repetitevely - ramp up the load, measure latencies and if the system meets SLAs (latencies below limits) continue ramping up the load until it breaks.

In Hyperfoil, this all is expressed through phases. Phases can run independently of each other; these simulate certain load execute by a group of users. Within one phase all users execute the same scenario (e.g. logging into the system, buying some goods and then logging off).

A phase can be in one of these states:

  • not running (scheduled): As the name clearly says, the phase is not yet getting executed.
  • running: The agent started running the phase, i.e., performing the configured load.
  • finished: Users won’t start new scenarios but we’ll let already-started users complete the scenario.
  • terminated: All users are done, all stats are collected and no further requests will be made.
  • cancelled: Same as terminated but this phase hasn’t been run at all.

There are different types of phases based on the mode of starting new users:

TypeDescription
constantRateThe benchmark will start certain number of users according to a schedule regardless of previously started users completing the scenario. This is the open-model.
increasingRateSimilar to constantRate but ramps up the number of started users throughout the execution of the phase.
decreasingRateThe same as increasingRate but requires initialUsersPerSec > targetUsersPerSec.
atOnceAll users are be started when the phase starts running and once the scenario is completed the users won’t retry the scenario.
alwaysThere is fixed number of users and once the scenario is completed the users will start executing the scenario from beginning. This is called a closed-model and is similar to the way many benchmarks with fixed number of threads work.
noopThis phase cannot have any scenario (or forks). It might be useful to add periods of inactivity into the benchmark.

See the example of phases configuration:

...
phases:
# Over one minute ramp the number of users started each second from 1 to 100
- rampUp:
    increasingRate:
      initialUsersPerSec: 1
      targetUsersPerSec: 100
      # We expect at most 200 users being active at one moment - see below
      maxSessions: 200
      duration: 1m
      scenario: ...
# After rampUp is finished, run for 5 minutes and start 100 new users each second
- steadyState:
    constantRate:
      usersPerSec: 100
      maxSessions: 200
      startAfter: rampUp
      duration: 5m
      # If some users get stuck, forcefully terminate them after 6 minutes from the phase start
      maxDuration: 6m
      scenario: ...
# 2 minutes after the benchmark has started spawn 5 users constantly doing something for 2 minutes
- outOfBand:
    always:
      users: 5
      startTime: 2m
      duration: 2m
      scenario: ...
- final:
    atOnce:
      users: 1
      # Do something at the end: make sure that both rampUp and steadyState are terminated
      startAfterStrict:
      - rampUp
      - steadyState
      scenario: ...

These properties are common for all types of phases:

PropertyDescription
startTimeTime relative to benchmark start when this phase should be scheduled. In other words, it’s the earliest moment when it could be scheduled, other conditions (below) may delay that even further.
startAfterPhases that must be finished before this phase can start. You can use either single phase name, list of phases or a reference to certain iteration.
startAfterStrictPhases that must be terminated before this phase can start. Use the same syntax as for startAfter.
durationIntended duration for the phase (must be defined but for the atOnce type). After this time elapses no new sessions will be started; there might be some running sessions still executing operations, though.
maxDurationAfter this time elapses all sessions are forcefully terminated.
isWarmupThis marker property is propagated to results JSON and allows the reporter to hide some phases by default.
maxUnfinishedSessionsMaximum number of session that are allowed to be open when the phase finishes. When there are more open sessions all the other sessions are cancelled and the benchmark is terminated. Unlimited by default.
maxIterationsMaximum number of iterations this phase will be scaled to. More about that below.
scenarioThe scenario this phase should execute.
forksSee forks section below.

Below are properties specific for different phase types:

  • atOnce:
    • users: Number of users started at the start of the phase.
  • always:
    • users: Number of users started at the start of the phase. When a user finishes it is immediatelly restarted (any pause must be part of the scenario).
  • constantRate:
    • usersPerSec: Number of users started each second.
    • variance: Randomize delays between starting users following the exponential distribution. That way the starting users behave as the Poisson point process. If this is set to false users will be started with uniform delays. Default is true.
    • maxSessions: Number of preallocated sessions. This number is split between all agents/executors evenly.
  • increasingRate / decreasingRate:
    • initialUsersPerSec: Rate of started users at the beginning of the phase.
    • targetUsersPerSec: Rate of started users at the end of the phase.
    • variance: Same as in `constantRate
    • maxSessions: Same as in constantRate.

Hyperfoil initializes all phases before the benchmark starts, pre-allocating memory for sessions. In the open-model phases it’s not possible to know how many users will be active at the same moment (if the server experiences a 3-second hiccup and we have 100 new users per second this should be at least 300 as all the users will be blocked). However we need to provide the estimate for memory pre-allocation. If the estimate gets exceeded the benchmark won’t fail nor block new users from starting, but new sessions will be allocated which might negatively impact results accuracy.

Properties users, usersPerSec, initialUsersPerSec and targetUsersPerSec can be either a scalar number or scale with iterations using the base and increment components. You’ll see an example below.

Forks

As mentioned earlier, users in each phase execute the same scenario. Often it’s convenient to define the ramp-up and steady-state phases just once: the builders allow to declare such ‘sub-phases’ called forks. For all purposes but the benchmark configuration these become regular phases of the same type, duration and dependencies (startAfter, startAfterStrict) as the ‘parent’ phase but slice the users according to their weight:

...
phases:
- steadyState:
    constantRate:
      usersPerSec: 30
      duration: 5m
      forks:
        sellShares:
          # This phase will start 10 users per second
          weight: 1
          scenario: ...
        buyShares:
          # This phase will start 20 users per second
          weight: 2
          scenario: ...

These phases will be later identified as steadyState/sellShares and steadyState/buyShares. Other phases can still reference steadyState (without suffix) as the dependency: there will be a no-op phase steadyState that starts (becomes running) as soon as both the forks finish, finish immediately and terminate once both the forks terminate.

Iterations

In some types of tests it’s useful to repeat given phase with increasing load - we call this concept iterations. In the example below you can see that *usersPerSec are not scalar values; in first iteration the actual value is set to the base value but in each subsequent iteration the value is increased by increment.

...
phases:
- rampUp:
    increasingRate:
      # Create phases rampUp/000, rampUp/001 and rampUp/002
      maxIterations: 3
      # rampUp/000 will go from 1 to 100 users, rampUp will go from 101 to 200 users...
      initialUsersPerSec:
        base: 1
        increment: 100
      targetUsersPerSec:
        base: 100
        increment: 100
      # rampUp/001 will start after steadyState/000 finishes
      startAfter:
        phase: steadyState
        iteration: previous
      duration: 1m
      scenario: ...
- steadyState:
    constantRate:
      maxIterations: 3
      usersPerSec:
        base: 100
        increment: 100
      # steadyState/000 will start after rampUp/000 finishes
      startAfter:
        phase: rampUp
        iteration: same
      duration: 5m

Similar to forks, there will be a no-op phase rampUp that will start after all rampUp/xxx phases finish and terminate after these terminate. Also there’s an implicit dependency between consecutive iterations: subsequent iteration won’t start until previous iteration terminates.

The startAfter property in this example uses a relative reference to iteration in another phase. Each reference has these properties:

| Property | Description | | ——— | | | phase | Name of the referenced phase. | | iteration | Relative number of the iteration; either none (default) which references the top-level phase, same meaning the iteration with same number, or previous with number one lower. | | fork | Reference to particular fork in the phase/iteration. |

Iterations can be combined with forks as well - the result name would be e.g. steadyState/000/sellShares.

Note that the maxSessions parameter is not scaling in iterations: all iterations execute the same scenario, the execution does not overlap and therefore it is possible to share the pool of sessions. Therefore you should provide an estimate for the iteration spawning the highest load.

Staircase

Hyperfoil tries to make opinionated decisions, simplifying common types of benchmark setups. That’s why it offers a simplified syntax for the scenario where you:

  • ramp the load to a certain level
  • execute steady state for a while
  • ramp it up further
  • execute another steady state
  • repeat previous two steps over and over

This is called a staircase as the load increases in a shape of tilted stairs. Phases such benchmark should consist of are automatically created and linked together, using the same scenario/forks.

staircase as a top-level element in the benchmark is mutually exclusive to scenario and phases elements.

Here is a minimalistic example of such configuration:

name: simple benchmark
http:
  host: http://localhost:8080
staircase:
  initialRampUpDuration: 10s
  steadyStateDuration: 10s
  rampUpDuration: 5s
  initialUsersPerSec: 5
  incrementUsersPerSec: 1
  maxIterations: 3
  scenario:
  - test:
    - httpRequest:
        GET: /foo

This element uses these properties:

PropertyDescription
initialRampUpDurationDuration of the very first phase. Default is no initial ramp-up.
initialUsersPerSecRate of users starting at the end of the initial ramp-up.
steadyStateDurationDuration of each steady-state phase.
rampUpDurationDuration of each but first ramp-up. Default are no ramp-ups.
incrementUsersPerSecIncrease in the rate of started users in each iteration.
maxIterationsMaximum number of steady-state iterations.
scenarioThe scenario to be executed.
forksThe forks with different scenarios.

4 - Scenario

Defines the behavior and sequence of actions that virtual users (VU) perform during a benchmark execution

Scenario

Scenario is a set of sequences. The sequence is a block of sequentially executed steps. Contrary to steps in a sequence the sequences within a scenario do not need to be executed sequentially.

The scenario defines one or more initialSequences that are enabled from the beginning and other sequences that must be enabled by any of the previously executed sequences. To be more precise it is not the sequence that is enabled but a sequence instance as we can run a sequence multiple times in parallel (on different data). The initialSequences enable one instance of each of the referenced sequence.

The session keeps a currently executed step for each of the enabled sequence instances. The step can be blocked (e.g. waiting for a response to come). The session is looping through current steps in each of the enabled sequence instances and if the step is not blocked, it is executed. There’s no guaranteed order in which non-blocked steps from multiple enabled sequence instances will be executed.

Here is an example of scenario:

scenario:
  initialSequences:
  - login:
    - httpRequest:
        POST: /login
    # Enable instance of sequence 'wait5seconds'
    - next: wait5seconds
  sequences:
  - wait5seconds:
    - thinkTime:
        duration: 5s
    - next: logout
  - logout:
    - httpRequest:
        POST: /logout

While this generic approach is useful for complex scenarios with branching logic, simple sequential scenarios can use orderedSequences short-cut enabling sequences in given order:

scenario:
  orderedSequences:
  - login:
    - httpRequest:
        POST: /login
  - wait5seconds:
    - thinkTime:
        duration: 5s
  - logout:
    - httpRequest:
        POST: /logout

This syntax makes the first sequence (login in this case) an initial sequence, adds the subsequent sequences and as the last step of each but the last sequence appends a next step scheduling a new instance of the following sequence.

To make configuration even more concise you can omit the orderedSequences level and start defining the list of sequences under scenario right away:

scenario:
- login:
  - httpRequest:
      POST: /login
- wait5seconds:
  - thinkTime:
      duration: 5s
- logout:
  - httpRequest:
      POST: /logout

An exhaustive list of steps can be found in the steps reference.

5 - Variables

Data placeholders within sessions that hold values throughout the execution of a benchmark scenario

All but the simplest scenarios will use session variables. Hyperfoil sports steps that generate values into these variables (randomInt, randomItem, …), processors that write data from other sources to variables (store, array) and many places that read variables and use the values to perform some operations (httpRequest.path ) or alter control flow.

Hyperfoil uses different types of variables (slots in the session) for integer variables and generic objects (commonly strings). When a numeric value is received as a string (e.g. when parsing response headers) and you want to use it in a step that expects exclusively integral values you have to convert it explicitly, e.g. using the stringToInt action. Steps that read values to form a string can usually consume both types of variables, without any need for conversion.

Besides user-defined variables there are some read-only pseudo-variables that can be used in the scenario as if these were regular variables:

VariableTypeDescription
hyperfoil.agent.idintegerZero-based index of the agent node
hyperfoil.agentsintegerNumber of agent nodes or 1 when running in in-VM mode (standalone or CLI)
hyperfoil.agent.thread.idintegerZero-based index of current executor thread within this agent.
hyperfoil.agent.thread sintegerNumber of executor threads running in this agent.
hyperfoil.global.thread.idintegerZero-based index of current executor thread across all agents (unique).
hyperfoil.global.threadsintegerTotal number of executor threads on all agents.
hyperfoil.phase.nameobjectFull name of the currently executed phase (possibly including fork and iteration number).
hyperfoil.phase.idintegerIndex of the currently executed phase.
hyperfoil.phase.iterationintegerIteration number of the currently executed phase.
hyperfoil.run.idobjectIdentifier of the current run, e.g. 0123.
hyperfoil.session.idintegerUnique index of this virtual user (session). Note that in benchmarks with multiple phases the indices might not be zero-based.

String interpolation

Components that accept string values usually allow you to use a pattern - parts of the string can be replaced in runtime with the value from a session variable. A simple example of pattern would be The quick brown ${wild-animal} jumps over the lazy ${domestic-animal} - variables wild-animal and domestic-animal would get replaced with their respective values.

When you really want to use ${wild-animal} in a value for such component you should escape it with one more dollar sign: This $${variable} won't be replaced will be rendered into This ${variable} won't be replaced.

There are a few transformations that you can perform with a variable value while interpolating the pattern:

  • ${urlencode:my-variable} will replace characters in the my-variable using URLEncoder.encode (using UTF-8 encoding).
  • ${{ '{%05d' }}:my-number} and other formatter strings ending with d, o, x or X will convert an integer variable using Formatter.
  • ${replace/<regexp>/<replacement>/<flags>:my-variable} perform Java regexp replacement on my-variable contents. Note that you can use any character after replace, not just / - this becomes the separator between regexp, replacement and flags. The only flags currently supported is g - replacing all occurences of that string (by default only first occurence is replaced).

Sequence-scoped access

When an array or collection is stored in a session variable you can access the individual elements by appending [.] to the variable name, e.g. my-variable[.]. You can see that we don’t use the actual index into the array: instead we use current sequence instance index. You can read more about running multiple sequences concurrently in the Architecture/Scenario Execution.

6 - Templates

Templates in Hyperfoil allow for efficient benchmark parametrization, enabling users to customize benchmarks based on specific execution environments or intended loads

It is often useful to keep a single benchmark in version control but change parts of it depending on the infrastructure where it is executed or intended load. Since version 0.18 Hyperfoil supports parametrization of the benchmark through templates.

Inspired by other (more complex) YAML templating systems we decided to use YAML tags to pre-process the YAML. Templating happens even before applying the YAML nodes onto BenchmarkBuilder, therefore it is not possible to do that programmatically or with the serialized form.

If you are working with CLI or WebCLI there is little difference to regular benchmarks: you upload and edit the benchmark as usual. However it is not possible to auto-detect files before the benchmark is constructed from the template (the reference to a file could be a template, too!), therefore you need to pass all files using option -f to the upload/edit command.

When the benchmark template is uploaded, upon running it (run mybenchmark) you either pass the parameters using option -P or you are interactively asked to provide those params. The parameters are stored in CLI context and on subsequent invocations of run you don’t need to set these. If you want to remove the parameters from the context use option -r/--reset-params. To see both default and current parameters you can use the inspect command.

Param

You should use !param to replace single scalar value:

1name: example
2http:
3  host: http://localhost:8080
4usersPerSec: !param NUM_USERS
5duration: !param DURATION 60s
6scenario: # ...

In this simple constant-rate benchmark you can customize the number of users starting each second as well as the duration. There’s no default for NUM_USERS; you will be asked to provide it when you run the benchmark. On the other hand DURATION has a default value of 60s - anything after the space after parameter name counts as the default value.

Parameters don’t have to be upper-case. The identifier is case-sensitive, though.

run scalar-value-example -PNUM_USERS=5 -PDURATION=60s

Concat

Sometimes you need to replace only part of a string: !concat will let you do that:

1name: example
2http:
3  host: !concat [ "http://", !param SERVER localhost, ":8080" ]
4usersPerSec: 10
5duration: 60s
6scenario: # ...

In this example we will customize the host with the concatenation of http://, parameter SERVER with default localhost and :8080. This example uses inline-form of list, though you can use the regular list (one item per line), too.

Foreach

Chances are you need to generate a list based on a param: you can do this using the !foreach:

 1name: example
 2http: !foreach
 3  items: http://example.com,http://hyperfoil.io
 4  separator: "," # comma is the default separator
 5  param: ITEM   # ITEM is the default parameter name
 6  do:
 7    host: !param ITEM
 8usersPerSec: 10
 9duration: 60s
10scenario: # ...

This splits the items using the separator regexp and produces a list of values or mappings while the param ITEM is set to one of the values from items list. The example above would result in:

name: example
http:
- host: http://example.com
- host: http://hyperfoil.io
usersPerSec: 10
duration: 60s
scenario: # ...

You can also set items to a YAML list; in that case the separator is not used:

myList: !foreach
  items: ["A", !param B, "C"]
  param: FOO
  do: !param FOO

The last example with -PB=bar would result in:

myList:
- A
- bar
- C

Renaming the param used for iteration can be useful in nested loops: without renaming the inner foreach would shadow the outer one.

Anchors and aliases

YAML has a built-in concept for removing repetitive sections: anchors and aliases. With the templating system you can use that universally throughout the file (in versions before 0.18 the support was limited to forks, scenarios and sequences):

foo: &hello-world
  hello: world
anotherFoo:
  sayHi: *hello-world
  myList:
  - *hello-world
  - bar

is interpretted as

foo:
  hello: world
anotherFoo:
  sayHi:
    hello: world
  myList:
  - hello: world
  - bar

7 - Hooks

Mechanisms that allow users to run specific scripts or commands automatically before and after executing a benchmark run

It might be useful to run certain scripts before and after the run, e.g. starting some infrastructure, preloading database, gathering CPU stats during the test and so on. That’s why Hyperfoil introduces pre- and post-hooks to the run.

Some scripts are not specific to the test being run - these should be deployed on controller as files in *root*/hooks/pre/ and *root*/hooks/post directories where root is controller’s root directory, /tmp/hyperfoil/ by default. Each of these directories should contain executable scripts or binaries that will be run in alphabetic order. We strongly suggest using the format 00-my-script.sh to set the order using first two digits.

Kubernetes/Openshift deployments use the same strategy; the only difference is that the pre and post directories are mapped as volumes from a ConfigMap resource.

Other scripts may be specific to the benchmark executed and therefore you can define them directly in the YAML files. You can either use inline command that will be executed using sh -c your-command --your-options or create a Java class implementing io.hyperfoil.core.hooks.RunHook and register it to be loaded as other Hyperfoil extensions.

name: my-benchmark
pre:
  01-inline: curl http://example.com
  02-custom:
    my-hook:
      foo: bar
post:
  99-some-final-hook: ...
...

The lists of hooks from controller directories and benchmark are merged; if there’s a conflict between two hooks from these two sources the final execution order is not defined (but both get executed).

In case of inline command execution the stderr output will stay on stderr, stdout will be caputered by Hyperfoil and stored in *rundir*/*XXXX*/hooks.json. As the post-hooks are executed after info.json and all.json get written the output cannot be included inside those files. This order of execution was chosen because it’s likely that you will upload these files to a database - yes, using a post-hook.

8 - Ergonomics

Configuration options that enhance usability and automation of benchmarking sessions

This section hosts only single property at this moment:

PropertyDefaultDescription
repeatCookiestrueAutomatically parse cookies from HTTP responses, store them in session and resend them with subsequent requests.
userAgentFromSessiontrueAdd user-agent header to each request, holding the agent name and session id.
autoRangeChecktrueMark 4xx and 5xx responses as invalid. You can also turn this off in each step.
stopOnInvalidtrueWhen the session receives an invalid response it does not execute any further steps, cancelling all requests and stopping immediately.
followRedirectNEVERDefault value for httpRequest.handler.followRedirect.