Skip to content

Commit b0702e1

Browse files
committed
add copy to heap and off-heap options for FFM
1 parent 7da247a commit b0702e1

4 files changed

Lines changed: 108 additions & 6 deletions

File tree

runners/s3-benchrunner-java/src/main/java/com/example/s3benchrunner/Main.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.example.s3benchrunner.crtjava.CRTJavaBenchmarkRunner;
77
import com.example.s3benchrunner.ffmjava.FFMJavaBenchmarkRunner;
8+
import com.example.s3benchrunner.ffmjava.FFMJavaTask;
89
import com.example.s3benchrunner.sdkjava.SDKJavaBenchmarkRunner;
910

1011
public class Main {
@@ -71,6 +72,14 @@ public static void main(String[] args) throws Exception {
7172
BenchmarkRunner runner = switch (s3ClientId) {
7273
case "crt-java" -> new CRTJavaBenchmarkRunner(config, bucket, region, targetThroughputGbps);
7374
case "ffm-java" -> new FFMJavaBenchmarkRunner(config, bucket, region, targetThroughputGbps);
75+
// FFM with explicit copy to GC-managed byte[] — simulates an app that needs
76+
// a Java-owned copy of downloaded data (same destination as JNI, but via FFM)
77+
case "ffm-java-copy-heap" -> new FFMJavaBenchmarkRunner(config, bucket, region, targetThroughputGbps,
78+
FFMJavaTask.CopyMode.HEAP_COPY);
79+
// FFM with explicit copy to pre-allocated off-heap MemorySegment — simulates
80+
// an app that needs an owned copy but wants to avoid GC pressure entirely
81+
case "ffm-java-copy-offheap" -> new FFMJavaBenchmarkRunner(config, bucket, region, targetThroughputGbps,
82+
FFMJavaTask.CopyMode.OFFHEAP_COPY);
7483
case "sdk-java-client-crt" ->
7584
new SDKJavaBenchmarkRunner(config, bucket, region, targetThroughputGbps, false, true);
7685
case "sdk-java-tm-crt" ->
@@ -80,7 +89,8 @@ public static void main(String[] args) throws Exception {
8089
case "sdk-java-tm-classic" ->
8190
new SDKJavaBenchmarkRunner(config, bucket, region, targetThroughputGbps, true, false);
8291
default -> throw new RuntimeException(
83-
"Unsupported S3_CLIENT. Options are: crt-java, ffm-java, sdk-java-client-crt, sdk-java-tm-crt, sdk-java-client-classic, sdk-java-tm-classic");
92+
"Unsupported S3_CLIENT. Options are: crt-java, ffm-java, ffm-java-copy-heap, ffm-java-copy-offheap, "
93+
+ "sdk-java-client-crt, sdk-java-tm-crt, sdk-java-client-classic, sdk-java-tm-classic");
8494
};
8595

8696
long bytesPerRun = config.bytesPerRun();

runners/s3-benchrunner-java/src/main/java/com/example/s3benchrunner/ffmjava/FFMJavaBenchmarkRunner.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
* of aws-crt-java (requires Java 22+). The public API is identical to the JNI
2222
* backed CRTJavaBenchmarkRunner; the difference is that this runner is built
2323
* against an aws-crt-java branch that uses FFM instead of JNI under the hood.
24+
* <p>
25+
* The {@link FFMJavaTask.CopyMode} controls what happens with downloaded data:
26+
* <ul>
27+
* <li>{@link FFMJavaTask.CopyMode#NONE} — zero-copy, data discarded</li>
28+
* <li>{@link FFMJavaTask.CopyMode#HEAP_COPY} — copy to GC-managed byte[]</li>
29+
* <li>{@link FFMJavaTask.CopyMode#OFFHEAP_COPY} — copy to pre-allocated off-heap buffer</li>
30+
* </ul>
2431
*/
2532
public class FFMJavaBenchmarkRunner extends BenchmarkRunner {
2633

@@ -35,10 +42,20 @@ public class FFMJavaBenchmarkRunner extends BenchmarkRunner {
3542
// derived from bucket and region (e.g. mybucket.s3.us-west-2.amazonaws.com)
3643
String endpoint;
3744

45+
// Controls what happens with downloaded data in the response body callback
46+
FFMJavaTask.CopyMode copyMode;
47+
3848
public FFMJavaBenchmarkRunner(BenchmarkConfig config, String bucket, String region, double targetThroughputGbps) {
49+
this(config, bucket, region, targetThroughputGbps, FFMJavaTask.CopyMode.NONE);
50+
}
51+
52+
public FFMJavaBenchmarkRunner(BenchmarkConfig config, String bucket, String region, double targetThroughputGbps,
53+
FFMJavaTask.CopyMode copyMode) {
3954

4055
super(config, bucket, region);
4156

57+
this.copyMode = copyMode;
58+
4259
// S3 Express buckets look like "mybucket--usw2-az3--x-s3"
4360
Matcher s3ExpressMatcher = Pattern.compile("--(.*)--x-s3$").matcher(bucket);
4461
boolean isS3Express = s3ExpressMatcher.find();
@@ -97,7 +114,7 @@ public void run() {
97114
// kick off all tasks
98115
var runningTasks = new ArrayList<FFMJavaTask>(config.tasks.size());
99116
for (int i = 0; i < config.tasks.size(); ++i) {
100-
runningTasks.add(new FFMJavaTask(this, i));
117+
runningTasks.add(new FFMJavaTask(this, i, copyMode));
101118
}
102119

103120
// wait until all tasks are done

runners/s3-benchrunner-java/src/main/java/com/example/s3benchrunner/ffmjava/FFMJavaTask.java

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,62 @@
3131
* buffer via {@link MemorySegment} (no {@code DirectByteBuffer} wrapper
3232
* object allocation).</li>
3333
* </ul>
34+
* <p>
35+
* The {@link CopyMode} controls what happens with downloaded data in the
36+
* response body callback:
37+
* <ul>
38+
* <li>{@link CopyMode#NONE} — data is discarded (zero-copy, benchmark
39+
* measures pure download throughput with no data processing)</li>
40+
* <li>{@link CopyMode#HEAP_COPY} — data is copied into a GC-managed
41+
* {@code byte[]} (simulates an application that needs a Java-owned copy)</li>
42+
* <li>{@link CopyMode#OFFHEAP_COPY} — data is copied into a pre-allocated
43+
* off-heap {@link MemorySegment} (simulates an application that needs an
44+
* owned copy but wants to avoid GC pressure)</li>
45+
* </ul>
3446
*/
3547
class FFMJavaTask implements S3MetaRequestResponseHandler {
3648

49+
/**
50+
* Controls what happens with downloaded data in the response body callback.
51+
*/
52+
enum CopyMode {
53+
/** Discard data immediately — zero-copy, no allocation. */
54+
NONE,
55+
/** Copy into a GC-managed {@code byte[]} on every callback. */
56+
HEAP_COPY,
57+
/** Copy into a pre-allocated off-heap {@link MemorySegment}. */
58+
OFFHEAP_COPY,
59+
}
60+
3761
FFMJavaBenchmarkRunner runner;
3862
int taskI;
3963
TaskConfig config;
4064
S3MetaRequest metaRequest;
4165
CompletableFuture<Void> doneFuture;
66+
final CopyMode copyMode;
4267

43-
FFMJavaTask(FFMJavaBenchmarkRunner runner, int taskI) {
68+
/**
69+
* Pre-allocated off-heap buffer for {@link CopyMode#OFFHEAP_COPY}.
70+
* Sized to the maximum expected chunk size (8 MiB = typical CRT part size).
71+
* Reused across all callbacks for this task to avoid repeated allocation.
72+
*/
73+
private final MemorySegment offheapCopyBuffer;
74+
private static final long OFFHEAP_BUFFER_SIZE = 8L * 1024 * 1024; // 8 MiB
75+
76+
FFMJavaTask(FFMJavaBenchmarkRunner runner, int taskI, CopyMode copyMode) {
4477
this.runner = runner;
4578
this.taskI = taskI;
4679
this.config = runner.config.tasks.get(taskI);
80+
this.copyMode = copyMode;
4781
doneFuture = new CompletableFuture<Void>();
4882

83+
// Pre-allocate off-heap buffer if needed
84+
if (copyMode == CopyMode.OFFHEAP_COPY) {
85+
offheapCopyBuffer = Arena.ofAuto().allocate(OFFHEAP_BUFFER_SIZE);
86+
} else {
87+
offheapCopyBuffer = null;
88+
}
89+
4990
var options = new S3MetaRequestOptions();
5091

5192
options.withResponseHandler(this);
@@ -112,11 +153,41 @@ void waitUntilDone() {
112153

113154
/**
114155
* FFM download path: body chunk delivered as a zero-copy {@link MemorySegment}
115-
* view of native memory. The benchmark discards the data, so we just return 0.
156+
* view of native memory.
157+
* <p>
158+
* Behaviour depends on {@link #copyMode}:
159+
* <ul>
160+
* <li>{@link CopyMode#NONE} — data is discarded immediately (zero-copy)</li>
161+
* <li>{@link CopyMode#HEAP_COPY} — data is copied into a new {@code byte[]}
162+
* on the Java GC heap</li>
163+
* <li>{@link CopyMode#OFFHEAP_COPY} — data is copied into a pre-allocated
164+
* off-heap {@link MemorySegment} (no GC pressure)</li>
165+
* </ul>
116166
*/
117167
@Override
118168
public int onResponseBody(MemorySegment bodyBytesIn, long objectRangeStart, long objectRangeEnd) {
119-
// Benchmark discards downloaded data — no backpressure increment needed.
169+
switch (copyMode) {
170+
case NONE:
171+
// Zero-copy: discard data, no allocation.
172+
break;
173+
174+
case HEAP_COPY:
175+
// Copy into a GC-managed byte[]. This simulates an application
176+
// that needs a Java-owned copy of the data. The byte[] is
177+
// immediately eligible for GC after this callback returns.
178+
@SuppressWarnings("unused")
179+
byte[] heapCopy = bodyBytesIn.toArray(ValueLayout.JAVA_BYTE);
180+
break;
181+
182+
case OFFHEAP_COPY:
183+
// Copy into the pre-allocated off-heap buffer. This simulates
184+
// an application that needs an owned copy but wants to avoid
185+
// GC pressure. The buffer is reused across callbacks.
186+
long chunkSize = bodyBytesIn.byteSize();
187+
MemorySegment dest = offheapCopyBuffer.asSlice(0, chunkSize);
188+
MemorySegment.copy(bodyBytesIn, 0, dest, 0, chunkSize);
189+
break;
190+
}
120191
return 0;
121192
}
122193

scripts/utils/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ def _add_runner(runner: Runner):
3838
_add_runner(Runner('cpp', s3_clients=[
3939
'sdk-cpp-client-crt', 'sdk-cpp-client-classic', 'sdk-cpp-tm-classic']))
4040
_add_runner(Runner('java',
41-
s3_clients=['crt-java', 'ffm-java', 'sdk-java-client-crt', 'sdk-java-tm-crt', 'sdk-java-client-classic', 'sdk-java-tm-classic']))
41+
s3_clients=['crt-java', 'ffm-java',
42+
'ffm-java-copy-heap', # FFM + copy to GC-managed byte[]
43+
'ffm-java-copy-offheap', # FFM + copy to pre-allocated off-heap MemorySegment
44+
'sdk-java-client-crt', 'sdk-java-tm-crt',
45+
'sdk-java-client-classic', 'sdk-java-tm-classic']))
4246
_add_runner(Runner('python',
4347
s3_clients=['crt-python', 'cli-crt', 'cli-classic', 'boto3-crt', 'boto3-classic']))
4448
_add_runner(Runner('rust', s3_clients=['sdk-rust-tm']))

0 commit comments

Comments
 (0)