Jenkins integration example

Runperf allows to integrate with Jenkins via xunit results and also can deliver html results with great level of details especially for regression testing. Let me present one example which is not meant to be copy&pasted to your environment, but can serve as an inspiration for integration. An overview might look like this:

_images/diagram.png

Which might generate following html results (slightly outdated version that compares a different setting on host and guest): here

Let’s imagine we have example.org machine, we can create rp-example job to run regression testing, then rp-example-manual job to allow testing of changes or custom params. For each of these we might want to create $name-ident jobs to allow cherry-picking and analyzing of results in order to create models to easier evaluate the expected results.

A useful addition is the rp-analysis job to allow running custom compare queries without the need to download and run them from your machines and the rp-prune-artifacts to automatically remove big tarballs with full results and only keep the json results that are small and suffice for compare-perf usecases.

The latest addition is an upstream qemu bisect pipeline called rp-example-upstream-bisect.

The above pipelines are using Jenkins shared library runperf, which is attached below.

All of these can be easily defined via Jenkins Job Builder:

##############################################################################
# Default configuration
##############################################################################
- defaults:
    name: "global"
    mailto: ""
    wrappers:
        - ansicolor
        - timestamps
        - workspace-cleanup
    build-discarder:
        days-to-keep: 365
        artifact-num-to-keep: 60
    # Default runperf params
    param-distro: ''
    param-guest-distro: ''
    param-tests: "'fio:{{\"targets\": \"/fio\"}}' 'uperf:{{\"protocols\": \"tcp\"}}' 'uperf:{{\"protocols\": \"udp\", \"test-types\": \"rr\"}}'"
    param-profiles: "Localhost DefaultLibvirt TunedLibvirt"
    param-src-build: '1'
    param-cmp-tolerance: 5
    param-cmp-stddev-tolerance: 10
    param-cmp-model-job: ''
    param-cmp-model-build: ''
    param-host-bkr-links: ''
    param-no-reference-builds: 14
    param-fio-nbd-setup: false
    param-upstream-qemu-commit: ''
    param-github-publisher: ''
    param-metadata: ''
    param-host-script: ''
    param-worker-script: ''
    trigger-on: "H 17 * * *"
    disabled: false


##############################################################################
# Definition for the run-perf execution job
##############################################################################
- job-template:
    name: "{name}-run"
    triggers:
        - timed: "{trigger-on}"
    project-type: pipeline
    parameters:
        - string:
            name: DISTRO
            description: 'Distribution to be installed/is installed (Fedora-31), when empty latest el8 nightly build is obtained from bkr'
            default: "{param-distro}"
        - string:
            name: GUEST_DISTRO
            description: 'Distribution to be installed on guest, when empty "distro" is used'
            default: "{param-guest-distro}"
        - string:
            name: MACHINE
            description: 'Machine to be provisioned and tested'
            default: "{param-machine}"
        - string:
            name: ARCH
            description: 'Target machine architecture'
            default: "{param-arch}"
        - string:
            name: TESTS
            description: 'Space separated list of tests to be executed (WARNING: fio-nbd test requires the FIO_NBD_SETUP checkbox enabled!)'
            default: "{param-tests}"
        - string:
            name: PROFILES
            description: 'Space separated list of profiles to be applied'
            default: "{param-profiles}"
        - string:
            name: SRC_BUILD
            description: 'Base build to compare with'
            default: "{param-src-build}"
        - string:
            name: CMP_MODEL_JOB
            description: 'Job to copy linear "model.json" from'
            default: "{param-cmp-model-job}"
        - string:
            name: CMP_MODEL_BUILD
            description: 'Build to copy linear "model.json" from (-1 means lastSuccessful)'
            default: "{param-cmp-model-build}"
        - string:
            name: CMP_TOLERANCE
            description: Tolerance for mean values
            default: "{param-cmp-tolerance}"
        - string:
            name: CMP_STDDEV_TOLERANCE
            description: Tolerance for standard deviation values
            default: "{param-cmp-stddev-tolerance}"
        - string:
            name: HOST_KERNEL_ARGS
            description: Add custom kernel arguments on host
            default: ""
        - string:
            name: HOST_BKR_LINKS
            description: 'Space separated list of urls to be grepped for "http.*$arch\\.rpm" and "http.*noarch\\.rpm", filtered by HOST_BKR_LINKS_FILTER and then set to be installed on host/workers with "--allowerase". Intended usage is to use koji/beaker link like: https://koji.fedoraproject.org/koji/buildinfo?buildID=1534744'
            default: "{param-host-bkr-links}"
        - string:
            name: HOST_BKR_LINKS_FILTER
            description: 'Filter to be split and applied via "grep -v -e EXPR1 -e EXPR2 ..."'
            default: "debug"
        - string:
            name: GUEST_KERNEL_ARGS
            description: Add custom kernel argsuments on workers/guests
            default: ""
        - string:
            name: GUEST_BKR_LINKS
            description: 'Space separated list of urls to be grepped for "http.*$arch\\.rpm" and "http.*noarch\\.rpm", filtered by GUEST_BKR_LINKS_FILTER and then set to be installed on host/workers with "--allowerase". Intended usage is to use koji/beaker link like: https://koji.fedoraproject.org/koji/buildinfo?buildID=1534744'
            default: ""
        - string:
            name: GUEST_BKR_LINKS_FILTER
            description: 'Filter to be split and applied via "grep -v -e EXPR1 -e EXPR2 ..."'
            default: "debug"
        - bool:
            name: PBENCH_PUBLISH
            description: 'Push the pbench results to company pbench server'
            default: "{param-pbench-publish}"
        - string:
            name: GITHUB_PUBLISHER_PROJECT
            description: 'Github publisher project ID (when you want to publish your results)'
            default: "{param-github-publisher}"
        - bool:
            name: FIO_NBD_SETUP
            description: 'Compile and install fio with nbd ioengine enabled before test execution'
            default: "{param-fio-nbd-setup}"
        - string:
            name: UPSTREAM_QEMU_COMMIT
            description: 'Compile and install qemu using provided commit/tag from the upstream git. Use it by using $PROFILE:{{"qemu_bin": "/usr/local/bin/qemu-system-$ARCH"}} when specifying profiles.'
            default: "{param-upstream-qemu-commit}"
        - bool:
            name: FEDORA_LATEST_KERNEL
            description: 'Install the latest kernel from koji (Fedora rpm)'
            default: false
        - string:
            name: METADATA
            description: 'Additional run-perf --metadata arguments'
            default: "{param-metadata}"
        - text:
            name: HOST_SCRIPT
            description: 'Host script to be executed on all --servers'
            default: "{param-host-script}"
        - text:
            name: WORKER_SCRIPT
            description: 'Worker script to be executed on all runperf workers'
            default: "{param-worker-script}"
        - string:
            name: DESCRIPTION_PREFIX
            description: Description prefix (describe the difference from default)
            default: ""
        - string:
            name: NO_REFERENCE_BUILDS
            description: "Number of reference builds for comparison"
            default: "{param-no-reference-builds}"
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "runperf.groovy"
        lightweight-checkout: true


