Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.properties
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ resultbrowser.dir.source = resultbrowser/dist
resultbrowser.dir.target = ${classes.dir}/com/xceptance/xlt/engine/resultbrowser/assets

# Linux
timerrecorder.chrome.executable = chromium
timerrecorder.chrome.executable = chromium-browser
# Windows
#timerrecorder.chrome.executable = C:/Program Files (x86)/Google/Chrome/Application/chrome.exe
# macOS
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
*/
package com.xceptance.xlt.mastercontroller;

import com.xceptance.common.util.AbsoluteOrRelativeNumber;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import com.xceptance.common.util.AbsoluteOrRelativeNumber;

/**
* Utility class for parsing and calculating load functions.
Expand Down Expand Up @@ -431,4 +435,154 @@ public static boolean isValidStartingPoint(final AbsoluteOrRelativeNumber<Intege
{
return !time.isRelativeNumber() && time.getValue() == LoadFunctionUtils.START_TIME && !value.isRelativeNumber();
}

/**
* Applies jitter to the given load function.
*
* @param loadFunction
* the load function (may be <code>null</code>)
* @param jitterFunction
* the jitter function (may be <code>null</code>)
* @param random
* the random number generator to use
* @return the jittered load function
*/
public static int[][] applyJitter(final int[][] loadFunction, final int[][] jitterFunction, final Random random)
{
if (loadFunction == null || loadFunction.length == 0 || jitterFunction == null || jitterFunction.length == 0 || random == null)
{
return loadFunction;
}

// A simple jitter function with a value of 0 means no jitter
if (isSimpleLoadFunction(jitterFunction) && jitterFunction[0][1] == 0)
{
return loadFunction;
}

final List<int[]> newLoadFunctionPoints = new ArrayList<>();
final Set<Integer> supportingPoints = new HashSet<>();
for (final int[] point : loadFunction)
{
supportingPoints.add(point[0]);
}

final int duration = loadFunction[loadFunction.length - 1][0];

for (int t = 0; t <= duration; t++)
{
final double baseValue = interpolate(loadFunction, t);

if (supportingPoints.contains(t))
{
newLoadFunctionPoints.add(new int[]
{
t, (int) Math.round(baseValue)
});
}
else
{
final double jitterPermil = interpolate(jitterFunction, t);
final double jitter = jitterPermil / 1000.0;

// nextGaussian() has a mean of 0 and a standard deviation of 1.
// Clamp it to the range [-1, 1] to avoid extreme values and adhere to the hint in the issue.
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment mentions 'the hint in the issue' but this reference is unclear and not helpful for future maintainers. Consider replacing with a more specific explanation of why clamping to [-1, 1] is necessary.

Suggested change
// Clamp it to the range [-1, 1] to avoid extreme values and adhere to the hint in the issue.
// Clamp the value to the range [-1, 1] to prevent rare but extreme outliers from the Gaussian distribution,
// which could otherwise cause unrealistic or excessive deviations in the load function.

Copilot uses AI. Check for mistakes.
final double clampedGaussian = Math.max(-1.0, Math.min(1.0, random.nextGaussian()));

final double deviation = jitter * clampedGaussian;
double newValue = baseValue * (1 + deviation);

// Clamp the new value to be between 0 and 2 * baseValue
newValue = Math.max(0, newValue);
newValue = Math.min(newValue, baseValue * 2);
Comment on lines +495 to +497
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rationale for clamping the upper bound to 2 * baseValue is not documented. This seems like an arbitrary constraint that should be explained or made configurable.

Copilot uses AI. Check for mistakes.

final int finalValue = (int) Math.round(newValue);
newLoadFunctionPoints.add(new int[]
{
t, finalValue
});
}
}

// Simplify the new load function by removing redundant points
final List<int[]> finalFunction = new ArrayList<>();
if (!newLoadFunctionPoints.isEmpty())
{
finalFunction.add(newLoadFunctionPoints.get(0));
for (int i = 1; i < newLoadFunctionPoints.size(); i++)
{
final int[] currentPoint = newLoadFunctionPoints.get(i);
final int[] lastAddedPoint = finalFunction.get(finalFunction.size() - 1);

// Keep the point if its value is different from the last added point's value.
if (currentPoint[1] != lastAddedPoint[1])
{
finalFunction.add(currentPoint);
}
// Also keep all supporting points regardless of their value.
else if (supportingPoints.contains(currentPoint[0]))
{
if (currentPoint[0] != lastAddedPoint[0])
{
finalFunction.add(currentPoint);
}
}
}
}

return finalFunction.toArray(new int[0][]);
}

