diff --git a/build.properties b/build.properties index 63eb4f2f2..dac729755 100644 --- a/build.properties +++ b/build.properties @@ -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 diff --git a/samples/testsuite-showcases/PDFBox.log b/samples/testsuite-showcases/PDFBox.log new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/java/com/xceptance/xlt/mastercontroller/LoadFunctionUtils.java b/src/main/java/com/xceptance/xlt/mastercontroller/LoadFunctionUtils.java index fcd2b8f47..865bf43bd 100644 --- a/src/main/java/com/xceptance/xlt/mastercontroller/LoadFunctionUtils.java +++ b/src/main/java/com/xceptance/xlt/mastercontroller/LoadFunctionUtils.java @@ -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. @@ -431,4 +435,154 @@ public static boolean isValidStartingPoint(final AbsoluteOrRelativeNumbernull) + * @param jitterFunction + * the jitter function (may be null) + * @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 newLoadFunctionPoints = new ArrayList<>(); + final Set 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. + 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); + + final int finalValue = (int) Math.round(newValue); + newLoadFunctionPoints.add(new int[] + { + t, finalValue + }); + } + } + + // Simplify the new load function by removing redundant points + final List 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); + } } diff --git a/src/main/java/com/xceptance/xlt/mastercontroller/TestCaseLoadProfileConfiguration.java b/src/main/java/com/xceptance/xlt/mastercontroller/TestCaseLoadProfileConfiguration.java index d4a72ae50..1e43a3d10 100644 --- a/src/main/java/com/xceptance/xlt/mastercontroller/TestCaseLoadProfileConfiguration.java +++ b/src/main/java/com/xceptance/xlt/mastercontroller/TestCaseLoadProfileConfiguration.java @@ -28,6 +28,8 @@ public class TestCaseLoadProfileConfiguration private int initialDelay; + private int[][] jitter; + private int[][] loadFactor; private int measurementPeriod; @@ -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 + */ + 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 @@ -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; diff --git a/src/main/java/com/xceptance/xlt/mastercontroller/TestLoadProfileConfiguration.java b/src/main/java/com/xceptance/xlt/mastercontroller/TestLoadProfileConfiguration.java index 7036234d4..2b1b68785 100644 --- a/src/main/java/com/xceptance/xlt/mastercontroller/TestLoadProfileConfiguration.java +++ b/src/main/java/com/xceptance/xlt/mastercontroller/TestLoadProfileConfiguration.java @@ -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; @@ -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 */ @@ -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 */ @@ -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); @@ -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); @@ -404,6 +417,7 @@ private void configure(final String[] testCaseNames, final Map(true, -25))); } + // ==================================== + // applyJitter + // ==================================== + + @Test + public void applyJitter() + { + // --- Test Case: Null/Empty Inputs --- + final int[][] loadFunction = + { + { + 0, 100 + }, + { + 10, 200 + } + }; + final int[][] jitterFunction = + { + { + 0, 100 + } + }; // 10% + final Random random = new Random(12345L); + + Assert.assertArrayEquals("Null load function", null, LoadFunctionUtils.applyJitter(null, jitterFunction, random)); + Assert.assertArrayEquals("Empty load function", new int[0][], LoadFunctionUtils.applyJitter(new int[0][], jitterFunction, random)); + Assert.assertArrayEquals("Null jitter function", loadFunction, LoadFunctionUtils.applyJitter(loadFunction, null, random)); + Assert.assertArrayEquals("Empty jitter function", loadFunction, + LoadFunctionUtils.applyJitter(loadFunction, new int[0][], random)); + Assert.assertArrayEquals("Null random", loadFunction, LoadFunctionUtils.applyJitter(loadFunction, jitterFunction, null)); + + // --- Test Case: Zero Jitter --- + final int[][] zeroJitter = + { + { + 0, 0 + } + }; + final int[][] zeroJitterResult = LoadFunctionUtils.applyJitter(loadFunction, zeroJitter, random); + Assert.assertArrayEquals("Jitter of 0 should not change the function", loadFunction, zeroJitterResult); + + // --- Test Case: Predictable Jitter --- + final int[][] jitterFunc = + { + { + 0, 250 + } + }; // 25% + final Random random1 = new Random(54321L); + final int[][] result1 = LoadFunctionUtils.applyJitter(loadFunction, jitterFunc, random1); + + final Random random2 = new Random(54321L); + final int[][] result2 = LoadFunctionUtils.applyJitter(loadFunction, jitterFunc, random2); + Assert.assertArrayEquals("With the same seed, jitter must be predictable", result1, result2); + + // --- Test Case: Supporting Points Unchanged --- + final int[][] multiPointLoadFunction = + { + { + 0, 100 + }, + { + 5, 150 + }, + { + 10, 100 + } + }; + final int[][] highJitterFunc = + { + { + 0, 500 + } + }; // 50% + final Random highJitterRandom = new Random(98765L); + final int[][] highJitterResult = LoadFunctionUtils.applyJitter(multiPointLoadFunction, highJitterFunc, highJitterRandom); + + // Find the points in the result that correspond to the original supporting points + final int p0 = Arrays.stream(highJitterResult).filter(p -> p[0] == 0).findFirst().get()[1]; + final int p5 = Arrays.stream(highJitterResult).filter(p -> p[0] == 5).findFirst().get()[1]; + final int p10 = Arrays.stream(highJitterResult).filter(p -> p[0] == 10).findFirst().get()[1]; + + Assert.assertEquals("Value at supporting point t=0 should be unchanged", 100, p0); + Assert.assertEquals("Value at supporting point t=5 should be unchanged", 150, p5); + Assert.assertEquals("Value at supporting point t=10 should be unchanged", 100, p10); + + // --- Test Case: Value Boundaries --- + final int[][] boundaryLoadFunc = + { + { + 0, 100 + }, + { + 10, 100 + } + }; + final int[][] fullJitterFunc = + { + { + 0, 1000 + } + }; // 100% + final Random boundaryRandom = new Random(42L); + final int[][] boundaryResult = LoadFunctionUtils.applyJitter(boundaryLoadFunc, fullJitterFunc, boundaryRandom); + + boolean hasIntermediatePoints = false; + for (final int[] point : boundaryResult) + { + // check boundaries for intermediate points + if (point[0] > 0 && point[0] < 10) + { + hasIntermediatePoints = true; + final double baseValue = 100; + Assert.assertTrue("Value should not be negative", point[1] >= 0); + Assert.assertTrue("Value should not be more than double the base value", point[1] <= baseValue * 2); + } + } + Assert.assertTrue("Test is not effective without intermediate points", hasIntermediatePoints); + + // --- Test Case: General Correctness --- + // Just make sure it runs and produces a different result + Assert.assertFalse("With jitter, the function should be different", Arrays.deepEquals(loadFunction, result1)); + } + @SuppressWarnings("unused") private static void printLoadFunction(final int[][] function) {