##############################################################################
# Definition for the upstream qemu bisect job
##############################################################################
- job-template:
    name: "{name}-bisect-qemu"
    project-type: pipeline
    parameters:
        - string:
            name: DISTRO
            description: 'Distribution to be installed/is installed (Fedora-31), when empty latest el8 nightly build is obtained from bkr'
            default: "{param-distro}"
        - string:
            name: GUEST_DISTRO
            description: 'Distribution to be installed on guest, when empty "distro" is used'
            default: "{param-guest-distro}"
        - string:
            name: MACHINE
            description: 'Machine to be provisioned and tested'
            default: "{param-machine}"
        - string:
            name: ARCH
            description: 'Target machine architecture'
            default: "{param-arch}"
        - string:
            name: TESTS
            description: 'Space separated list of tests to be executed (WARNING: fio-nbd test requires the FIO_NBD_SETUP checkbox enabled!)'
            default: "{param-tests}"
        - string:
            name: PROFILES
            description: 'Space separated list of profiles to be applied'
            default: "{param-profiles}"
        - text:
            name: HOST_SCRIPT
            description: 'Host script to be executed on all --servers'
            default: "{param-host-script}"
        - text:
            name: WORKER_SCRIPT
            description: 'Worker script to be executed on all runperf workers'
            default: "{param-worker-script}"
        - string:
            name: HOST_KERNEL_ARGS
            description: Add custom kernel arguments on host
            default: ""
        - string:
            name: HOST_BKR_LINKS
            description: 'Space separated list of urls to be grepped for "http.*$arch\\.rpm" and "http.*noarch\\.rpm", filtered by HOST_BKR_LINKS_FILTER and then set to be installed on host/workers with "--allowerase". Intended usage is to use koji/beaker link like: https://koji.fedoraproject.org/koji/buildinfo?buildID=1534744'
            default: ""
        - string:
            name: HOST_BKR_LINKS_FILTER
            description: 'Filter to be split and applied via "grep -v -e EXPR1 -e EXPR2 ..."'
            default: "debug"
        - string:
            name: GUEST_KERNEL_ARGS
            description: Add custom kernel argsuments on workers/guests
            default: ""
        - string:
            name: GUEST_BKR_LINKS
            description: 'Space separated list of urls to be grepped for "http.*$arch\\.rpm" and "http.*noarch\\.rpm", filtered by GUEST_BKR_LINKS_FILTER and then set to be installed on host/workers with "--allowerase". Intended usage is to use koji/beaker link like: https://koji.fedoraproject.org/koji/buildinfo?buildID=1534744'
            default: ""
        - string:
            name: GUEST_BKR_LINKS_FILTER
            description: 'Filter to be split and applied via "grep -v -e EXPR1 -e EXPR2 ..."'
            default: "debug"
        - bool:
            name: PBENCH_PUBLISH
            description: 'Push the pbench results to company pbench server'
            default: "{param-pbench-publish}"
        - bool:
            name: FIO_NBD_SETUP
            description: 'Compile and install fio with nbd ioengine enabled before test execution'
            default: "{param-fio-nbd-setup}"
        - bool:
            name: TWO_OUT_OF_THREE
            description: 'Use 2 out of 3 result evaluation (longer duration, better stability for jittery results)'
            default: true
        - string:
            name: UPSTREAM_QEMU_GOOD
            description: 'SHA of the last good (older) upstream qemu.'
            default: "{param-upstream-qemu-commit}"
        - string:
            name: UPSTREAM_QEMU_BAD
            description: 'SHA of the last bad (newer) upstream qemu.'
            default: "{param-upstream-qemu-commit}"
        - string:
            name: DESCRIPTION_PREFIX
            description: Description prefix (describe the difference from default)
            default: ""
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "upstream_bisect.groovy"
        lightweight-checkout: true


##############################################################################
# Definition for a multi-run-perf execution job
##############################################################################
- job-template:
    name: "{name}-multi"
    project-type: pipeline
    parameters:
        - string:
            name: JOB_NAME
            description: 'Name of the run-perf job to be used for triggering the tests.\nWarning: there might be concurrency issues in case one attempts to concurrently schedule jobs!'
            default: "{param-job}"
        - string:
            name: MACHINE
            description: 'Machine to be provisioned and tested'
            default: "{param-machine}"
        - string:
            name: ARCH
            description: 'Target machine architecture'
            default: "{param-arch}"
        - string:
            name: TESTS
            description: 'Space separated list of tests to be executed (WARNING: fio-nbd test requires the FIO_NBD_SETUP checkbox enabled!)'
            default: "{param-tests}"
        - string:
            name: PROFILES
            description: 'Space separated list of profiles to be applied'
            default: "{param-profiles}"
        - string:
            name: CMP_MODEL_JOB
            description: 'Job to copy linear "model.json" from'
            default: "{param-cmp-model-job}"
        - string:
            name: CMP_MODEL_BUILD
            description: 'Build to copy linear "model.json" from (-1 means lastSuccessful)'
            default: "{param-cmp-model-build}"
        - string:
            name: CMP_TOLERANCE
            description: Tolerance for mean values
            default: "{param-cmp-tolerance}"
        - string:
            name: CMP_STDDEV_TOLERANCE
            description: Tolerance for standard deviation values
            default: "{param-cmp-stddev-tolerance}"
        - bool:
            name: FIO_NBD_SETUP
            description: 'Compile and install fio with nbd ioengine enabled before test execution'
            default: "{param-fio-nbd-setup}"
        - string:
            name: DESCRIPTION_PREFIX
            description: Description prefix (describe the difference from default)
            default: ""
        - bool:
            name: PBENCH_PUBLISH
            description: 'Push the pbench results to company pbench server'
            default: "{param-pbench-publish}"
        - string:
            name: GITHUB_PUBLISHER_PROJECT
            description: 'Github publisher project ID (when you want to publish your results)'
            default: ""
        - text:
            name: HOST_SCRIPT
            description: 'Host script to be executed on all --servers'
            default: "{param-host-script}"
        - text:
            name: WORKER_SCRIPT
            description: 'Worker script to be executed on all runperf workers'
            default: "{param-worker-script}"

        - string:
            name: NO_ITERATIONS
            description: 'How many times to run each iteration'
            default: ''
        - string:
            name: DISTROS
            description: '`;` separated list of distributions to be installed/is installed (Fedora-31), when empty latest el8 nightly build is obtained from bkr, when `..` is used it uses bkr to fill all available versions in between the specified versions'
            default: "{param-distro}"
        - string:
            name: GUEST_DISTROS
            description: '`;` separated list of distribution to be installed on guest, when empty "distro" is used, when `..` is used it uses bkr to fill all available versions in between the specified versions'
            default: "{param-guest-distro}"
        - string:
            name: HOST_KERNEL_ARGSS
            description: '`;` separated list of Add custom kernel arguments on host'
            default: ""
        - string:
            name: HOST_BKR_LINKSS
            description: '`;` separated list of space separated lists of urls to be grepped for "http.*$arch\\.rpm" and "http.*noarch\\.rpm", filtered by HOST_BKR_LINKS_FILTER and then set to be installed on host/workers with "--allowerase". Intended usage is to use koji/beaker link like: https://koji.fedoraproject.org/koji/buildinfo?buildID=1534744'
            default: "{param-host-bkr-links}"
        - string:
            name: HOST_BKR_LINKS_FILTER
            description: 'Filter to be split and applied via "grep -v -e EXPR1 -e EXPR2 ..."'
            default: "debug"
        - string:
            name: GUEST_KERNEL_ARGSS
            description: '`;` separated list of custom kernel argsuments on workers/guests'
            default: ""
        - string:
            name: GUEST_BKR_LINKSS
            description: '`;` separated list of space separated lists of urls to be grepped for "http.*$arch\\.rpm" and "http.*noarch\\.rpm", filtered by GUEST_BKR_LINKS_FILTER and then set to be installed on host/workers with "--allowerase". Intended usage is to use koji/beaker link like: https://koji.fedoraproject.org/koji/buildinfo?buildID=1534744'
            default: ""
        - string:
            name: GUEST_BKR_LINKS_FILTER
            description: 'Filter to be split and applied via "grep -v -e EXPR1 -e EXPR2 ..."'
            default: "debug"
        - string:
            name: UPSTREAM_QEMU_COMMITS
            description: '`;` separated list of qemu commit/tags to be deployed from the upstream git. Use it by using $PROFILE:{{"qemu_bin": "/usr/local/bin/qemu-system-$ARCH"}} when specifying profiles.'
            default: "{param-upstream-qemu-commit}"
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "multi_runperf.groovy"
        lightweight-checkout: true


##############################################################################
# Definition of the analyze-perf job
##############################################################################
- job-template:
    name: "rp-analysis-{user}"
    project-type: pipeline
    concurrent: false
    description: |
        This job allows to cherry-pick results from runperf job and redo the analysis. It is
        not thread-safe, therefor it is advised to copy this job with user-suffix and run
        the analysis in series storing the graphs manually before submitting next comparison.
    parameters:
        - string:
            name: SRC_JOB
            default: "{param-src-job}"
            desciption: Source jenkins job
        - string:
            name: BUILDS
            default: ""
            description: "List of space separated build numbers to be analyzed, first build is used as source build (not included in graphs)"
        - string:
            name: DESCRIPTION
            default: ""
            description: Description of this analysis
        - string:
            name: CMP_MODEL_JOB
            description: 'Job to copy linear "model.json" from'
            default: "{param-cmp-model-job}"
        - string:
            name: CMP_MODEL_BUILD
            description: 'Build to copy linear "model.json" from (-1 means lastSuccessful)'
            default: "{param-cmp-model-build}"
        - string:
            name: CMP_TOLERANCE
            description: Tolerance for mean values
            default: "{param-cmp-tolerance}"
        - string:
            name: CMP_STDDEV_TOLERANCE
            description: Tolerance for standard deviation values
            default: "{param-cmp-stddev-tolerance}"
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "compareperf.groovy"
        lightweight-checkout: true