/**
* Performs linear interpolation on a given load function for a specific time.
*
* @param function
* the load function
* @param time
* the time for which to interpolate the value
* @return the interpolated value at the given time
*/
private static double interpolate(final int[][] function, final int time)
{
if (time <= function[0][0])
{
return function[0][1];
}
if (time >= function[function.length - 1][0])
{
return function[function.length - 1][1];
}

int[] p1 = null;
int[] p2 = null;

for (int i = 0; i < function.length - 1; i++)
{
if (function[i][0] <= time && time < function[i + 1][0])
{
p1 = function[i];
p2 = function[i + 1];
break;
}
}

if (p1 == null)
{
// Should not happen given the checks above
return function[function.length - 1][1];
}

// Linear interpolation
final double t1 = p1[0];
final double v1 = p1[1];
final double t2 = p2[0];
final double v2 = p2[1];

if (t1 == t2)
{
return v1;
}

return v1 + (time - t1) * (v2 - v1) / (t2 - t1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class TestCaseLoadProfileConfiguration

private int initialDelay;

private int[][] jitter;

private int[][] loadFactor;

private int measurementPeriod;
Expand Down Expand Up @@ -61,6 +63,17 @@ public int getInitialDelay()
return initialDelay;
}

/**
* Returns the jitter as two-dimensional array of non-negative integral values whereas the 1st dimension specifies
* the time offset in seconds and the 2nd dimension specifies the jitter in per mil at the given time offset.
*
* @return the jitter
Comment on lines +67 to +70
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The javadoc states 'non-negative integral values' but doesn't mention that null is also a valid return value, which should be documented for API clarity.

Suggested change
* Returns the jitter as two-dimensional array of non-negative integral values whereas the 1st dimension specifies
* the time offset in seconds and the 2nd dimension specifies the jitter in per mil at the given time offset.
*
* @return the jitter
* Returns the jitter as a two-dimensional array of non-negative integral values whereas the 1st dimension specifies
* the time offset in seconds and the 2nd dimension specifies the jitter in per mil at the given time offset.
* <p>
* May return {@code null} if no jitter is configured.
*
* @return the jitter, or {@code null} if not configured

Copilot uses AI. Check for mistakes.
*/
public int[][] getJitter()
{
return jitter;
}

/**
* Returns the load factor as two-dimensional array of non-negative integral values whereas the 1st dimension
* specifies the time offset in seconds and the 2nd dimension specifies the load factor in per mil at the given time
Expand Down Expand Up @@ -128,6 +141,11 @@ public void setInitialDelay(final int initialDelay)
this.initialDelay = Math.max(initialDelay, 0);
}

public void setJitter(final int[][] jitter)
{
this.jitter = jitter;
}

public void setLoadFactor(final int[][] loadFactor)
{
this.loadFactor = loadFactor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;

Expand Down Expand Up @@ -64,6 +65,11 @@ public class TestLoadProfileConfiguration extends AbstractConfiguration
*/
private static final String PROP_SUFFIX_ITERATIONS = ".iterations";

/**
* property suffix to set the jitter of a test
*/
private static final String PROP_SUFFIX_JITTER = ".jitter";

/**
* property suffix to set the load factor of a test
*/
Expand Down Expand Up @@ -124,6 +130,11 @@ public class TestLoadProfileConfiguration extends AbstractConfiguration

private static final String PROP_SUFFIX_ISCLIENTPERFTEST = ".clientPerformanceTest";

/**
* property name to get the initial value for the random number generator
*/
private static final String PROP_RANDOM_INIT_VALUE = "com.xceptance.xlt.random.initValue";

/**
* property name to get the base value for the action think time
*/
Expand Down Expand Up @@ -348,6 +359,7 @@ private DefaultTestCaseLoadProfileConfiguration getDefaultTestCaseLoadProfileCon
final int defaultRampUpStepSize = getIntProperty(PROP_LOAD_TEST_DEFAULTS + PROP_SUFFIX_RAMP_UP_STEP_SIZE, -1);
final int defaultRampUpInitialValue = getIntProperty(PROP_LOAD_TEST_DEFAULTS + PROP_SUFFIX_RAMP_UP_INITIAL_VALUE, -1);
final int[][] defaultLoadFactor = getDoubleLoadFunction(PROP_LOAD_TEST_DEFAULTS + PROP_SUFFIX_LOAD_FACTOR, null);
final int[][] defaultJitter = getDoubleLoadFunction(PROP_LOAD_TEST_DEFAULTS + PROP_SUFFIX_JITTER, null);
final int[][] defaultUsers = getLoadFunction(PROP_LOAD_TEST_DEFAULTS + PROP_SUFFIX_USERS, null);
final int[][] defaultArrivalRate = getLoadFunction(PROP_LOAD_TEST_DEFAULTS + PROP_SUFFIX_ARRIVAL_RATE, null);
final int defaultActionThinkTime = getIntProperty(PROP_ACTION_THINK_TIME, 0);
Expand All @@ -365,6 +377,7 @@ private DefaultTestCaseLoadProfileConfiguration getDefaultTestCaseLoadProfileCon
defaultConfig.setRampUpStepSize(defaultRampUpStepSize);
defaultConfig.setRampUpInitialValue(defaultRampUpInitialValue);
defaultConfig.setLoadFactor(defaultLoadFactor);
defaultConfig.setJitter(defaultJitter);
defaultConfig.setNumberOfUsers(defaultUsers);
defaultConfig.setArrivalRate(defaultArrivalRate);
defaultConfig.setActionThinkTime(defaultActionThinkTime);
Expand Down Expand Up @@ -404,6 +417,7 @@ private void configure(final String[] testCaseNames, final Map<String, TestCaseL
final int rampUpInitialValue = getIntProperty(propertyName + PROP_SUFFIX_RAMP_UP_INITIAL_VALUE,
defaultConfiguration.getRampUpInitialValue());
final int[][] loadFactor = getDoubleLoadFunction(propertyName + PROP_SUFFIX_LOAD_FACTOR, defaultConfiguration.getLoadFactor());
final int[][] jitter = getDoubleLoadFunction(propertyName + PROP_SUFFIX_JITTER, defaultConfiguration.getJitter());
int[][] users = getLoadFunction(propertyName + PROP_SUFFIX_USERS, defaultConfiguration.getNumberOfUsers());
int[][] arrivalRate = getLoadFunction(propertyName + PROP_SUFFIX_ARRIVAL_RATE, defaultConfiguration.getArrivalRate());
final boolean isCPTest = getBooleanProperty(propertyName + PROP_SUFFIX_ISCLIENTPERFTEST, false);
Expand Down Expand Up @@ -466,6 +480,16 @@ private void configure(final String[] testCaseNames, final Map<String, TestCaseL
arrivalRate = LoadFunctionUtils.scaleLoadFunction(arrivalRate, loadFactor);
users = LoadFunctionUtils.scaleLoadFunction(users, loadFactor);

// apply jitter function
if (jitter != null)
{
final long seed = getLongProperty(PROP_RANDOM_INIT_VALUE, System.currentTimeMillis()) + testCaseName.hashCode();
final Random random = new Random(seed);

arrivalRate = LoadFunctionUtils.applyJitter(arrivalRate, jitter, random);
users = LoadFunctionUtils.applyJitter(users, jitter, random);
}

// set the load function for the test report
int[][] complexLoadFunction = null;
if (arrivalRate != null && LoadFunctionUtils.isComplexLoadFunction(arrivalRate))
Expand Down Expand Up @@ -517,6 +541,7 @@ else if (LoadFunctionUtils.isComplexLoadFunction(users))
config.setComplexLoadFunction(complexLoadFunction);
config.setRampUpPeriod(rampUpPeriod);
config.setLoadFactor(loadFactor);
config.setJitter(jitter);
config.setCPTest(isCPTest);
config.setActionThinkTime(actionThinkTime);
config.setActionThinkTimeDeviation(actionThinkTimeDeviation);
Expand Down
Loading