Skip to content

Commit 82c897b

Browse files
authored
Merge pull request #52 from Blazemeter/PERFORMANCE_IMPROVEMENTS
Performance improvements
2 parents a085571 + c1c22ac commit 82c897b

8 files changed

Lines changed: 281 additions & 4 deletions

File tree

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,55 @@ The sampler will automatically add an `X-MEDIA-SEGMENT-DURATION` HTTP response h
132132

133133
In the case of MPEG DASH, the View Results Tree Listener displays the resultant samples with the associated type (manifest, inits and segments for media, audio and subtitles) to easily identify them as well.
134134

135+
## Memory tuning: response data release
136+
137+
To reduce JVM memory usage during large/long load tests, the sampler can release
138+
in-memory response bodies after each sample has been measured and passed to
139+
listeners. Two JMeter properties control this behavior (both read once at first
140+
use and cached for the JVM lifetime — they cannot be changed mid-run via
141+
`${__setProperty()}` without restarting JMeter):
142+
143+
### Segment bodies (default: release enabled)
144+
145+
hls.sampler.releaseSegmentResponseData=true # default
146+
147+
What it affects:
148+
149+
- Applies to HLS and DASH **media and init segment** samples.
150+
- Performance metrics are NOT affected: bytes received, latency, throughput, response
151+
codes, headers (including `X-MEDIA-SEGMENT-DURATION`) and download order are all preserved.
152+
- Only the raw segment **payload bytes** are dropped.
153+
154+
When to disable (set to `false` in `user.properties` or via `-J`):
155+
156+
- You need to inspect or assert on the actual segment binary content.
157+
158+
jmeter -Jhls.sampler.releaseSegmentResponseData=false ...
159+
160+
### Playlist and manifest bodies (default: release disabled)
161+
162+
hls.sampler.releasePlaylistResponseData=false # default
163+
164+
What it affects:
165+
166+
- Applies to HLS **master/media/audio/subtitle playlists** and DASH **manifest**
167+
samples on the playback loop (the 3-arg `downloadPlaylist` path used during sampling).
168+
- Variant-discovery requests (`getVariants` / GUI "Load Playlist") are not affected.
169+
- Playlists and manifests are small; enable only when you want to trim retained
170+
`SampleResult` text during long soaks.
171+
172+
When to enable:
173+
174+
jmeter -Jhls.sampler.releasePlaylistResponseData=true ...
175+
176+
### GUI / View Results Tree caveat (both properties)
177+
178+
Response bodies are cleared synchronously right after listeners are notified.
179+
In **GUI mode** with "Save Response Data" enabled, View Results Tree renders
180+
bodies lazily when you click a row — after the body has already been cleared.
181+
Released samples appear with empty response data in the tree. Use non-GUI mode
182+
for production-like soaks, or disable release when debugging response content.
183+
135184
## Assertions and Post Processors
136185

137186
The plugin supports adding assertions and post processors on any of the potential types of sample results (master playlist, media playlist, media segment, audio playlist, audio segment, subtitles, subtitles playlist and subtitles segment).

src/main/java/com/blazemeter/jmeter/videostreaming/core/VideoStreamingSampler.java

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
2222
import org.apache.jmeter.samplers.SampleResult;
23+
import org.apache.jmeter.util.JMeterUtils;
2324
import org.slf4j.Logger;
2425
import org.slf4j.LoggerFactory;
2526

