You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by vl...@apache.org on 2020/01/27 22:13:25 UTC

[jmeter] branch master updated: Bug 64091 - Precise Throughput Timer might produce less samples when low test duration is used (#553)

This is an automated email from the ASF dual-hosted git repository.

vladimirsitnikov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jmeter.git


The following commit(s) were added to refs/heads/master by this push:
     new 03693f7  Bug 64091 - Precise Throughput Timer might produce less samples when low test duration is used (#553)
03693f7 is described below

commit 03693f7c66707ddf84346430a979660abe4c58e3
Author: Vladimir Sitnikov <si...@gmail.com>
AuthorDate: Tue Jan 28 01:13:19 2020 +0300

    Bug 64091 - Precise Throughput Timer might produce less samples when low test duration is used (#553)
    
    This reworks the generation algorithm.
    For now, the timer generates random numbers in 0..testDuration interval
    and sorts them.
    
    Note: the properties of the resulting sequence are still the same,
    however, the algorithm is NlogN now.
---
 .../ConstantPoissonProcessGenerator.java           | 109 +++++--------
 .../poissonarrivals/PreciseThroughputTimer.java    |  11 +-
 .../PreciseThroughputTimerBeanInfo.java            |  10 +-
 .../PreciseThroughputTimerTest.java                | 169 +++++++++++++++++----
 xdocs/changes.xml                                  |   2 +
 .../timers/precise_throughput_timer.png            | Bin 36007 -> 49413 bytes
 xdocs/usermanual/component_reference.xml           |  23 +--
 7 files changed, 206 insertions(+), 118 deletions(-)

diff --git a/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/ConstantPoissonProcessGenerator.java b/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/ConstantPoissonProcessGenerator.java
index 7a3601f..7205e79 100644
--- a/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/ConstantPoissonProcessGenerator.java
+++ b/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/ConstantPoissonProcessGenerator.java
@@ -18,6 +18,7 @@
 package org.apache.jmeter.timers.poissonarrivals;
 
 import java.nio.DoubleBuffer;
+import java.util.Arrays;
 import java.util.Random;
 
 import org.apache.jmeter.testelement.AbstractTestElement;
@@ -33,37 +34,34 @@ public class ConstantPoissonProcessGenerator implements EventProducer {
 
     private static final double PRECISION = 0.00001;
 
-    private Random rnd = new Random();
-    private ThroughputProvider throughputProvider;
-    private int batchSize;
-    private int batchThreadDelay;
-    private DurationProvider durationProvider;
+    private final Random rnd = new Random();
+    private final ThroughputProvider throughputProvider;
+    private final int batchSize;
+    private final int batchThreadDelay;
+    private final DurationProvider durationProvider;
+    private final boolean logFirstSamples;
+
+    private int batchItemIndex;
     private double lastThroughput;
-    private int exactLimit;
-    private double allowedThroughputSurplus;
+    private double lastThroughputDurationFinish;
     private DoubleBuffer events;
-    private double lastEvent;
-    private final boolean logFirstSamples;
 
     public ConstantPoissonProcessGenerator(
             ThroughputProvider throughput, int batchSize, int batchThreadDelay,
-            DurationProvider duration, int exactLimit, double allowedThroughputSurplus,
+            DurationProvider duration,
             Long seed, boolean logFirstSamples) {
         this.throughputProvider = throughput;
         this.batchSize = batchSize;
         this.batchThreadDelay = batchThreadDelay;
         this.durationProvider = duration;
-        this.exactLimit = exactLimit;
-        this.allowedThroughputSurplus = allowedThroughputSurplus;
         this.logFirstSamples = logFirstSamples;
         if (seed != null && seed.intValue() != 0) {
             rnd.setSeed(seed);
         }
-        ensureCapacity();
+        ensureCapacity(0);
     }
 
-    private void ensureCapacity() {
-        int size = (int) Math.round((throughputProvider.getThroughput() * durationProvider.getDuration() + 1) * 3);
+    private void ensureCapacity(int size) {
         if (events != null && events.capacity() >= size) {
             return;
         }
@@ -76,47 +74,26 @@ public class ConstantPoissonProcessGenerator implements EventProducer {
         if (batchSize > 1) {
             throughput /= batchSize;
         }
+        batchItemIndex = 0;
         long duration = this.durationProvider.getDuration();
-        ensureCapacity();
         int samples = (int) Math.ceil(throughput * duration);
-        double time;
-        int i = 0;
+        ensureCapacity(samples);
         long t = System.currentTimeMillis();
-        int loops = 0;
-        double currentAllowedThroughputSurplus = samples < exactLimit ? 0.0d : this.allowedThroughputSurplus / 100;
-        do {
-            time = 0;
-            events.clear();
-            if (throughput < 1e-5) {
-                log.info("Throughput should exceed zero");
-                break;
-            }
-            if (duration < 5) {
-                log.info("Duration should exceed 5 seconds");
-                break;
-            }
-            i = 0;
-            while (time < duration) {
-                double u = rnd.nextDouble();
-                // https://en.wikipedia.org/wiki/Exponential_distribution#Generating_exponential_variates
-                double delay = -Math.log(1 - u) / throughput;
-                time += delay;
-                events.put(time + lastEvent);
-                i++;
-            }
-            loops++;
-        } while (System.currentTimeMillis() - t < 5000 &&
-                (i < samples + 1 // not enough samples
-                        || (i - 1 - samples) * 1.0f / samples > currentAllowedThroughputSurplus));
+        events.clear();
+        for (int i = 0; i < samples; i++) {
+            events.put(lastThroughputDurationFinish + rnd.nextDouble() * duration);
+        }
+        Arrays.sort(events.array(), events.arrayOffset(), events.position());
         t = System.currentTimeMillis() - t;
         if (t > 1000) {
             log.warn("Spent {} ms while generating sequence of delays for {} samples, {} throughput, {} duration",
                     t, samples, throughput, duration);
         }
+        lastThroughputDurationFinish += duration;
         if (logFirstSamples) {
             if (log.isDebugEnabled()) {
-                log.debug("Generated {} events ({} required, rate {}) in {} ms, restart was issued {} times",
-                        events.position(), samples, throughput, t, loops);
+                log.debug("Generated {} events ({} required, rate {}) in {} ms",
+                        events.position(), samples, throughput, t);
             }
             if (log.isInfoEnabled()) {
                 StringBuilder sb = new StringBuilder();
@@ -126,13 +103,10 @@ public class ConstantPoissonProcessGenerator implements EventProducer {
                 }
                 sb.append(" ").append(samples).append(" required, rate ").append(throughput)
                         .append(", duration ").append(duration)
-                        .append(", exact lim ").append(exactLimit)
-                        .append(", i").append(i)
-                        .append(") in ").append(t)
-                        .append(" ms, restart was issued ").append(loops).append(" times. ");
-                sb.append("First 15 events will be fired at: ");
+                        .append(") in ").append(t).append(" ms");
+                sb.append(". First 15 events will be fired at: ");
                 double prev = 0;
-                for (i = 0; i < events.position() && i < 15; i++) {
+                for (int i = 0; i < events.position() && i < 15; i++) {
                     if (i > 0) {
                         sb.append(", ");
                     }
@@ -145,29 +119,28 @@ public class ConstantPoissonProcessGenerator implements EventProducer {
             }
         }
         events.flip();
-        if (batchSize > 1) {
-            // If required to generate "pairs" of events, then just duplicate events in the buffer
-            // TODO: for large batchSizes it makes sense to use counting instead
-            DoubleBuffer tmpBuffer = DoubleBuffer.allocate(batchSize * events.remaining());
-            while (events.hasRemaining()) {
-                double curTime = events.get();
-                for (int j = 0; j < batchSize; j++) {
-                    tmpBuffer.put(curTime + j * batchThreadDelay);
-                }
-            }
-            tmpBuffer.flip();
-            events = tmpBuffer;
-        }
     }
 
     @Override
     public double next() {
-        if (!events.hasRemaining()
+        if ((batchItemIndex == 0 && !events.hasRemaining())
                 || !valuesAreEqualWithPrecision(throughputProvider.getThroughput(),lastThroughput)) {
             generateNext();
         }
-        lastEvent = events.get();
-        return lastEvent;
+        if (batchSize == 1) {
+            return events.get();
+        }
+        batchItemIndex++;
+        if (batchItemIndex == 1) {
+            // The first item advances the position
+            return events.get();
+        }
+        if (batchItemIndex == batchSize) {
+            batchItemIndex = 0;
+        }
+        // All the other items in the batch refer to the previous position
+        // since #position() points to the next item to be returned
+        return events.get(events.position() - 1);
     }
 
     private boolean valuesAreEqualWithPrecision(double throughput, double lastThroughput) {
diff --git a/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimer.java b/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimer.java
index 37a42d2..df38ae0 100644
--- a/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimer.java
+++ b/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimer.java
@@ -28,6 +28,7 @@ import org.apache.jmeter.testelement.TestStateListener;
 import org.apache.jmeter.threads.AbstractThreadGroup;
 import org.apache.jmeter.timers.Timer;
 import org.apache.jorphan.util.JMeterStopThreadException;
+import org.apiguardian.api.API;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -134,7 +135,7 @@ public class PreciseThroughputTimer extends AbstractTestElement implements Clone
         return
                 groupEvents.computeIfAbsent(tg, x -> new ConstantPoissonProcessGenerator(
                         () -> PreciseThroughputTimer.this.getThroughput() / throughputPeriod,
-                        batchSize, batchThreadDelay, this, exactLimit, allowedThroughputSurplus, seed, true));
+                        batchSize, batchThreadDelay, this, seed, true));
     }
 
     /**
@@ -174,18 +175,26 @@ public class PreciseThroughputTimer extends AbstractTestElement implements Clone
         this.duration = duration;
     }
 
+    @Deprecated
+    @API(status = API.Status.DEPRECATED, since = "5.3.0")
     public int getExactLimit() {
         return exactLimit;
     }
 
+    @Deprecated
+    @API(status = API.Status.DEPRECATED, since = "5.3.0")
     public void setExactLimit(int exactLimit) {
         this.exactLimit = exactLimit;
     }
 
+    @Deprecated
+    @API(status = API.Status.DEPRECATED, since = "5.3.0")
     public double getAllowedThroughputSurplus() {
         return allowedThroughputSurplus;
     }
 
+    @Deprecated
+    @API(status = API.Status.DEPRECATED, since = "5.3.0")
     public void setAllowedThroughputSurplus(double allowedThroughputSurplus) {
         this.allowedThroughputSurplus = allowedThroughputSurplus;
     }
diff --git a/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimerBeanInfo.java b/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimerBeanInfo.java
index db83eb2..46b7ddf 100644
--- a/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimerBeanInfo.java
+++ b/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimerBeanInfo.java
@@ -66,21 +66,15 @@ public class PreciseThroughputTimerBeanInfo extends BeanInfoSupport {
         p.setValue(DEFAULT, 0);
 
 
-        createPropertyGroup(
-                "accuracy", //$NON-NLS-1$
-                new String[]{
-                        "exactLimit", //$NON-NLS-1$
-                        "allowedThroughputSurplus"    //$NON-NLS-1$
-                }
-        );
-
         p = property("exactLimit"); //$NON-NLS-1$
         p.setValue(NOT_UNDEFINED, Boolean.TRUE);
         p.setValue(DEFAULT, 10000);
+        p.setHidden(true);
 
         p = property("allowedThroughputSurplus"); //$NON-NLS-1$
         p.setValue(NOT_UNDEFINED, Boolean.TRUE);
         p.setValue(DEFAULT, 1.0d);
+        p.setHidden(true);
 
         createPropertyGroup(
                 "repeatability", //$NON-NLS-1$
diff --git a/src/components/src/test/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimerTest.java b/src/components/src/test/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimerTest.java
index df54b65..ed5e973 100644
--- a/src/components/src/test/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimerTest.java
+++ b/src/components/src/test/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimerTest.java
@@ -18,8 +18,10 @@
 package org.apache.jmeter.timers.poissonarrivals;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Random;
 
 import org.junit.jupiter.api.Test;
@@ -31,51 +33,156 @@ public class PreciseThroughputTimerTest {
 
     @Test
     public void testTimer1() throws Exception {
-        ConstantPoissonProcessGenerator gen = getConstantPoissonProcessGenerator(2, 5, 42L);
+        // The case is to produce 2 samples per second, and we specify "test duration to be 5 seconds"
+        // It means every 5 seconds the sampler must produce 2*5=10 samples
+        // However, we generate 31 samples (to ensure generator overflows properly)
+        final int throughput = 2;
+        final int duration = 5;
+        final long seed = 42L;
+        final int batchSize = 1;
+        ConstantPoissonProcessGenerator gen = getConstantPoissonProcessGenerator(throughput, duration, seed, batchSize);
         gen.generateNext();
-        assertEquals(0.6501751901910952, gen.next(), 0.01);
-        assertEquals(1.2249545461599474, gen.next(), 0.01);
-        assertEquals(1.409559315928937, gen.next(), 0.01);
-        assertEquals(1.5717866281130652, gen.next(), 0.01);
-        assertEquals(2.1194190047658874, gen.next(), 0.01);
-        assertEquals(3.2878637366551384, gen.next(), 0.01);
-        assertEquals(3.517916456559849, gen.next(), 0.01);
-        assertEquals(3.679224444929692, gen.next(), 0.01);
-        assertEquals(3.9907119513763165, gen.next(), 0.01);
-        assertEquals(4.754414649148714, gen.next(), 0.01);
-        // ^^ 10 samples for 5 seconds
-        assertEquals(6.013095167372755, gen.next(), 0.01);
+        double[] expected = new double[] {
+                1.3787403472085118, 1.3853924503706834,
+                1.5435972766632988, 1.8439145670565282,
+                2.3182678790457665, 3.327744758972868,
+                3.416117358799227, 3.6378184001643405,
+                3.914508893950179, 4.516861323360891,
+                // ^^ 10 samples for 5 seconds
+                5.861089688439262, 5.886892389546892,
+                6.04883784433166, 6.932834371796743,
+                7.182454872116432, 7.937136908931478,
+                7.9717495544484205, 8.749530906277236,
+                9.129829359439105, 9.596638914343584,
+                // ^^ 10 samples for the next 5 seconds
+                10.157091194132905, 11.789599597385642,
+                12.088437733764593, 12.855201742074335,
+                12.900124422510304, 13.567031289116144,
+                13.7564020338373, 13.762549742953254,
+                14.088984654178198, 14.870178407479408,
+                // ^^ 10 samples for the next 5 seconds
+                16.45828248705902
+                // ^^ it should exceed 15
+        };
+        double[] actual = new double[expected.length];
+        boolean ok = true;
+        for (int i = 0; i < actual.length; i++) {
+            actual[i] = gen.next();
+            ok = ok && Math.abs(actual[i] - expected[i]) < 0.01;
+        }
+
+        if (!ok) {
+            assertEquals(
+                    "Schedule does not match expectation, " +
+                            "throughput=" + throughput + ", duration=" + duration +
+                            "seed=" + seed + ", batchSize=" + batchSize,
+                    Arrays.toString(expected), Arrays.toString(actual)
+            );
+        }
     }
 
     @Test
     public void testExactNumberOfSamples() throws Exception {
         Random rnd = new Random();
-        long seed = rnd.nextLong();
-        // Log seed, so the test can be reproduced in case of failure
-        LOG.info("testExactNumberOfSamples is using seed " + seed);
-        rnd.setSeed(seed);
-
-        int testDuration = 5;
-        for (int i = 0; i < 1000; i++) {
-            ConstantPoissonProcessGenerator gen =
-                    getConstantPoissonProcessGenerator(2, testDuration, rnd.nextLong());
-            gen.generateNext();
-            for (int j = 0; j < 10; j++) {
-                double next = gen.next();
-                assertTrue("Delay #" + j + " (0-based) exceeds " + testDuration + " seconds", next < 5.0);
+        for (int i = 0; i < 100; i++) {
+            long seed = rnd.nextLong();
+            final int testDuration = rnd.nextInt(100) + 5;
+            final int throughput = rnd.nextInt(40) + 1;
+            final int throughputInterval = rnd.nextInt(100) + 1;
+            verifyExactThroughput(seed, testDuration, throughput, throughputInterval);
+        }
+    }
+
+    @Test
+    public void testSingleExactNumberOfSamples() throws Exception {
+        long seed = 6217980321110818258L;
+        verifyExactThroughput(seed, 60, 5, 60);
+    }
+
+    @Test
+    public void testSingleExactNumberOfSamples6812190053835844998() throws Exception {
+        long seed = 6812190053835844998L;
+        verifyExactThroughput(seed, 60, 5, 60);
+    }
+
+    private void verifyExactThroughput(
+            long seed, int testDuration, int throughput, int throughputInterval) {
+        for (int batchSize = 1; batchSize < 3; batchSize++) {
+            verifyExactThroughput(seed, testDuration, throughput, throughputInterval, batchSize);
+        }
+    }
+
+    @Test
+    public void repro2110188512211996814L() {
+        verifyExactThroughput(2110188512211996814L, 27, 1, 85, 1);
+    }
+
+    @Test
+    public void reproduer4389853422207095555() {
+        verifyExactThroughput(/*seed=*/ 4389853422207095555L, /*testDuration=*/ 21, /*throughput=*/ 26, /*throughputInterval=*/ 79, /*batchSize=*/ 1);
+    }
+
+    private void verifyExactThroughput(
+            long seed, int testDuration, int throughput, int throughputInterval, int batchSize) {
+        // 5 per second, and we specify the test duration as 1 second
+        // The generator would prepare the input data for 1 second (~5 samples)
+        // However, then we continue sampling, and the generator should still produce
+        // exactly 5 items for each new second
+        ConstantPoissonProcessGenerator gen =
+                getConstantPoissonProcessGenerator(
+                        throughput * batchSize * 1.0 / throughputInterval, testDuration, seed, batchSize);
+        int samplesPerTest = (int) Math.ceil(throughput * 1.0 / throughputInterval * testDuration) * batchSize;
+        try {
+            ArrayList<Double> delays = new ArrayList<>();
+            // The test will last 100 times longer than expected, so generator would have to re-generate
+            // values 100 times or so
+            double prev = 0;
+            for (int time = 0; time < 100; time++) {
+                for (int i = 0; i < samplesPerTest; i++) {
+                    double next = gen.next();
+                    if (prev > next) {
+                        fail(
+                                "Schedule should be monotonic, so each new event comes later. " +
+                                        "prev: " + prev + ", next: " + next +
+                                        ". Full schedule so far: " + delays
+                        );
+                    }
+                    prev = next;
+                    delays.add(next);
+
+                    if (time * testDuration <= next && next < (time + 1) * testDuration) {
+                        // OK
+                        continue;
+                    }
+                    fail(
+                            "Throughput violation at second #" + time + ". Event #" + delays.size() +
+                                    " is scheduled at " + next + ", however it should be " +
+                                    " between " + time * testDuration + " and " + (time + 1) * testDuration +
+                                    ". Full schedule so far: " + delays
+                    );
+
+                }
             }
+        } catch (Throwable t) {
+            // This adds reproducer right into the stacktrace
+            final String seedHex = Long.toUnsignedString(seed);
+            t.addSuppressed(new Throwable(
+                    "@Test public void reproduer" + seedHex + "() {" +
+                    "verifyExactThroughput(/*seed=*/ " + seed + "L, /*testDuration=*/ " + testDuration +
+                            ", /*throughput=*/ " + throughput + ", /*throughputInterval=*/ " + throughputInterval +
+                            ", /*batchSize=*/ " + batchSize + "); }")
+            );
+            throw t;
         }
     }
 
     protected ConstantPoissonProcessGenerator getConstantPoissonProcessGenerator(
-            final double throughput, final int duration, long seed) {
+            final double throughput, final int duration, long seed, int batchSize) {
         return new ConstantPoissonProcessGenerator(
                 () -> throughput, // samples per second
-                1,
+                batchSize,
                 0,
                 () -> duration, // "expected" test duration: 3 seconds
-                10000,
-                0.1,
                 seed, // Seed
                 false
         );
diff --git a/xdocs/changes.xml b/xdocs/changes.xml
index da246c0..23514ee 100644
--- a/xdocs/changes.xml
+++ b/xdocs/changes.xml
@@ -99,6 +99,7 @@ to view the last release notes of version 5.2.1.
 
 <h3>Timers, Assertions, Config, Pre- &amp; Post-Processors</h3>
 <ul>
+    <li><bug>64091</bug>Precise Throughput Timer schedule generation is improved significantly (e.g. 2 seconds for 10M samples)</li>
 </ul>
 
 <h3>Functions</h3>
@@ -147,6 +148,7 @@ to view the last release notes of version 5.2.1.
 
 <h3>Timers, Assertions, Config, Pre- &amp; Post-Processors</h3>
 <ul>
+    <li><bug>64091</bug>Precise Throughput Timer might produce less samples when low test duration is used</li>
 </ul>
 
 <h3>Functions</h3>
diff --git a/xdocs/images/screenshots/timers/precise_throughput_timer.png b/xdocs/images/screenshots/timers/precise_throughput_timer.png
index e6f6d22..142ef2a 100644
Binary files a/xdocs/images/screenshots/timers/precise_throughput_timer.png and b/xdocs/images/screenshots/timers/precise_throughput_timer.png differ
diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml
index d8a1559..253cb7f 100644
--- a/xdocs/usermanual/component_reference.xml
+++ b/xdocs/usermanual/component_reference.xml
@@ -5095,7 +5095,7 @@ Note that the throughput value should not be changed too often during a test
 </properties>
 </component>
 
-<component name="Precise Throughput Timer" index="&sect-num;.6.5" width="523" height="318" screenshot="timers/precise_throughput_timer.png">
+<component name="Precise Throughput Timer" index="&sect-num;.6.5" width="573" height="407" screenshot="timers/precise_throughput_timer.png">
 <description><p>This timer introduces variable pauses, calculated to keep the total throughput (e.g. in terms of samples per minute) as close as possible to a give figure. Of course the throughput will be lower if the server is not capable of handling it, or if other timers, or if there's not enough threads, or time-consuming test elements prevent it.</p>
 <p>Although the Timer is called Precise Throughput Timer, it does not aim to produce precisely the same number of samples over one-second intervals during the test.</p>
 <p>The timer works best for rates under 36000 requests/hour, however your mileage might vary (see monitoring section below if your goals are
@@ -5166,13 +5166,18 @@ the required load.</p>
 <p>Note: when using multiple thread groups with same throughput rates and same non-zero seed it might result in unwanted firing the samples at the same time.</p>
 
 <h4>Testing high rates and/or long test durations</h4>
-<p>When the number of samples is high (e.g. it exceeds 10'000), schedule generation might take noticeable time (e.g. seconds) as <code>Precise Throughput Timer</code> tries
-    to produce the exact number of samples. There's memory consumption as well, however it should not matter much as every item in the schedule consumes 8 bytes.
-    In order to reduce schedule generation overhead, <code>Precise Throughput Timer</code> allows some slack when generating long schedules. It is controlled by
-    <code>Accuracy of generated delays</code> properties. By default, inexact schedules are allowed when number of samples exceeds 10'000.</p>
-<p>If you want to perform 2-week long test with 5'000 per hour rate, you do not need to create a schedule for that 2 weeks.
-    You can set <code>Test duration (seconds)</code> property of the timer to 1 hour. The timer would create a schedule of 5'000 samples for an hour, and when the schedule is exhausted, the timer would generate
+<p><code>Precise Throughput Timer</code> generates the schedule and keeps it in memory. In most cases it should not be a problem,
+    however, remember that you might want to keep the schedule shorter than 1'000'000 samples.
+    It takes ~200ms to generate a schedule for 1'000'000 samples, and the schedule consumes 8 megabytes in the heap.
+    Schedule for 10 million entries takes 1-2 second to build and it consumes 80 megabytes in the heap.
+</p>
+<p>For instance, if you want to perform 2-week long test with 5'000 per hour rate, then you probably want to have exactly 5'000 samples
+    for each hour. You can set <code>Test duration (seconds)</code> property of the timer of the timer to 1 hour.
+    Then the timer would create a schedule of 5'000 samples for an hour, and when the schedule is exhausted, the timer would generate
     a schedule for the next hour.</p>
+<p>At the same time, you can set <code>Test duration (seconds)</code> to 2 weeks, and the timer would generate a schedule with
+    <code>168'000 samples = 2 weeks * 5'000 samples/hour = 2*7*24*500</code>. The schedule would take ~30ms to generate, and it would
+    consume a little more than 1 megabyte.</p>
 
 <h4>Bursty load</h4>
 <p>There might be a case when all the samples should come in pairs, triples, etc. Certain cases might be solved via <complink name="Synchronizing Timer"/>, however
@@ -5189,7 +5194,7 @@ the required load.</p>
 <h4>Monitoring</h4>
 <p>As next schedule is generated, <code>Precise Throughput Timer</code> logs a message to <code>jmeter.log</code>:
     <code>2018-01-04 17:34:03,635 INFO o.a.j.t.ConstantPoissonProcessGenerator: Generated 21 timings (... 20 required, rate 1.0, duration 20, exact lim 20000,
-    i21) in 0 ms, restart was issued 3 times. First 15 events will be fired at: 1.1869653574244292 (+1.1869653574244292), 1.4691340403043207 (+0.2821686828798915),
+    i21) in 0 ms. First 15 events will be fired at: 1.1869653574244292 (+1.1869653574244292), 1.4691340403043207 (+0.2821686828798915),
     3.638151706179226 (+2.169017665874905), 3.836357090410566 (+0.19820538423134026), 4.709330071408575 (+0.8729729809980085), 5.61330076999953 (+0.903970698590955),
         ...</code>
 This shows that schedule generation took 0ms, and it shows absolute timestamps in seconds. In the case above, the rate was set to be 1 per second, and the actual timestamps
@@ -5202,8 +5207,6 @@ This shows that schedule generation took 0ms, and it shows absolute timestamps i
     <property name="Test duration (seconds)" required="Yes">This is used to ensure you'll get throughput*duration samples during "test duration" timeframe.</property>
     <property name="Number of threads in the batch (threads)" required="Yes">If the value exceeds 1, then multiple threads depart from the timer simultaneously. Average throughput still meets "throughput" value.</property>
     <property name="Delay between threads in the batch (ms)" required="Yes">For instance, if set to 42, and the batch size is 3, then threads will depart at x, x+42ms, x+84ms.</property>
-    <property name="Use approximate throughput when sequence length exceeds (samples)" required="Yes">When the required number of samples is less than this limit, timer will generate exact number of samples.</property>
-    <property name="Allowed throughput surplus (percents)" required="Yes">When more than "max exact samples" samples is required, timer might generate slightly more events than specified by throughput.</property>
     <property name="Random seed (change from 0 to random)" required="Yes">Note: different timers should better have different seed values. Constant seed ensures timer generates the same delays each test start. The value of "0" means the timer is truly random (non-repeatable from one execution to another)..</property>
 </properties>
 </component>