##############################################################################
# Definition of the analyze-perf job
##############################################################################
- job-template:
    name: "{name}-identify"
    project-type: pipeline
    description: |
        This job uses analyze-perf script to create model that can be used to better
        evaluate run-perf results.
    parameters:
        - string:
            name: SRC_JOB
            default: "{name}-run"
            desciption: Source jenkins job
        - string:
            name: BUILDS
            default: ""
            description: "List of space separated build numbers to be used"
        - string:
            name: DESCRIPTION
            default: ""
            description: Free-form description
        - string:
            name: EXTRA_ARGS
            default: ""
            description: Additional analyze-perf arguments, for example -t to override default tolerance
        - string:
            name: REBASE_MODEL_BUILD
            description: 'Build number of this job to be used as "--rebase-model" argument'
            default: ""
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "identify.groovy"
        lightweight-checkout: true


##############################################################################
# Definition of the prune artifacts job
##############################################################################
- job-template:
    name: "rp-prune-artifacts"
    node: master
    description: |
        Remove the big tar.xz files from oldish results not tagged
        as keep-forewer.
    triggers:
        - timed: "H 06 * * *"
    parameters:
        - string:
            name: JOB
            default: "{list,your,runperf,jobs,here,to,clean,them,daily}"
            description: Name of the job to be pruned
        - string:
            name: AGE
            default: "{param-age}"
            description: How old results should be pruned
    builders:
        - python:
            !include-raw-escape: ../scripts/prune_artifacts.py


##############################################################################
# Definition of the git-publisher job
##############################################################################
- job-template:
    name: "rp-publish-results-git"
    description: |
        Publish the build result in git so it can be viewed eg. in github
        pages
    project-type: pipeline
    parameters:
        - string:
            name: JOB
            default: ""
            description: "Job containing the result"
        - string:
            name: BUILD
            default: ""
            description: "Build of the job with the result"
        - bool:
            name: STATUS
            default: false
            description: "Status of the comparison (GOOD=true/BAD=false)"
        - string:
            name: NOTES
            default: ""
            description: "Notes to be added as description to the result entry"
        - string:
            name: PROJECT
            default: ""
            description: "Owner of the results (usually a group/company name + project/machine)"
        - string:
            name: TAG
            default: "all"
            description: "Version tag used to split results of different versions/tags"
        - bool:
            name: STRIP_RESULTS
            default: true
            description: "Publish stripped results (MB->KB)"
        - string:
            name: OS_VERSION
            default: ""
            description: "Override the os version"
        - string:
            name: QEMU_SHA
            defalut: ""
            description: "Override the qemu SHA"
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "publish-results-git.groovy"
        lightweight-checkout: true

###############################################################################
## Project to define jobs for automated regression jobs on example.org machine
###############################################################################
#- project:
#    name: rp-example
#    param-machine: "example.org"
#    param-arch: "x86_64"
#    param-src-build: 1
#    param-cmp-model-job: "{name}-identify"
#    param-cmp-model-build: -1
#    param-pbench-publish: true
#    jobs:
#        - "{name}-run"
#        - "{name}-identify"
#
#
###############################################################################
## Project to define manual jobs for example.org machine
###############################################################################
#- project:
#    name: rp-example-manual
#    param-machine: "example.org"
#    param-arch: "x86_64"
#    param-distro: "YOUR STABLE RELEASE"
#    param-src-build: 1
#    param-cmp-model-job: "rp-example-manual-identify"
#    param-cmp-model-build: 1
#    param-pbench-publish: false
#    param-job: "{name}-run"
#    trigger-on: ""
#    jobs:
#        - "{name}-run"
#        - "{name}-identify"
#        - "{name}-multi":
#            param-cmp-model-job: ''
#            param-cmp-model-build: ''
#
#
###############################################################################
## Project to allow users to run custom queries out of existing results
###############################################################################
#- project:
#    name: rp-analysis
#    user:
#        - virt
#    param-src-job: "rp-example-manual"
#    param-cmp-model-job: "rp-example-manual-identify"
#    param-cmp-model-build: 1
#    jobs:
#        - "rp-analysis-{user}"
#
###############################################################################
## Prune artifacts after 14 days, hopefully we would notice and mark/move
## them when full details are needed.
###############################################################################
#- project:
#    name: rp-prune-artifacts
#    param-age: 14
#    jobs:
#        - "rp-prune-artifacts"

Now let’s have a look at the runperf.groovy pipeline:

// Pipeline to run runperf and compare to given results
// groovylint-disable-next-line
@Library('runperf') _

// Following `params` have to be defined in job (eg. via jenkins-job-builder)

// Machine to be provisioned and tested
machine = params.MACHINE.trim()
// target machine's architecture
arch = params.ARCH.trim()
// Distribution to be installed/is installed (Fedora-32)
// when empty it will pick the latest available nightly el8
_distro = params.DISTRO.trim()
_distro = _distro ?: 'latest-RHEL-8.0%.n.%'
// Distribution to be installed on guest, when empty "distro" is used
guestDistro = params.GUEST_DISTRO.trim()
// Space separated list of tests to be executed
tests = params.TESTS.trim()
// Space separated list of profiles to be applied
profiles = params.PROFILES.trim()
// Base build to compare with
srcBuild = params.SRC_BUILD.trim()
// Compareperf tollerances
cmpModelJob = params.CMP_MODEL_JOB.trim()
cmpModelBuild = params.CMP_MODEL_BUILD.trim()
cmpTolerance = params.CMP_TOLERANCE.trim()
cmpStddevTolerance = params.CMP_STDDEV_TOLERANCE.trim()
// Add custom kernel arguments on host
hostKernelArgs = params.HOST_KERNEL_ARGS.trim()
// Install rpms from (beaker) urls
hostBkrLinks = params.HOST_BKR_LINKS.trim()
// filters for hostBkrLinks
hostBkrLinksFilter = params.HOST_BKR_LINKS_FILTER.trim()
// Add custom kernel argsuments on workers/guests
guestKernelArgs = params.GUEST_KERNEL_ARGS.trim()
// Install rpms from (beaker) urls
guestBkrLinks = GUEST_BKR_LINKS.trim()
// filters for guestBkrLinks
guestBkrLinksFilter = params.GUEST_BKR_LINKS_FILTER.trim()
// Add steps to fetch, compile and install the upstream fio with nbd ioengine compiled in
fioNbdSetup = params.FIO_NBD_SETUP
// Add steps to checkout, compile and install the upstream qemu from git
upstreamQemuCommit = params.UPSTREAM_QEMU_COMMIT.trim()
// Add steps to install the latest kernel from koji (Fedora rpm)
fedoraLatestKernel = params.FEDORA_LATEST_KERNEL
// Description prefix (describe the difference from default)
descriptionPrefix = params.DESCRIPTION_PREFIX
// Number of reference builds
noReferenceBuilds = params.NO_REFERENCE_BUILDS.toInteger()
// Pbench-publish related options
pbenchPublish = params.PBENCH_PUBLISH
// Github-publisher project ID
githubPublisherProject = params.GITHUB_PUBLISHER_PROJECT.trim()
githubPublisherTag = ''
// Additional run-perf metadata
metadata = params.METADATA
// Custom host/guest setups cript
hostScript = params.HOST_SCRIPT
workerScript = params.WORKER_SCRIPT

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// runperf git branch
gitBranch = 'main'
// extra runperf arguments
extraArgs = ''

