How to distribute VU's across different scenarios with k6

And is this possible to mention users percentiles?
For example, if I want 10% users to run one block of code, 30% - another block, and the rest of users - third scenario, all in one test.

2 Likes

You can use modulo operation (%) to do something like that in your case

if (__VU % 10 == 0) {
// 10% of the VUs
} else if (__VU % 10 <= 3) {
// 30% of the VUs
} else {
// the rest of the VUs (60%)
}

Obviously you can write the three different code blocks as different functions to make it more readable on your default function to be just the code above calling the different functions.

Also in order for the above code to work you should have 10+ VUs and they should be divisable by 10 without remainder. Or you can make it based on the iteration in which case just us e __ITER instead of __VU it will have the same problem but usually iterations are a lot more than VUs so the error should be marginal.

3 Likes

I want to propose another option that may be easier to work with by using a switch statement. The below is a simple case of each VU being sent to each individual request a third of the time, but it can obviously be adapted to fit your needs. To keep the script clean, I would recommend writing the different journeys as separate JavaScript files and importing them. My cursory testing in the Load Impact cloud resulted in the following distribution of 6322 requests:

2176 requests to case 1
2043 requests case 2
2103 requests to case 3

Not perfectly even, but should be good enough for most implementations!

export default function() {
  let userDistro = Math.floor(Math.random() * 100);

  switch (true) {
    case (userDistro <= 33):
      http.get("http://test.loadimpact.com?switch=caseone");
      break;
    case (userDistro > 33 && userDistro <= 66):
      http.get("http://test.loadimpact.com?switch=casetwo");
      break;
    case (userDistro > 66 && userDistro < 100):
      http.get("http://test.loadimpact.com?switch=casethree");
      break;
    default:
      http.get("http://test.loadimpact.com?switch=default");
      break;

  sleep(1);


  }

};
3 Likes

Related feature request Helper for conditional percent statements · Issue #239 · grafana/k6 · GitHub

2 Likes

So there are no way to distribute the load on different groups based on a preset percentage? Like the way you distribute user scenarios in a test in the Lua version?

@larzy456 The k6 cloud web app doesn’t have a GUI that allows test distro in that same way. This mostly comes down to k6 has many new concepts that didn’t exist in the Lua product, such as modularization and expressing configurations as code. The current way to handle distributions would be to do it programmatically in some fashion.

That said - do note that PR 1007 will make this easier once merged. It’s the top priority for the k6 dev team currently.

3 Likes

Hello, I have the same need now and I see the PR has been merged but I don’t have a clear idea on how to use the new executors to achieve this.

@mark is there somewhere I can look for inspiration?

Thanks.

Hi @dedsm,
the PR has been merged, but a release is pending(this week).

As for information you can look at the future release notes or the documentation in progress.

In order to use it though you will need either compile from master or use the master docker tag.

Any feedback on either the new executors as a whole or the documentation/release notes linked above are welcome :smiley:

Awesome! I’ll certainly give it a try and provide feedback if needed, thanks!

Just wanted to let you know that k6 v0.27.0 was officially released today and it includes native support for multi-scenario test runs:

@mstoykov I’ve been using it the last couple of days, I’m just missing one thing that maybe is possible so that’s why I ask here, I’m initializing with a list of auth tokens and I was using the __VU and __ITER variables to distribute the tokens between all the vus, I was expecting the __VU variable to be local to the scenario but it seems to be global to all scenarios, and I can’t find a simple solution now to the distribution.

it would be nice (if it’s not there already) to have a __VU-like counter but local to the scenarios.

@dedsm, unfortunately there isn’t an equivalent to __VU that’s local for each scenario yet :disappointed: We plan to add such functionality in the future, though likely not as magic constants, but as helper functions, part of the k6 JS API: Improve execution information in scripts · Issue #1320 · grafana/k6 · GitHub

The underlying cause why __VU is global is that it’s a unique sequential identifier for each initialized VU, and VUs can be reused between non-overlapping scenarios…

Another issue you might want to follow is Data segmentation API framework · Issue #1539 · grafana/k6 · GitHub. If I understand your use case correctly, when we implement this, it should solve your issue even more cleanly than a scenario-local VU number. Instead of you manually partitioning an array of tokens between VUs, it should be possible for you to map one to the other and have each VU automagically get its token, without you having to manually partition anything.

1 Like

Maybe I am really too stupid. How can I achieve any percentage numbers across different scenarios in combination with constant-vus or ramping-vus?
Goal is:
I wanna run as many requests as possible, but they should be seperated over my scenarios in a fixed percentage base way. Of course one cannot guarantee the distribution, but it should be as close as possible. Like jMeters ThroughputController - is there no way to achieve this (or similar) in k6?
1 % scenario A
2 % scenario B
…
35 % scenario XYZ

@grafbumsdi, I am not sure I completely understand your question. But if you have dozens of scenarios, you can write a helper function that generates the final options.scenarios you want, automatically calculating the VUs of each scenario based on the percentage of total VUs you want to have for the whole test.

And, of course, you can still use modulo arithmetic based on the __VU execution context variable, as shown in this answer in the thread. Or use Math.random() to partition iterations, as demonstrated by this answer. Both of these old k6 distribution strategies still can be used, separately or together with scenarios.

1 Like

@ned ok thx. Yeah first approach only works, if you have very few scenarios OR you know beforehand how many total requests you wanna make. But I have > 50 scenarios. how shall one spread this over “total VUs” of lets say 2 VUs and scaling/ramping number of VUs?