@@ -28,11 +29,17 @@ public abstract class VideoStreamingSampler<T, U extends MediaSegment> {
2829
public static final String SUBTITLES_TYPE_NAME = "subtitles";
2930
public static final String VIDEO_TYPE_NAME = "video";
3031
public static final String AUDIO_TYPE_NAME = "audio";
32+
public static final String RELEASE_SEGMENT_RESPONSE_DATA_PROP =
33+
"hls.sampler.releaseSegmentResponseData";
34+
public static final String RELEASE_PLAYLIST_RESPONSE_DATA_PROP =
35+
"hls.sampler.releasePlaylistResponseData";
3136
protected static final String MASTER_TYPE_NAME = "master";
3237
protected static final String MEDIA_TYPE_NAME = "media";
3338
private static final byte[] BOM_BYTES = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
3439

3540
private static final Logger LOG = LoggerFactory.getLogger(VideoStreamingSampler.class);
41+
private static volatile Boolean releaseSegmentResponseData;
42+
private static volatile Boolean releasePlaylistResponseData;
3643

3744
protected final VideoStreamingHttpClient httpClient;
3845
protected final TimeMachine timeMachine;
@@ -116,6 +123,7 @@ protected T downloadPlaylist(URI uri, Function<T, String> name, PlaylistParser<T
116123
if (!playlistResult.isSuccessful()) {
117124
String playlistName = name.apply(null);
118125
sampleResultProcessor.accept(playlistName, playlistResult);
126+
releasePlaylistResponseBodyIfEnabled(playlistResult);
119127
throw new PlaylistDownloadException(playlistName, uri);
120128
}
121129

@@ -141,9 +149,11 @@ protected T downloadPlaylist(URI uri, Function<T, String> name, PlaylistParser<T
141149
playlistResult.setRequestHeaders(requestHeaders + videoType);
142150
}
143151
sampleResultProcessor.accept(name.apply(playlist), playlistResult);
152+
releasePlaylistResponseBodyIfEnabled(playlistResult);
144153
return playlist;
145154
} catch (PlaylistParsingException e) {
146155
sampleResultProcessor.accept(name.apply(null), baseSampler.errorResult(playlistResult, e));
156+
releasePlaylistResponseBodyIfEnabled(playlistResult);
147157
throw e;
148158
}
149159
}
@@ -196,8 +206,14 @@ protected T getManifest(URI uri, Function<T, String> name, PlaylistParser<T> pla
196206
*/
197207
private String getPlaylistContents(HTTPSampleResult result) {
198208
byte[] bytes = result.getResponseData();
199-
return (bytesStartsWith(bytes, BOM_BYTES))
200-
? new String(bytes, StandardCharsets.UTF_8) : result.getResponseDataAsString();
209+
if (bytes == null || bytes.length == 0) {
210+
return "";
211+
}
212+
int offset = 0;
213+
if (bytesStartsWith(bytes, BOM_BYTES)) {
214+
offset = BOM_BYTES.length;
215+
}
216+
return new String(bytes, offset, bytes.length - offset, StandardCharsets.UTF_8);
201217
}
202218

203219
private boolean bytesStartsWith(byte[] bytes, byte[] start) {
@@ -239,6 +255,7 @@ protected void downloadSegment(MediaSegment segment, String type) {
239255
result.getResponseHeaders() + "X-MEDIA-SEGMENT-DURATION: " + segment.getDurationSeconds()
240256
+ "\n");
241257
sampleResultProcessor.accept(VideoStreamingSampler.buildSegmentName(type), result);
258+
releaseSegmentResponseBodyIfEnabled(result);
242259
}
243260

244261
protected void downloadInitSegment(InitializationSegment initializationSegment, String type) {
@@ -247,6 +264,53 @@ protected void downloadInitSegment(InitializationSegment initializationSegment,
247264
+ (initializationSegment.getByteOffset() + initializationSegment.getByteLength() - 1));
248265
SampleResult result = httpClient.downloadUri(initializationSegment.getUri());
249266
sampleResultProcessor.accept(VideoStreamingSampler.buildInitSegmentName(type), result);
267+
releaseSegmentResponseBodyIfEnabled(result);
268+
}
269+
270+
protected void releaseSegmentResponseBodyIfEnabled(SampleResult result) {
271+
if (isReleaseSegmentResponseDataEnabled()) {
272+
clearResponseBodyPreservingMetrics(result);
273+
}
274+
}
275+
276+
protected void releasePlaylistResponseBodyIfEnabled(SampleResult result) {
277+
if (isReleasePlaylistResponseDataEnabled()) {
278+
clearResponseBodyPreservingMetrics(result);
279+
}
280+
}
281+
282+
private static void clearResponseBodyPreservingMetrics(SampleResult result) {
283+
long bytes = result.getBytesAsLong();
284+
long bodySize = result.getBodySizeAsLong();
285+
result.setResponseData(new byte[0]);
286+
result.setBytes(bytes);
287+
result.setBodySize(bodySize);
288+
}
289+
290+
static boolean isReleaseSegmentResponseDataEnabled() {
291+
if (releaseSegmentResponseData == null) {
292+
releaseSegmentResponseData =
293+
JMeterUtils.getPropDefault(RELEASE_SEGMENT_RESPONSE_DATA_PROP, true);
294+
}
295+
return releaseSegmentResponseData;
296+
}
297+
298+
static boolean isReleasePlaylistResponseDataEnabled() {
299+
if (releasePlaylistResponseData == null) {
300+
releasePlaylistResponseData =
301+
JMeterUtils.getPropDefault(RELEASE_PLAYLIST_RESPONSE_DATA_PROP, false);
302+
}
303+
return releasePlaylistResponseData;
304+
}
305+
306+
@VisibleForTesting
307+
public static void resetReleaseSegmentResponseDataCache() {
308+
releaseSegmentResponseData = null;
309+
}
310+
311+
@VisibleForTesting
312+
public static void resetReleasePlaylistResponseDataCache() {
313+
releasePlaylistResponseData = null;
250314
}
251315

252316
@VisibleForTesting

src/main/java/com/blazemeter/jmeter/videostreaming/dash/DashSampler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ private void downloadInitializationSegment() {
235235
}
236236
SampleResult result = httpClient.downloadUri(uri);
237237
sampleResultProcessor.accept(buildInitSegmentName(type), result);
238+
releaseSegmentResponseBodyIfEnabled(result);
238239
}
239240