node(workerNode) {
    stage('Preprocess') {
        (distro, guestDistro, descriptionPrefix) = runperf.preprocessDistros(_distro, guestDistro,
                                                                             arch, descriptionPrefix)
        currentBuild.description = "${distro} - in progress"
    }

    stage('Measure') {
        runperf.deployDownstreamConfig(gitBranch)
        runperf.deployRunperf(gitBranch)
        // Use grubby to update default args on host
        hostScript = runperf.setupScript(hostScript, hostKernelArgs, hostBkrLinks, hostBkrLinksFilter,
                                         arch, fioNbdSetup)
        workerScript = runperf.setupScript(workerScript, guestKernelArgs, guestBkrLinks, guestBkrLinksFilter,
                                           arch, fioNbdSetup)
        // Build custom qemu
        if (upstreamQemuCommit) {
            // Always translate the user input into the actual commit and also get the description
            sh 'rm -Rf upstream_qemu'
            dir('upstream_qemu') {
                sh 'git clone --filter=tree:0 https://gitlab.com/qemu-project/qemu.git .'
                upstreamQemuVersion = sh(returnStdout: true,
                                         script: "git rev-parse ${upstreamQemuCommit}").trim()
                githubPublisherTag = sh(returnStdout: true,
                                        script: "git describe --tags --always ${upstreamQemuCommit}"
                                       ).trim().split('-')[0]
                println("Using qemu $githubPublisherTag commit $upstreamQemuVersion")
            }
            sh '\\rm -Rf upstream_qemu'
            hostScript += '\n\n# UPSTREAM_QEMU_SETUP'
            hostScript += '\nOLD_PWD="$PWD"'
            hostScript += '\ndnf install --skip-broken -y python3-devel zlib-devel gtk3-devel glib2-static '
            hostScript += 'spice-server-devel usbredir-devel make gcc libseccomp-devel numactl-devel '
            hostScript += 'libaio-devel git ninja-build'
            hostScript += '\ncd /root'
            hostScript += '\n[ -e "qemu" ] || { mkdir qemu; cd qemu; git init; git remote add origin '
            hostScript += 'https://gitlab.com/qemu-project/qemu.git; cd ..; }'
            hostScript += '\ncd qemu'
            hostScript += "\ngit fetch --depth=1 origin ${upstreamQemuVersion}"
            hostScript += "\ngit checkout -f ${upstreamQemuVersion}"
            hostScript += '\ngit submodule update --init'
            hostScript += '\nVERSION=$(git rev-parse HEAD)'
            hostScript += '\ngit diff --quiet || VERSION+="-dirty"'
            hostScript += '\n./configure --target-list="$(uname -m)"-softmmu --disable-werror --enable-kvm '
            hostScript += '--enable-vhost-net --enable-attr --enable-fdt --enable-vnc --enable-seccomp '
            hostScript += '--enable-usb-redir --disable-opengl --disable-virglrenderer '
            hostScript += '--with-pkgversion="$VERSION"'
            hostScript += runperf.makeInstallCmd
            hostScript += '\nchcon -Rt qemu_exec_t /usr/local/bin/qemu-system-"$(uname -m)"'
            hostScript += '\n\\cp -f build/config.status /usr/local/share/qemu/'
            hostScript += '\ncd $OLD_PWD'
        }
        // Install the latest kernel from koji (Fedora rpm)
        if (fedoraLatestKernel) {
            kernelBuild = sh(returnStdout: true,
                             script: ("curl '$runperf.kojiUrl/packageinfo?packageID=8' | " +
                                      'grep -B 4 "complete" | grep "kernel" | ' +
                                      'grep "git" | grep -m 1 -o -e \'href="[^"]*"\'')
                             ).trim()[6..-2]
            kernelBuildUrl = runperf.kojiUrl + kernelBuild
            kernelBuildFilter = 'debug bpftool kernel-tools perf kernel-selftests kernel-doc'
            hostScript += runperf.getBkrInstallCmd(kernelBuildUrl, kernelBuildFilter, arch)
            workerScript += runperf.getBkrInstallCmd(kernelBuildUrl, kernelBuildFilter, arch)
        }
        if (hostScript) {
            writeFile file: 'host_script', text: hostScript
            extraArgs += ' --host-setup-script host_script --host-setup-script-reboot'
        }
        if (workerScript) {
            writeFile file: 'worker_script', text: workerScript
            extraArgs += ' --worker-setup-script worker_script'
        }
        if (pbenchPublish) {
            metadata += ' pbench_server_publish=yes'
        }
        // Using jenkins locking to prevent multiple access to a single machine
        lock(machine) {
            sh '$KINIT'
            status = sh(returnStatus: true,
               script: "python3 scripts/run-perf ${extraArgs} -v --hosts ${machine} --distro ${distro} " +
               "--provisioner Beaker --default-password YOUR_DEFAULT_PASSWORD --profiles ${profiles} " +
               '--log run.log --paths ./downstream_config --metadata ' +
               "'build=${currentBuild.number}${descriptionPrefix}' " +
               "'url=${currentBuild.absoluteUrl}' 'project=YOUR_PROJECT_ID ${currentBuild.projectName}' " +
               "'pbench_server=YOUR_PBENCH_SERVER_URL' " +
               "'machine_url_base=https://YOUR_BEAKER_URL/view/%(machine)s' " +
               "${metadata} -- ${tests}")
        }
        // Add new-line after runperf output (ignore error when does not exists
        sh(returnStatus: true, script: "echo >> \$(echo -n result*)/RUNPERF_METADATA")
        stage('Archive results') {
            // Archive only "result_*" as we don't want to archive "resultsNoArchive"
            sh returnStatus: true, script: 'tar cf - result_* | xz -T2 -7e - > "$(echo result_*)".tar.xz'
            archiveArtifacts allowEmptyArchive: true, artifacts: runperf.runperfArchiveFilter
        }
        if (status) {
            runperf.tryOtherDistros(_distro, arch)
            runperf.failBuild('Run-perf execution failed',
                              "run-perf returned non-zero status ($status)",
                              distro)
        }
    }

    stage('Compare') {
        // Get up to noReferenceBuilds json results to use as a reference
        referenceBuilds = []
        for (build in runperf.getGoodBuildNumbers(env.JOB_NAME)) {
            copyArtifacts(filter: runperf.runperfResultsFilter, optional: true,
                          fingerprintArtifacts: true, projectName: env.JOB_NAME, selector: specific("${build}"),
                          target: "reference_builds/${build}/")
            if (findFiles(glob: "reference_builds/${build}/result*/*/*/*/*.json")) {
                referenceBuilds.add("${build}:" + sh(returnStdout: true,
                                     script: "echo reference_builds/${build}/*").trim())
                if (referenceBuilds.size() >= noReferenceBuilds) {
                    break
                }
            }
        }
        // Get src build's json results to compare against
        copyArtifacts(filter: runperf.runperfResultsFilter, optional: true,
                      fingerprintArtifacts: true, projectName: env.JOB_NAME, selector: specific(srcBuild),
                      target: 'src_result/')
        // If model build set get the model from it's job
        if (cmpModelBuild) {
            if (cmpModelBuild == '-1') {
                copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                              projectName: cmpModelJob, selector: lastSuccessful(), target: runperf.thisPath)
            } else {
                copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                              projectName: cmpModelJob, selector: specific(cmpModelBuild), target: runperf.thisPath)
            }
            cmpExtra = '--model-linear-regression ' + runperf.modelJson
        } else {
            cmpExtra = ''
        }
        // Compare the results and generate html as well as xunit results
        status = sh(returnStatus: true,
                    script: ('python3 scripts/compare-perf --log compare.log ' +
                             '--tolerance ' + cmpTolerance + ' --stddev-tolerance ' + cmpStddevTolerance +
                             " --xunit ${runperf.resultXml} --html ${runperf.htmlIndex} --html-small-file " + cmpExtra +
                             ' -- src_result/* ' + referenceBuilds.reverse().join(' ') +
                             ' $(find . -maxdepth 1 -type d ! -name "*.tar.*" -name "result*")'))
        if (fileExists(runperf.resultXml)) {
            if (status) {
                // This could mean there were no tests to compare or other failures, interrupt the build
                echo "Non-zero exit status: ${status}"
            }
        } else {
            runperf.failBuild('Compare-perf execution failed',
                              "Missing ${runperf.resultXml}, exit code: ${status}",
                              distro)
        }
    }

    stage('Postprocess') {
        // Build description
        currentBuild.description = "${descriptionPrefix}${srcBuild} ${currentBuild.number} ${distro}"
        // Store and publish html results
        archiveArtifacts allowEmptyArchive: true, artifacts: runperf.htmlIndex
        if (fileExists(runperf.htmlPath)) {
            publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: true, reportDir: runperf.htmlPath,
                         reportFiles: runperf.htmlFile, reportName: 'HTML Report', reportTitles: ''])
        }
        // Junit results
        junit allowEmptyResults: true, testResults: runperf.resultXml
        // Remove the unnecessary big files
        sh runperf.runperfArchFilterRmCmd
        // Publish the results
        if (githubPublisherProject) {
            build(job: 'rp-publish-results-git',
                   parameters: [string(name: 'JOB', value: env.JOB_NAME),
                                string(name: 'BUILD', value: env.BUILD_NUMBER),
                                booleanParam(name: 'STATUS', value: status == 0),
                                string(name: 'NOTES', value: descriptionPrefix),
                                string(name: 'PROJECT', value: githubPublisherProject),
                                string(name: 'TAG', value: githubPublisherTag),
                                booleanParam(name: 'STRIP_RESULTS', value: true)],
                   quietPeriod: 0,
                   wait: false)
        }
    }
}

