|
31 | 31 | * buffer via {@link MemorySegment} (no {@code DirectByteBuffer} wrapper |
32 | 32 | * object allocation).</li> |
33 | 33 | * </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> |
34 | 46 | */ |
35 | 47 | class FFMJavaTask implements S3MetaRequestResponseHandler { |
36 | 48 |
|
| 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 | + |
37 | 61 | FFMJavaBenchmarkRunner runner; |
38 | 62 | int taskI; |
39 | 63 | TaskConfig config; |
40 | 64 | S3MetaRequest metaRequest; |
41 | 65 | CompletableFuture<Void> doneFuture; |
| 66 | + final CopyMode copyMode; |
42 | 67 |
|
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) { |
44 | 77 | this.runner = runner; |
45 | 78 | this.taskI = taskI; |
46 | 79 | this.config = runner.config.tasks.get(taskI); |
| 80 | + this.copyMode = copyMode; |
47 | 81 | doneFuture = new CompletableFuture<Void>(); |
48 | 82 |
|
| 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 | + |
49 | 90 | var options = new S3MetaRequestOptions(); |
50 | 91 |
|
51 | 92 | options.withResponseHandler(this); |
@@ -112,11 +153,41 @@ void waitUntilDone() { |
112 | 153 |
|
113 | 154 | /** |
114 | 155 | * 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> |
116 | 166 | */ |
117 | 167 | @Override |
118 | 168 | 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 | + } |
120 | 191 | return 0; |
121 | 192 | } |
122 | 193 |
|
|
0 commit comments