240241
private void downloadUntilTimeSecond(double untilTimeSecond) throws InterruptedException {

src/main/java/com/blazemeter/jmeter/videostreaming/dash/Manifest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818

1919
public class Manifest extends com.blazemeter.jmeter.videostreaming.core.Manifest {
2020

21+
/**
22+
* Shared parser instance. Jackson {@code XmlMapper} read paths are thread-safe after
23+
* configuration; concurrent {@code parse} calls are validated by
24+
* {@code ManifestTest.shouldParseManifestConsistentlyWhenParsingConcurrentlyFromSharedParser}.
25+
*/
26+
private static final MPDParser MPD_PARSER = new MPDParser();
27+
2128
private final MPD mpd;
2229
private final Instant lastDownloadTime;
2330
private Instant playbackStartTime;
@@ -54,7 +61,7 @@ private List<MediaPeriod> buildPeriods(MPD mpd) {
5461
public static Manifest fromUriAndBody(URI uri, String body, Instant timestamp)
5562
throws PlaylistParsingException {
5663
try {
57-
return new Manifest(uri, new MPDParser().parse(body), timestamp);
64+
return new Manifest(uri, MPD_PARSER.parse(body), timestamp);
5865
} catch (Exception e) {
5966
throw new PlaylistParsingException(uri, e);
6067
}

src/main/java/com/blazemeter/jmeter/videostreaming/hls/Playlist.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ private Playlist(URI uri, String body, Instant downloadTimestamp, IPlaylist play
5656
public static Playlist fromUriAndBody(URI uri, String body, Instant timestamp)
5757
throws PlaylistParsingException {
5858
try {
59-
AbstractPlaylist p = PlaylistFactory.parsePlaylist(TWELVE, body.replace("\r", ""));
59+
String normalizedBody = body;
60+
if (body.indexOf('\r') >= 0) {
61+
normalizedBody = body.replace("\r", "");
62+
}
63+
AbstractPlaylist p = PlaylistFactory.parsePlaylist(TWELVE, normalizedBody);
6064
if (p.getTags().isEmpty()) {
6165
throw new PlaylistParsingException(uri, "No playlist tags found");
6266
}

src/test/java/com/blazemeter/jmeter/videostreaming/VideoStreamingSamplerTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import static org.mockito.Mockito.verify;
88
import static org.mockito.Mockito.when;
99

10+
import com.blazemeter.jmeter.JMeterTestUtils;
1011
import com.blazemeter.jmeter.videostreaming.core.SampleResultProcessor;
1112
import com.blazemeter.jmeter.videostreaming.core.TimeMachine;
1213
import com.blazemeter.jmeter.videostreaming.core.VideoStreamingHttpClient;
14+
import com.blazemeter.jmeter.videostreaming.core.VideoStreamingSampler;
1315
import com.google.common.base.Charsets;
1416
import com.google.common.io.Resources;
1517
import java.io.IOException;
@@ -21,6 +23,7 @@
2123
import java.util.function.Function;
2224
import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
2325
import org.apache.jmeter.samplers.SampleResult;
26+
import org.apache.jmeter.util.JMeterUtils;
2427
import org.junit.Before;
2528
import org.junit.runner.RunWith;
2629
import org.mockito.ArgumentCaptor;
@@ -125,6 +128,11 @@ protected static HTTPSampleResult buildBaseSampleResult(URI uri) {
125128

126129
@Before
127130
public void setUp() {
131+
JMeterTestUtils.setupJmeterEnv();
132+
JMeterUtils.setProperty(VideoStreamingSampler.RELEASE_SEGMENT_RESPONSE_DATA_PROP, "false");
133+
JMeterUtils.setProperty(VideoStreamingSampler.RELEASE_PLAYLIST_RESPONSE_DATA_PROP, "false");
134+
VideoStreamingSampler.resetReleaseSegmentResponseDataCache();
135+
VideoStreamingSampler.resetReleasePlaylistResponseDataCache();
128136
buildSampler(uriSampler);
129137
}
130138

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.blazemeter.jmeter.videostreaming.dash;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.google.common.base.Charsets;
6+
import com.google.common.io.Resources;
7+
import java.io.IOException;
8+
import java.net.URI;
9+
import java.time.Instant;
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.concurrent.Callable;
13+
import java.util.concurrent.ExecutorService;
14+
import java.util.concurrent.Executors;
15+
import java.util.concurrent.Future;
16+
import org.junit.Test;
17+
18+
public class ManifestTest {
19+
20+
private static final URI TEST_URI = URI.create("http://test/manifest.mpd");
21+
private static final int THREAD_COUNT = 32;
22+
23+
@Test
24+
public void shouldParseManifestConsistentlyWhenParsingConcurrentlyFromSharedParser()
25+
throws Exception {
26+
String defaultBody = loadResource("defaultManifest.mpd");
27+
String dynamicBody = loadResource("dynamicTimelineManifest.mpd");
28+
String refreshedBody = loadResource("dynamicTimelineManifestRefreshed.mpd");
29+
30+
Manifest expectedDefault = Manifest.fromUriAndBody(TEST_URI, defaultBody, Instant.EPOCH);
31+
Manifest expectedDynamic = Manifest.fromUriAndBody(TEST_URI, dynamicBody, Instant.EPOCH);
32+
Manifest expectedRefreshed = Manifest.fromUriAndBody(TEST_URI, refreshedBody, Instant.EPOCH);
33+
34+
List<Callable<Manifest>> tasks = new ArrayList<>();
35+
for (int i = 0; i < THREAD_COUNT; i++) {
36+
String body = i % 3 == 0 ? defaultBody : (i % 3 == 1 ? dynamicBody : refreshedBody);
37+
tasks.add(() -> Manifest.fromUriAndBody(TEST_URI, body, Instant.EPOCH));
38+
}
39+
40+
List<Manifest> results = runConcurrently(tasks);
41+
for (int i = 0; i < results.size(); i++) {
42+
Manifest parsed = results.get(i);
43+
Manifest expected = i % 3 == 0 ? expectedDefault : (i % 3 == 1 ? expectedDynamic
44+
: expectedRefreshed);
45+
assertManifestEquivalent(parsed, expected);
46+
}
47+
}
48+
49+
private static void assertManifestEquivalent(Manifest parsed, Manifest expected) {
50+
assertThat(parsed.isDynamic()).isEqualTo(expected.isDynamic());
51+
assertThat(parsed.getPeriods()).hasSize(expected.getPeriods().size());
52+
assertThat(parsed.getBandwidths()).isEqualTo(expected.getBandwidths());
53+
assertThat(parsed.getResolutions()).isEqualTo(expected.getResolutions());
54+
if (expected.isDynamic()) {
55+
assertThat(parsed.getMinimumUpdatePeriod()).isEqualTo(expected.getMinimumUpdatePeriod());
56+
} else {
57+
assertThat(parsed.getMediaPresentationDuration())
58+
.isEqualTo(expected.getMediaPresentationDuration());
59+
}
60+
}
61+
62+
private static List<Manifest> runConcurrently(List<Callable<Manifest>> tasks) throws Exception {
63+
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
64+
try {
65+
List<Future<Manifest>> futures = executor.invokeAll(tasks);
66+
List<Manifest> results = new ArrayList<>(futures.size());
67+
for (Future<Manifest> future : futures) {
68+
results.add(future.get());
69+
}
70+
return results;
71+
} finally {
72+
executor.shutdownNow();
73+
}
74+
}
75+
76+
private static String loadResource(String name) throws IOException {
77+
return Resources.toString(Resources.getResource(ManifestTest.class, name), Charsets.UTF_8);
78+
}
79+
80+
@Test
81+
public void shouldParseStaticManifestWhenFromUriAndBody() throws Exception {
82+
String body = loadResource("defaultManifest.mpd");
83+
Manifest manifest = Manifest.fromUriAndBody(TEST_URI, body, Instant.EPOCH);
84+
assertThat(manifest.isDynamic()).isFalse();
85+
assertThat(manifest.getPeriods()).hasSize(1);
86+
}
87+
88+
@Test
89+
public void shouldParseDynamicManifestWhenFromUriAndBody() throws Exception {
90+
String body = loadResource("dynamicTimelineManifest.mpd");
91+
Manifest manifest = Manifest.fromUriAndBody(TEST_URI, body, Instant.EPOCH);
92+
assertThat(manifest.isDynamic()).isTrue();
93+
assertThat(manifest.getMinimumUpdatePeriod()).isNotNull();
94+
}
95+
96+
}

0 commit comments

Comments
 (0)