Following compareperf.groovy pipeline is extremely useful for later analysis, or extra comparison of manual pipelines:

// Pipeline to create comparison of previously generated runperf results
// Following `params` have to be defined in job (eg. via jenkins-job-builder)
// groovylint-disable-next-line
@Library('runperf') _

// Source jenkins job
srcJob = params.SRC_JOB.trim()
// List of space separated build numbers to be analyzed, first build is used
// as source build (not included in graphs)
builds = params.BUILDS.split().toList()
// Description of this analysis
description = params.DESCRIPTION
// Compareperf tollerances
cmpModelJob = params.CMP_MODEL_JOB.trim()
cmpModelBuild = params.CMP_MODEL_BUILD.trim()
cmpTolerance = params.CMP_TOLERANCE.trim()
cmpStddevTolerance = params.CMP_STDDEV_TOLERANCE.trim()

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// runperf git branch
gitBranch = 'main'
// misc variables
thisPath = '.'
spaceChr = ' '
lastBuildChr = '-1'

stage('Analyze') {
    node(workerNode) {
        assert builds.size() >= 2
        runperf.deployRunperf(gitBranch)
        referenceBuilds = []
        // Get all the reference builds (second to second-to-last ones)
        if (builds.size() > 2) {
            for (build in builds[1..-2]) {
                copyArtifacts(filter: runperf.runperfResultsFilter, optional: true,
                              fingerprintArtifacts: true, projectName: srcJob, selector: specific(build),
                              target: "reference_builds/${build}/")
                if (fileExists("reference_builds/${build}")) {
                    referenceBuilds.add("${build}:" + sh(returnStdout: true,
                                                          script: "echo reference_builds/${build}/*").trim())
                } else {
                    echo "Skipping reference build ${build}, failed to copy artifacts."
                }
            }
        }
        // Get the source build
        copyArtifacts(filter: runperf.runperfResultsFilter, optional: false,
                      fingerprintArtifacts: true, projectName: srcJob, selector: specific(builds[0]),
                      target: 'src_result/')
        // Get the destination build
        copyArtifacts(filter: runperf.runperfResultsFilter, optional: false,
                      fingerprintArtifacts: true, projectName: srcJob, selector: specific(builds[-1]),
                      target: thisPath)
        // Get the model
        if (cmpModelBuild) {
            if (cmpModelBuild == lastBuildChr) {
                copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                              projectName: cmpModelJob, selector: lastSuccessful(), target: thisPath)
            } else {
                copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                              projectName: cmpModelJob, selector: specific(cmpModelBuild), target: thisPath)
            }
            cmpExtra = '--model-linear-regression ' + runperf.modelJson
        } else {
            cmpExtra = ''
        }
        status = 0
        lock(workerNode) {
            // Avoid modifying workerNode's environment while executing compareperf
            sh runperf.pythonDeployCmd
            status = sh(returnStatus: true,
                        script: ('python3 scripts/compare-perf -vvv --tolerance ' + cmpTolerance +
                                 ' --stddev-tolerance ' + cmpStddevTolerance +
                                 ' --xunit ' + runperf.resultXml + ' --html ' + runperf.htmlIndex + spaceChr +
                                 cmpExtra + ' -- src_result/* ' + referenceBuilds.join(spaceChr) +
                                 ' $(find . -maxdepth 1 -type d ! -name "*.tar.*" -name "result*")'))
        }
        if (fileExists(runperf.resultXml)) {
            if (status) {
                // This could mean there were no tests to compare or other failures, interrupt the build
                echo "Non-zero exit status: ${status}"
            }
        } else {
            currentBuild.result = 'FAILED'
            error "Missing ${runperf.resultXml}, exit code: ${status}"
        }
        currentBuild.description = "${description}${builds} ${srcJob}"
        archiveArtifacts allowEmptyArchive: true, artifacts: runperf.htmlIndex
        junit allowEmptyResults: true, testResults: runperf.resultXml
        if (fileExists(runperf.htmlPath)) {
            publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: true, reportDir: runperf.htmlPath,
                         reportFiles: runperf.htmlFile, reportName: 'HTML Report', reportTitles: ''])
        }
        // Remove the unnecessary big files
        sh '\\rm -Rf result* src_result* reference_builds'
    }
}

And the identify.groovy to allow creating linear models:

// Pipeline to create comparison of previously generated runperf results
// groovylint-disable-next-line
@Library('runperf') _

// Following `params` have to be defined in job (eg. via jenkins-job-builder)
// Source jenkins job
srcJob = params.SRC_JOB
// List of space separated build numbers to be analyzed, first build is used
// as source build (not included in graphs)
builds = params.BUILDS.split().toList()
// Description of this analysis
description = params.DESCRIPTION
// Extra AnalyzePerf arguments
extraArgs = params.EXTRA_ARGS
// Build number used for --rebase-model
rebaseModelBuild = params.REBASE_MODEL_BUILD

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// runperf git branch
gitBranch = 'main'
// misc variables
spaceChr = ' '

stage('Analyze') {
    node(workerNode) {
        runperf.deployRunperf(gitBranch)
        // Get all the specified builds
        for (build in builds) {
            copyArtifacts(filter: 'result*/**/result*.json', optional: false, fingerprintArtifacts: true,
                          projectName: srcJob, selector: specific(build), target: 'results/')
        }
        // If rebaseModel set, get the model from that build
        if (rebaseModelBuild) {
            copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                          projectName: env.JOB_NAME, selector: specific(rebaseModelBuild),
                          target: 'src_model/')
            extraArgs += " --rebase-model 'src_model/$runperf.modelJson'"
        }
        status = 0
        lock(workerNode) {
            // Avoid modifying workerNode's environment while executing compareperf
            sh 'python3 setup.py develop --user'
            status = sh(returnStatus: true,
                        script: ('python3 scripts/analyze-perf -vvv --stddev-linear-regression ' +
                                 runperf.modelJson + spaceChr + extraArgs + ' -- results/*'))
        }
        if (fileExists(runperf.modelJson)) {
            // This could mean there were no tests to compare or other failures, interrupt the build
            if (status) {
                echo "Non-zero exit status: ${status}"
            }
        } else {
            currentBuild.result = 'FAILED'
            error "Missing ${runperf.modelJson}, exit code: ${status}"
        }
        if (description) {
            currentBuild.description = description + spaceChr + builds.join(spaceChr)
        } else {
            currentBuild.description = builds.join(spaceChr)
        }
        archiveArtifacts allowEmptyArchive: true, artifacts: runperf.modelJson
        sh '\\rm -Rf results*'
    }
}

The cleanup job prune_artifacts.py:

#!/bin/env python3
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2020
# Author: Lukas Doktor <ldoktor@redhat.com>
"""
When executed on a jenkins master it allows to walk the results and remove
"*.tar.*" files on older builds that are not manually marked as
keep-for-infinity.
"""

import time
import glob
import os
import re

JENKINS_DIR = "/var/lib/jenkins/jobs/"


def prune_result(path, before):
    """
    Prune result if older than age and keep forever not set
    """
    build_path = os.path.join(path, "build.xml")
    if not os.path.exists(build_path):
        print("KEEP  %s - no build.xml" % path)
        return
    treated_path = os.path.join(path, "ld_artifact_pruned")
    if os.path.exists(treated_path):
        print("SKIP  %s - already treated" % path)
        return
    with open(build_path) as build_fd:
        build_xml = build_fd.read()
    if "<keepLog>false</keepLog>" not in build_xml:
        print("KEEP  %s - keep forever set" % path)
        return
    match = re.findall(r"<startTime>(\d+)</startTime>", build_xml)
    if not match:
        print("KEEP  %s - no startTime\n%s" % (path, build_xml))
        return
    start_time = int(match[-1])
    if start_time > before:
        print("KEEP  %s - younger than %s (%s)" % (path, before, start_time))
        return
    print("PRUNE %s (%s)" % (path, start_time))
    for pth in glob.glob(os.path.join(path, "archive", "*.tar.*")):
        os.unlink(pth)
    with open(treated_path, 'wb'):
        """touching the file"""