Seems we are back to the modulo arithmetic or random (or some other custom implementation) done per VU.

What confused me and what I was desperatly looking for was any REAL native solution to configure anywhere % percentagebased distribution of scenarios, which is obviously (as I learned from your answer, thx :slight_smile: ) not possible at the moment

thx for your help, @ned

If your VUs are fewer than the number of scenarios, then options.scenarios will not work, since you can’t really split a VU between 2 different scenarios. Scenarios can reuse VUs, but they can’t be overlapping in time to do so.

I am unsure exactly why you consider this or this to not be real solutions? Part of the reason k6 uses a real programming language (JS) to script load tests is so that our users can build very custom logic inside of them. And in your case you can take them even further. It should be relatively easy to create a few helper functions that accept a list of your “scenarios” and their respective weights in the init context, and then randomly choose one for every iteration…

1 Like

Here’s a very crude example of how something like this can be implemented in a few lines of JS:

import { sleep } from 'k6';

function weightedSwitch(weightedFuncs) {
    var funcIntervals = new Array(weightedFuncs.length)

    var weightSum = 0;
    for (var i = 0; i < weightedFuncs.length; i++) {
        funcIntervals[i] = {
            start: weightSum,
            end: weightSum + weightedFuncs[i][0],
            func: weightedFuncs[i][1],
        }
        weightSum += weightedFuncs[i][0];
    }

    if (Math.abs(weightSum - 1) > 0.0001) {
        throw new Error('the sum of function weights should be 1 (100%), but is ' + weightSum);
    }

    return function (val) {
        var guess, min = 0, max = funcIntervals.length - 1;;
        while (min <= max) {
            guess = Math.floor((max + min) / 2);

            if (val >= funcIntervals[guess].end) {
                min = guess + 1;
            } else if (val < funcIntervals[guess].start) {
                max = guess - 1;
            } else {
                return funcIntervals[guess].func;
            }
        }
    }
}

export let options = {
    duration: '1m',
    vus: 2,
};

var getFunction = weightedSwitch([
    [0.1, () => "scenario 0 (10%)"],
    [0.1, () => "scenario 1 (10%)"],
    [0.21, () => "scenario 2 (21%)"],
    [0.39, () => "scenario 3 (39%)"],
    [0.199, () => "scenario 4 (19.9%)"],
    [0.001, () => "scenario 5 (0.1%)"],
])


export default function () {
    var rand = Math.random();
    var f = getFunction(rand);
    console.log(`VU ${__VU}, iter ${__ITER}, rand ${rand.toFixed(4)} executed ${f()}`);
    sleep(1);
}

I don’t make any guarantees that I don’t have an off-by-one error in the binary search code or something like that, I haven’t tested this code much, but it seems to work well enough…

When you run it, it should print something like this:

INFO[0000] VU 2, iter 0, rand 0.3661 executed scenario 2 (21%)  source=console
INFO[0000] VU 1, iter 0, rand 0.4241 executed scenario 3 (39%)  source=console
INFO[0001] VU 2, iter 1, rand 0.3712 executed scenario 2 (21%)  source=console
INFO[0001] VU 1, iter 1, rand 0.1931 executed scenario 1 (10%)  source=console
INFO[0002] VU 1, iter 2, rand 0.3077 executed scenario 2 (21%)  source=console
INFO[0002] VU 2, iter 2, rand 0.7144 executed scenario 3 (39%)  source=console
INFO[0003] VU 1, iter 3, rand 0.6757 executed scenario 3 (39%)  source=console
INFO[0003] VU 2, iter 3, rand 0.1091 executed scenario 1 (10%)  source=console
INFO[0004] VU 1, iter 4, rand 0.9282 executed scenario 4 (19.9%)  source=console
INFO[0004] VU 2, iter 4, rand 0.5826 executed scenario 3 (39%)  source=console
INFO[0005] VU 2, iter 5, rand 0.3474 executed scenario 2 (21%)  source=console
INFO[0005] VU 1, iter 5, rand 0.7628 executed scenario 3 (39%)  source=console
INFO[0006] VU 2, iter 6, rand 0.9356 executed scenario 4 (19.9%)  source=console
INFO[0006] VU 1, iter 6, rand 0.0033 executed scenario 0 (10%)  source=console
...
1 Like

thx, yes that’s exactly what I also implemented yesterday in similar way (just did some additional normalization beforehand to avoid sum of percentage being < 1 or > 1).
I really appreciate you spent even more time on this, and thx for approving that this is the one and only possible way :slight_smile:

I know that is very late to reply to this post but I just stumbled on it as a reference from another topic and had to point something out -

if (__VU % 10 ==1) {
// 10% of the VUs
} else if (__VU % 10 == 3) {
// 30% of the VUs
} else {
// the rest of the VUs (60%)
}

__VU % 10 == 3 does not give you 30% of the VUs, it will only give you 10% of the VUs, it is just a different 10% to those given by __VU % 10 == 1.

There are various ways to get 30%, but the simplest to read would be __VU % 10 >= 1 && __VU % 10 <= 3.

1 Like

Hi @martinp, thanks or this, I guess I tried to write this as simple as possible and did not test it. I have now updated it to keep still simple but also correct :).

I also tested it locally, and it’s a lot more obvious it works if you use __ITER and as single VU as it just goes 10%, 30% *3, 60%*6.

Thanks again for mentioning it :bowing_man:

1 Like