def prune_results(job, age):
    """
    Walk job's builds and prune them
    """
    if not job:
        print("No job specified, returning")
        return
    # Jenkins stores startTime * 1000
    before = int((time.time() - age) * 1000)
    print("Pruning %s builds older than %s" % (job, before))
    builds = glob.glob(os.path.join(JENKINS_DIR, job, "builds", "*"))
    for build in builds:
        prune_result(build, before)
    print("Done")


if __name__ == '__main__':
    prune_results(os.environ.get('JOB'),
                  int(os.environ.get('AGE', 14)) * 86400)

And a bisect job upstream_bisect.groovy:

// Pipeline to run runperf and compare to given results
// groovylint-disable-next-line
@Library('runperf') _

// Following `params` have to be defined in job (eg. via jenkins-job-builder)
// Machine to be provisioned and tested
machine = params.MACHINE.trim()
// target machine's architecture
arch = params.ARCH.trim()
// Distribution to be installed/is installed (Fedora-32)
// when empty it will pick the latest available nightly el8
distro = params.DISTRO.trim()
// Distribution to be installed on guest, when empty "distro" is used
guestDistro = params.GUEST_DISTRO.trim()
// Space separated list of tests to be executed
tests = params.TESTS.trim()
// Space separated list of profiles to be applied
profiles = params.PROFILES.trim()
// Add custom kernel arguments on host
hostKernelArgs = params.HOST_KERNEL_ARGS.trim()
// Install rpms from (beaker) urls
hostBkrLinks = params.HOST_BKR_LINKS.trim()
// filters for hostBkrLinks
hostBkrLinksFilter = params.HOST_BKR_LINKS_FILTER.trim()
// Add custom kernel argsuments on workers/guests
guestKernelArgs = params.GUEST_KERNEL_ARGS.trim()
// Install rpms from (beaker) urls
guestBkrLinks = GUEST_BKR_LINKS.trim()
// filters for guestBkrLinks
guestBkrLinksFilter = params.GUEST_BKR_LINKS_FILTER.trim()
// Add steps to fetch, compile and install the upstream fio with nbd ioengine compiled in
fioNbdSetup = params.FIO_NBD_SETUP
// Specify the bisection range
// Older commit
upstreamQemuGood = params.UPSTREAM_QEMU_GOOD.trim()
// Newer commit
upstreamQemuBad = params.UPSTREAM_QEMU_BAD.trim()
// Description prefix (describe the difference from default)
descriptionPrefix = params.DESCRIPTION_PREFIX
// Pbench-publish related options
pbenchPublish = params.PBENCH_PUBLISH
// Custom host/guest setups cript
hostScript = params.HOST_SCRIPT
workerScript = params.WORKER_SCRIPT

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// runperf git branch
gitBranch = 'main'
// extra runperf arguments
extraArgs = ''

String getBkrInstallCmd(String hostBkrLinks, String hostBkrLinksFilter, String arch) {
    return ('\nfor url in ' + hostBkrLinks + '; do dnf install -y --allowerasing ' +
            '$(curl -k \$url | grep -o -e "http[^\\"]*' + arch + '\\.rpm" -e ' +
            '"http[^\\"]*noarch\\.rpm" | grep -v $(for expr in ' + hostBkrLinksFilter + '; do ' +
            'echo -n " -e $expr"; done)); done')
}

node(workerNode) {
    stage('Preprocess') {
        (distro, guestDistro, descriptionPrefix) = runperf.preprocessDistros(distro, guestDistro,
                                                                             arch, descriptionPrefix)
        currentBuild.description = "${distro} - in progress"
    }

    stage('Measure') {
        runperf.deployDownstreamConfig(gitBranch)
        runperf.deployRunperf(gitBranch)
        metadata = ''
        hostScript = runperf.setupScript(hostScript, hostKernelArgs, hostBkrLinks, hostBkrLinksFilter,
                                         arch, fioNbdSetup)
        workerScript = runperf.setupScript(workerScript, guestKernelArgs, guestBkrLinks, guestBkrLinksFilter,
                                           arch, fioNbdSetup)
        if (hostScript) {
            writeFile file: 'host_script', text: hostScript
            extraArgs += ' --host-setup-script host_script --host-setup-script-reboot'
        }
        if (workerScript) {
            writeFile file: 'worker_script', text: workerScript
            extraArgs += ' --worker-setup-script worker_script'
        }
        if (pbenchPublish) {
            metadata += ' pbench_server_publish=yes'
        }
        // Using jenkins locking to prevent multiple access to a single machine
        lock(machine) {
            // Make sure we have the full upstream_qemu cloned (we don't need submodules, thought)
            sh 'rm -Rf upstream_qemu/'
            sh 'git clone https://gitlab.com/qemu-project/qemu.git upstream_qemu/'
            sh '$KINIT'
            // First run the provisioning and dummy test to age the machine a bit
            sh("python3 scripts/run-perf ${extraArgs} -v --hosts ${machine} --distro ${distro} " +
               '--provisioner Beaker --default-password YOUR_DEFAULT_PASSWORD ' +
               '--profiles DefaultLibvirt --paths ./downstream_config --log prejob.log -- ' +
              '\'fio:{"runtime": "30", "targets": "/fio", "block-sizes": "4", "test-types": "read", ' +
              '"samples": "1"}\'')
            // And now run the bisection without reprovisioning
            sh("DIFFPERF='python3 scripts/diff-perf' contrib/upstream_qemu_bisect.sh upstream_qemu/ " +
               "${upstreamQemuGood} ${upstreamQemuBad} python3 scripts/run-perf ${extraArgs} " +
               "-v --hosts ${machine} --distro ${distro} --log job.log " +
               "--default-password YOUR_DEFAULT_PASSWORD --profiles ${profiles} " +
               "--paths ./downstream_config --metadata " +
               "'project=virt-perf-ci ${currentBuild.projectName}' " +
               "'pbench_server=YOUR_PBENCH_SERVER_URL' " +
               "'machine_url_base=https://YOUR_BEAKER_URL/view/%(machine)s' " +
               "${metadata} -- ${tests}")
        }
    }

    stage('Postprocess') {
        // Build description
        currentBuild.description = "${descriptionPrefix} ${currentBuild.number} ${distro}"
        // Move results to mimic usual run-perf results path
        if (fileExists('.diff-perf/report.html')) {
            diffReportPath = 'html/index.html'
            sh('mkdir -p html')
            sh("mv '.diff-perf/report.html' '$diffReportPath'")
            // Store and publish html results
            archiveArtifacts allowEmptyArchive: true, artifacts: diffReportPath
            publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'html/',
                         reportFiles: 'index.html', reportName: 'HTML Report', reportTitles: ''])
        }
        // Remove the unnecessary big files
        sh 'contrib/bisect.sh clean'
    }
}

And a range job to trigger multiple runperf jobs:

// Pipeline to trigger a series of run-perf jobs to cover a range of params.
// Following `params` have to be defined in job (eg. via jenkins-job-builder)
// groovylint-disable-next-line
@Library('runperf') _

csvSeparator = ';'

// SHARED VALUES FOR ALL JOBS
// Job name to be triggered
jobName = params.JOB_NAME.trim()
// Machine to be provisioned and tested
machine = params.MACHINE.trim()
// target machine's architecture
arch = params.ARCH.trim()
// Space separated list of tests to be executed
tests = params.TESTS.trim()
// Space separated list of profiles to be applied
profiles = params.PROFILES.trim()
// Compareperf tollerances
cmpModelJob = params.CMP_MODEL_JOB.trim()
cmpModelBuild = params.CMP_MODEL_BUILD.trim()
cmpTolerance = params.CMP_TOLERANCE.trim()
cmpStddevTolerance = params.CMP_STDDEV_TOLERANCE.trim()
// Add steps to fetch, compile and install the upstream fio with nbd ioengine compiled in
fioNbdSetup = params.FIO_NBD_SETUP
// Description prefix (describe the difference from default)
descriptionPrefix = params.DESCRIPTION_PREFIX
// Pbench-publish related options
pbenchPublish = params.PBENCH_PUBLISH
// Github-publisher project ID
githubPublisherProject = params.GITHUB_PUBLISHER_PROJECT.trim()

// LIST OF VALUES
// Iterations of each combination
if (params.NO_ITERATIONS) {
    iterations = 1..params.NO_ITERATIONS.toInteger()
} else {
    iterations = [1]
}
// Distribution to be installed/is installed (Fedora-32)
// when empty it will pick the latest available nightly el8
distrosRaw = params.DISTROS.split(csvSeparator)
// Distribution to be installed on guest, when empty "distro" is used
guestDistrosRaw = params.GUEST_DISTROS.split(csvSeparator)
// Add custom kernel arguments on host
hostKernelArgss = params.HOST_KERNEL_ARGSS.split(csvSeparator)
// Install rpms from (beaker) urls
hostBkrLinkss = params.HOST_BKR_LINKSS.split(csvSeparator)
// filters for hostBkrLinks
hostBkrLinksFilter = params.HOST_BKR_LINKS_FILTER
// Add custom kernel argsuments on workers/guests
guestKernelArgss = params.GUEST_KERNEL_ARGSS.split(csvSeparator)
// Install rpms from (beaker) urls
guestBkrLinkss = GUEST_BKR_LINKSS.split(csvSeparator)
// filters for guestBkrLinks
guestBkrLinksFilter = params.GUEST_BKR_LINKS_FILTER
// Add steps to checkout, compile and install the upstream qemu from git
upstreamQemuCommits = params.UPSTREAM_QEMU_COMMITS.split(csvSeparator)
// Custom host/guest setups cript
hostScript = params.HOST_SCRIPT
workerScript = params.WORKER_SCRIPT

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// misc variables
srcBuildUnset = '-1'

distros = runperf.getDistrosRange(distrosRaw, workerNode, arch)
guestDistros = runperf.getDistrosRange(guestDistrosRaw, workerNode, arch)

referenceBuilds = 0
srcBuild = srcBuildUnset
paramTypes = [iterations, guestBkrLinkss, guestKernelArgss, hostBkrLinkss, hostKernelArgss,
               upstreamQemuCommits, guestDistros, distros]
for (params in paramTypes.combinations()) {
    println("Triggering with: $params")
    if (params[0] == 1) {
        prefix = descriptionPrefix
    } else {
        prefix = "${descriptionPrefix}${params[0]}"
    }
    // TODO: Add no-provisioning-version
    // Use a cleanup job to remove host-setup-script things
    srcBuild = runperf.triggerRunperf(env.JOB_NAME, srcBuild == srcBuildUnset, params[7], params[6],
                                      machine, arch, tests, profiles, srcBuild, params[4],
                                      params[3], hostBkrLinksFilter, params[2], params[1],
                                      guestBkrLinksFilter, params[5], prefix, pbenchPublish,
                                      fioNbdSetup, Math.max(0, referenceBuilds).toString(),
                                      cmpModelJob, cmpModelBuild, cmpTolerance, cmpStddevTolerance,
                                      githubPublisherProject, hostScript, workerScript)
    referenceBuilds += 1
}

To enable the Jenkins shared libraries look at Extending with Shared Libraries tutorial. Once you have your repo enabled, place the following file:

import groovy.transform.Field

// Use this by adding: @Library('runperf') _

// misc variables
@Field String resultXml = 'result.xml'
@Field String htmlPath = 'html'
@Field String htmlFile = 'index.html'
@Field String htmlIndex = "${htmlPath}/${htmlFile}"
@Field String modelJson = 'model.json'
@Field String thisPath = '.'
@Field String runperfArchiveFilter = ('result*/*/*/*/*.json,result*/RUNPERF_METADATA,result*/**/__error*__/**,' +
                                    'result*/**/__sysinfo*__/**,result_*.tar.xz,*.log')
@Field String runperfArchFilterRmCmd = "\\rm -Rf result* src_result* reference_builds ${htmlPath} *.log"
@Field String runperfResultsFilter = 'result*/*/*/*/*.json,result*/RUNPERF_METADATA,result*/**/__error*__/**'
@Field String makeInstallCmd = '\nmake -j $(getconf _NPROCESSORS_ONLN)\nmake install'
@Field String pythonDeployCmd = 'python3 setup.py develop --user'
@Field String kojiUrl = 'https://koji.fedoraproject.org/koji/'
@Field fioNbdScript = ('\n\n# FIO_NBD_SETUP' +
                       '\ndnf install --skip-broken -y fio gcc zlib-devel libnbd-devel make qemu-img libaio-devel' +
                       '\ncd /tmp' +
                       '\ncurl -L https://github.com/axboe/fio/archive/fio-3.19.tar.gz | tar xz' +
                       '\ncd fio-fio-3.19' +
                       '\n./configure --enable-libnbd\n' +
                       makeInstallCmd)

@Field String labController = 'ENTER_LAB_CONTROLLER_URL'
@Field String ownerEmail = 'ENTER_OPERATOR_EMAIL_ADDR

void failBuild(String subject, String details, String distro=distro) {
    // Set description, send email and raise exception
    currentBuild.description = "BAD ${distro} - $details"
    mail(to: ownerEmail,
         subject: "${env.JOB_NAME}: $subject",
         body: "Job: ${env.BUILD_URL}\n\n$details")
    error details
}

List preprocessDistros(String distro, String guestDistro, String arch, descriptionPrefix) {
    // Parses the distro and guestDistro params into actual [distro, guestDistro]
    if (distro.startsWith('latest-')) {
        if (distro.startsWith('latest-untested-')) {
            distro = getLatestUntestedDistro(distro[16..-1], arch)
            descriptionPrefix += 'U'    // To emphasize we use "untested" distros
            echo "Using latest-untested distro ${distro} from bkr"
        } else {
            distro = getLatestDistros(distro[7..-1], 1, arch)[0]
            echo "Using latest distro ${distro} from bkr"
        }
    } else {
        echo "Using distro ${distro} from params"
    }
    if (!guestDistro) {
        guestDistro == distro
    }
    if (guestDistro == distro) {
        echo "Using the same guest distro ${distro}"
    } else {
        echo "Using different guest distro: ${guestDistro} from host: ${distro}"
    }
    return [distro, guestDistro, descriptionPrefix]
}

void deployRunperf(gitBranch) {
    git branch: gitBranch, url: 'https://github.com/distributed-system-analysis/run-perf.git'
    // Remove files that might have been left behind
    sh runperfArchFilterRmCmd
    sh "mkdir ${htmlPath}"
    sh pythonDeployCmd
}

void deployDownstreamConfig(gitBranch) {
    // This way we add downstream plugins and other configuration
    dir('downstream_config') {
        git branch: gitBranch, url: 'git://PATH_TO_YOUR_REPO_WITH_PIPELINES/runperf_config.git'
        sh pythonDeployCmd
    }
}

String getBkrInstallCmd(String hostBkrLinks, String hostBkrLinksFilter, String arch) {
    // Constructs bash script to install pkgs from beaker
    return ('\ndnf remove -y --skip-broken qemu-kvm;' +
            '\nfor url in ' + hostBkrLinks + '; do dnf install -y --allowerasing --skip-broken ' +
            '$(curl -k \$url | grep -oP \'href="\\K[^"]*(noarch|' + arch + ')\\.rpm\' | ' +
            'sed -e "/^http/! s#^#$url/#" | grep -v $(for expr in ' + hostBkrLinksFilter + '; do ' +
            'echo -n " -e $expr"; done)); done')
}

List getLatestDistros(String name, Integer limit, String arch) {
    // Return latest $limit distros matching the name (use % to match anything)
    distros = sh(returnStdout: true,
                 script: ('echo -n $(bkr distro-trees-list --arch  ' + arch + ' --name=' + name +
                          ' --limit ' + limit + ' --labcontroller ' + bkrLabController +
                          ' --format json | grep \'"distro_name"\' | cut -d\'"\' -f4)'
                         )).trim().split()
    return(distros)
}

@NonCPS
List getTestedDistros(String jobName, String distro) {
    // Turn our distro (RHEL-8.0.0-20000000.n.0) into regex (RHEL-d.d.d-dddddddd.n.d)
    // (this is unsafe method that leaves the '.' and such, but should do for now)
    reNum = '[0-9]'
    reDistro = distro.replaceAll(reNum, reNum)
    reDistro = reDistro.replaceAll('%', '[^ ]*')
    distros = []
    for (build in Jenkins.instance.getJob(jobName).builds) {
        build?.description?.eachMatch(reDistro) {
            dist -> distros.add(dist)
        }
    }
    return(distros)
}

String getLatestUntestedDistro(String distro, String arch) {
    // Return latest distro that has not been tested by this job yet
    tested_distros = getTestedDistros(env.JOB_NAME, distro)
    latest_distros = getLatestDistros(distro, 10, arch)
    for (dist in latest_distros) {
        if (!(dist in tested_distros)) {
            return(dist)
        }
    }
    failBuild('No untested distros to try',
              "All past 10 distros were already tested ${latest_distros}")
    return("")
}

List getDistroRange(String[] range, String workerNode, String arch) {
    // Wrapper to allow "..".split() as well as ["foo", "bar"]]
    return(getDistroRange(range.toList(), workerNode, arch))
}

List getDistroRange(List range, String workerNode, String arch) {
    // Find all distros between range[0] and range[1] revision (max 100 versions)
    first = range[0]
    last = range[1]
    common = ''
    for (i = 0; i < Math.min(first.length(), last.length()); i++) {
        if (first[i] != last[i]) {
            break
        }
        common += first[i]
    }
    if (first.contains('n') && last.contains('n')) {
        common += '%n'
    } else if (first.contains('d') && last.contains('d')) {
        common += '%d'
    }
    node(workerNode) {
        distros = getLatestDistros(common + '%', 100, arch).reverse();
        distroRange = [];
        i = 0;
        while (i < distros.size()) {
            if (distros[i] == last) {
                break;
            }
            ++i;
        }
        while (i < distros.size()) {
            distroRange.add(distros[i]);
            if (distros[i++] == first) {
                break;
            }
        }
    }
    return(distroRange)
}

List getDistrosRange(String[] range, String workerNode, String arch) {
    // Wrapper to allow "..".split() as well as ["foo", "bar"]]
    return(getDistrosRange(range.toList(), workerNode, arch))
}

List getDistrosRange(List distrosRaw, String workerNode, String arch) {
    // Process list of distros and replace '..' ranges with individual versions
    println("getDistrosRange ${distrosRaw}")
    List distros = []
    for (distro in distrosRaw) {
        if (distro.contains('..')) {
            distroRange = getDistroRange(distro.split('\\.\\.'), workerNode, arch)
            println("range ${distroRange}")
            distros += distroRange.toList()
        } else {
            println("add ${distro}")
            distros.add(distro)
        }
    }
    return(distros)
}

@NonCPS
List getGoodBuildNumbers(String jobName) {
    // Build is non-serializable object, we have to use NonCPS
    // on the other hand we can not use copyArtifacts inside NonCPS
    // therefore we have to only query for all descriptions and
    // then iterate throught them, because we don't know how many
    // builds we are going to need (copyArtifacts can fail)
    builds = []
    for (build in Jenkins.instance.getJob(jobName).builds) {
        if (build?.description?.startsWith('BAD')) {
            println("skip ${build.description} ${build.number}")
        } else {
            builds.add(build.number)
        }
    }
    return builds
}

String setupScript(output, kernelArgs, bkrLinks, bkrFilter, arch, fioNbdSetup) {
    // Generate additional parts of the setup script
    if (kernelArgs) {
        output += "\ngrubby --args '${kernelArgs}' --update-kernel=\$(grubby --default-kernel)"
    }
    // Ugly way of installing all arch's rpms from a site, allowing a filter
    // this is usually used on koji/brew to allow updating certain packages
    // warning: It does not work when the url rpm is older.
    if (bkrLinks) {
        output += getBkrInstallCmd(bkrLinks, bkrFilter, arch)
    }
    // Install deps and compile custom fio with nbd ioengine
    if (fioNbdSetup) {
        output += fioNbdScript
    }
    output += '\n'
    return output
}

@NonCPS
def triggerRunperf(String jobName, Boolean waitForStart, String distro, String guestDistro,
                   String machine, String arch, String tests, String profiles, String srcBuild,
                   String hostKernelArgs, String hostBkrLinks, String hostBkrLinksFilter,
                   String guestKernelArgs, String guestBkrLinks, String guestBkrLinksFilter,
                   String upstreamQemuCommit, String descriptionPrefix,
                   Boolean pbenchPublish, Boolean fioNbdSetup, String noReferenceBuilds,
                   String cmpModelJob, String cmpModelBuild, String cmpTolerance,
                   String cmpStddevTolerance, String githubPublisherProject, String hostScript,
                   String workerScript) {
    // Trigger a run-perf job setting all of the params according to arguments
    // on waitForStart returns the triggered build.id, otherwise it
    // returns srcBuild value
    parameters = [
        new StringParameterValue('DISTRO', distro),
        new StringParameterValue('GUEST_DISTRO', guestDistro),
        new StringParameterValue('MACHINE', machine),
        new StringParameterValue('ARCH', arch),
        new StringParameterValue('TESTS', tests),
        new StringParameterValue('PROFILES', profiles),
        new StringParameterValue('SRC_BUILD', srcBuild),
        new StringParameterValue('HOST_KERNEL_ARGS', hostKernelArgs),
        new StringParameterValue('HOST_BKR_LINKS', hostBkrLinks),
        new StringParameterValue('HOST_BRK_LINKS_FILTER', hostBkrLinksFilter),
        new StringParameterValue('GUEST_KERNEL_ARGS', guestKernelArgs),
        new StringParameterValue('GUEST_BKR_LINKS', guestBkrLinks),
        new StringParameterValue('GUEST_BKR_LINKS_FILTER', guestBkrLinksFilter),
        new StringParameterValue('UPSTREAM_QEMU_COMMIT', upstreamQemuCommit),
        new StringParameterValue('DESCRIPTION_PREFIX', descriptionPrefix),
        new BooleanParameterValue('PBENCH_PUBLISH', pbenchPublish),
        new BooleanParameterValue('FIO_NBD_SETUP', fioNbdSetup),
        new StringParameterValue('NO_REFERENCE_BUILDS', noReferenceBuilds),
        new StringParameterValue('CMP_MODEL_JOB', cmpModelJob),
        new StringParameterValue('CMP_MODEL_BUILD', cmpModelBuild),
        new StringParameterValue('CMP_TOLERANCE', cmpTolerance),
        new StringParameterValue('CMP_STDDEV_TOLERANCE', cmpStddevTolerance),
        new StringParameterValue('GITHUB_PUBLISHER_PROJECT', githubPublisherProject),
        new TextParameterValue('HOST_SCRIPT', hostScript),
        new TextParameterValue('WORKER_SCRIPT', workerScript)
        ]
    job = Hudson.instance.getJob(jobName)
    queue = job.scheduleBuild2(0, new ParametersAction(parameters))
    if (waitForStart) {
        println('Waiting for build to be scheduled to obtain srcBuild ID')
        build = queue.waitForStart()
        srcBuild = "${build.id}"
    }
    // Explicitly clean build, job and queue, otherwise we get CPS failures
    build = job = queue = null
    return(srcBuild)
}

@NonCPS
void tryOtherDistros(String rawDistro, String arch) {
    // Re-trigger the job with another untested distro if possible
    String strProvisionFail = 'Provisioning failed'
    if (!rawDistro.startsWith('latest-')) {
        // Using strict distro version
        failBuild(strProvisionFail,
                  "Provisioning failed, bailing out as we are using strict distro ${rawDistro}")
    }
    if (rawDistro.startsWith('latest-untested-')) {
        latestDistro = rawDistro
    } else {
        latestDistro = "latest-untested-${rawDistro[7..-1]}"
    }
    triggerRunperf(env.JOB_NAME, false, latestDistro, params.GUEST_DISTRO, params.MACHINE, params.ARCH,
                   params.TESTS, params.PROFILES, params.SRC_BUILD, params.HOST_KERNEL_ARGS,
                   params.HOST_BKR_LINKS, params.HOST_BRK_LINKS_FILTER, params.GUEST_KERNEL_ARGS,
                   params.GUEST_BKR_LINKS, params.GUEST_BKR_LINKS_FILTER, params.UPSTREAM_QEMU_COMMIT,
                   params.DESCRIPTION_PREFIX, params.PBENCH_PUBLISH, params.FIO_NBD_SETUP,
                   params.NO_REFERENCE_BUILDS, params.CMP_MODEL_JOB, params.CMP_MODEL_BUILD,
                   params.CMP_TOLERANCE, params.CMP_STDDEV_TOLERANCE, params.GITHUB_PUBLISHER_PROJECT,
                   params.HOST_SCRIPT, params.WORKER_SCRIPT)
}

@NonCPS
def getBuildEnv(String jobName, String buildName) {
    env = Hudson.instance.getJob(jobName).getBuildByNumber(buildName as int).getEnvironment()
    str_env = "${env}"
    env = null
    return str_env
}

into $YOUR_SHARED_LIB_REPO/vars/runperf.groovy and you should be good to go. Feel free to ping me if anything goes wrong.