Skip to content

Commit f5ef901

Browse files
Trivial optimizations on copies
1 parent 67aa7c0 commit f5ef901

8 files changed

Lines changed: 237 additions & 68 deletions

File tree

sdk-core/src/main/java/dev/restate/sdk/core/HandlerContextImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,8 +530,8 @@ public void proposeFailure(Throwable toWrite, @Nullable RetryPolicy retryPolicy)
530530
}
531531

532532
private void pumpOutput() {
533-
byte[] chunk = stateMachine.takeOutput();
534-
if (chunk.length > 0) outputSink.accept(Slice.wrap(chunk));
533+
Slice chunk = stateMachine.takeOutput();
534+
if (chunk.readableBytes() > 0) outputSink.accept(chunk);
535535
}
536536

537537
@Override

sdk-core/src/main/java/dev/restate/sdk/core/RequestProcessorImpl.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public void onNext(Slice slice) {
131131
if (state == State.CLOSED) return;
132132

133133
try {
134-
stateMachine.notifyInput(slice.toByteArray());
134+
stateMachine.notifyInput(slice);
135135
onInputEvent();
136136
} catch (Throwable e) {
137137
onError(e);
@@ -199,19 +199,19 @@ private void onClose() {
199199
cancelInputSubscription();
200200

201201
// Pump remaining output
202-
byte[] chunk;
202+
Slice chunk;
203203
if (outputSubscriber != null) {
204204
chunk = stateMachine.takeOutput();
205205
} else {
206-
chunk = new byte[0];
206+
chunk = Slice.EMPTY;
207207
}
208208

209209
// Close state machine
210210
this.state = State.CLOSED;
211211
stateMachine.close();
212212

213213
// Send final bits and close output subscriber
214-
if (chunk.length > 0) outputSubscriber.onNext(Slice.wrap(chunk));
214+
if (chunk.readableBytes() > 0) outputSubscriber.onNext(chunk);
215215
outputSubscriber.onComplete();
216216
outputSubscriber = null;
217217
}

sdk-core/src/main/java/dev/restate/sdk/core/statemachine/JavaStateMachine.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
*
4242
* <p>This is a port of the legacy {@code dev.restate.sdk.core.statemachine.StateMachineImpl}, which
4343
* was a {@link Flow.Processor}. The canonical interface is imperative, so the legacy {@code
44-
* waitForReady()} / {@code onNextEvent()} signalling is replaced by {@link #notifyInput(byte[])} /
44+
* waitForReady()} / {@code onNextEvent()} signalling is replaced by {@link #notifyInput(Slice)} /
4545
* {@link #isReadyToExecute()} and synchronous {@link #doAwait(UnresolvedFuture)}, while the Flow
4646
* output side is replaced by a buffer drained via {@link #takeOutput()}.
4747
*/
@@ -101,9 +101,12 @@ public JavaStateMachine(HeadersAccessor headersAccessor) {
101101
// -------------------------------------------------------------------------
102102

103103
@Override
104-
public void notifyInput(byte[] bytes) {
104+
public void notifyInput(Slice bytes) {
105105
LOG.trace("Received input slice");
106-
this.messageDecoder.offer(Slice.wrap(bytes));
106+
// Zero-copy: hand the slice straight to the decoder (which retains it via unsafeWrap). Safe
107+
// because the transport hands out a retainable buffer (Vert.x wraps non-heap byte bufs as
108+
// unreleasable), matching the legacy/main input path.
109+
this.messageDecoder.offer(bytes);
107110

108111
InvocationInput invocationInput = this.messageDecoder.next();
109112
while (invocationInput != null) {
@@ -132,8 +135,8 @@ public void notifyError(Throwable throwable) {
132135
}
133136

134137
@Override
135-
public byte[] takeOutput() {
136-
return this.outputSink.take();
138+
public Slice takeOutput() {
139+
return Slice.wrap(this.outputSink.take());
137140
}
138141

139142
@Override

sdk-core/src/main/java/dev/restate/sdk/core/statemachine/StateMachine.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,18 @@ public interface StateMachine extends AutoCloseable {
4949
// Imperative replacement for main's Flow.Processor + waitForReady/onNextEvent: bytes are fed in
5050
// with notifyInput, drained out with takeOutput, and isReadyToExecute gates handler start.
5151

52-
void notifyInput(byte[] bytes);
52+
/** Feed the next chunk of wire input. */
53+
void notifyInput(Slice bytes);
5354

5455
void notifyInputClosed();
5556

5657
void notifyError(Throwable throwable);
5758

58-
byte[] takeOutput();
59+
/**
60+
* Drain the next chunk of serialized output ready for the wire, or {@link Slice#EMPTY} when
61+
* nothing is buffered.
62+
*/
63+
Slice takeOutput();
5964

6065
boolean isReadyToExecute();
6166

sdk-core/src/main/java23/dev/restate/sdk/core/statemachine/ffm/FfmEncoding.java

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.ByteArrayOutputStream;
2020
import java.lang.foreign.Arena;
2121
import java.lang.foreign.MemorySegment;
22+
import java.lang.foreign.SegmentAllocator;
2223
import java.lang.foreign.ValueLayout;
2324
import java.nio.charset.StandardCharsets;
2425
import java.util.Collection;
@@ -40,31 +41,45 @@ final class FfmEncoding {
4041
private FfmEncoding() {}
4142

4243
// -------------------------------------------------------------------------
43-
// Native segment allocation (inputs: copy-for-now into the call arena)
44+
// Native segment allocation (inputs: copy-for-now into the call scratch)
4445
// -------------------------------------------------------------------------
4546

4647
/**
4748
* Allocate a native segment holding the given bytes, or {@link MemorySegment#NULL} when {@code
4849
* bytes} is null/empty. The Rust side treats a null ptr / zero len as the empty slice.
4950
*/
50-
static MemorySegment allocateBytes(Arena arena, byte @Nullable [] bytes) {
51+
static MemorySegment allocateBytes(SegmentAllocator alloc, byte @Nullable [] bytes) {
5152
if (bytes == null || bytes.length == 0) {
5253
return MemorySegment.NULL;
5354
}
54-
MemorySegment seg = arena.allocate(bytes.length);
55+
MemorySegment seg = alloc.allocate(bytes.length);
5556
MemorySegment.copy(bytes, 0, seg, ValueLayout.JAVA_BYTE, 0, bytes.length);
5657
return seg;
5758
}
5859

60+
/**
61+
* Allocate a native segment holding the bytes of {@code slice}, copied straight from the slice's
62+
* buffer (no intermediate heap {@code byte[]}), or {@link MemorySegment#NULL} when empty.
63+
*/
64+
static MemorySegment allocateSlice(SegmentAllocator alloc, dev.restate.common.Slice slice) {
65+
int len = slice.readableBytes();
66+
if (len == 0) {
67+
return MemorySegment.NULL;
68+
}
69+
MemorySegment seg = alloc.allocate(len);
70+
slice.copyTo(seg.asByteBuffer());
71+
return seg;
72+
}
73+
5974
/**
6075
* Allocate a native segment holding the UTF-8 bytes of {@code s}, or {@link MemorySegment#NULL}
6176
* when {@code s} is null. An empty (non-null) string yields a zero-length, non-null segment.
6277
*/
63-
static MemorySegment allocateUtf8(Arena arena, @Nullable String s) {
78+
static MemorySegment allocateUtf8(SegmentAllocator alloc, @Nullable String s) {
6479
if (s == null) {
6580
return MemorySegment.NULL;
6681
}
67-
return allocateBytes(arena, s.getBytes(StandardCharsets.UTF_8));
82+
return allocateBytes(alloc, s.getBytes(StandardCharsets.UTF_8));
6883
}
6984

7085
static long len(MemorySegment seg) {
@@ -102,6 +117,37 @@ static String takeSliceString(MemorySegment sliceStruct) {
102117
return new String(bytes, StandardCharsets.UTF_8);
103118
}
104119

120+
/**
121+
* Wraps an owned result {@link Slice} (Rust-allocated; must be released via {@code free_buffer})
122+
* in a {@link MemorySegmentSlice} read zero-copy — no copy-out into a heap {@code byte[]}. The
123+
* native buffer is freed when the returned slice becomes unreachable (GC-tied via {@link
124+
* Arena#ofAuto()}), so it is safe to read later and from another thread (deserialization on a
125+
* different dispatcher, or an async socket write). Returns {@link dev.restate.common.Slice#EMPTY}
126+
* for the null/empty slice.
127+
*/
128+
static dev.restate.common.Slice wrapOwnedSlice(MemorySegment sliceStruct) {
129+
MemorySegment ptr = Slice.ptr(sliceStruct);
130+
long len = Slice.len(sliceStruct);
131+
if (ptr.address() == 0 || len == 0) {
132+
return dev.restate.common.Slice.EMPTY;
133+
}
134+
MemorySegment seg =
135+
ptr.reinterpret(len, Arena.ofAuto(), s -> SharedCoreNative.free_buffer(s, len));
136+
return new MemorySegmentSlice(seg);
137+
}
138+
139+
/**
140+
* Releases an owned result {@link Slice} we are not consuming (e.g. the unused {@code value}/
141+
* {@code extra} arm of a notification). No-op for the null/empty slice.
142+
*/
143+
static void freeSlice(MemorySegment sliceStruct) {
144+
MemorySegment ptr = Slice.ptr(sliceStruct);
145+
long len = Slice.len(sliceStruct);
146+
if (ptr.address() != 0 && len != 0) {
147+
SharedCoreNative.free_buffer(ptr, len);
148+
}
149+
}
150+
105151
private static final byte[] EMPTY_BYTES = new byte[0];
106152

107153
// -------------------------------------------------------------------------
@@ -114,54 +160,54 @@ static String takeSliceString(MemorySegment sliceStruct) {
114160
* header-list blob.
115161
*/
116162
static MemorySegment buildTarget(
117-
Arena arena,
163+
SegmentAllocator alloc,
118164
Target target,
119165
@Nullable String idempotencyKey,
120166
@Nullable Collection<Map.Entry<String, String>> headers) {
121-
MemorySegment t = TargetAbi.allocate(arena);
167+
MemorySegment t = TargetAbi.allocate(alloc);
122168

123-
MemorySegment service = allocateUtf8(arena, target.getService());
169+
MemorySegment service = allocateUtf8(alloc, target.getService());
124170
TargetAbi.service_ptr(t, service);
125171
TargetAbi.service_len(t, len(service));
126172

127-
MemorySegment handler = allocateUtf8(arena, target.getHandler());
173+
MemorySegment handler = allocateUtf8(alloc, target.getHandler());
128174
TargetAbi.handler_ptr(t, handler);
129175
TargetAbi.handler_len(t, len(handler));
130176

131-
MemorySegment key = allocateUtf8(arena, target.getKey());
177+
MemorySegment key = allocateUtf8(alloc, target.getKey());
132178
TargetAbi.key_ptr(t, key);
133179
TargetAbi.key_len(t, len(key));
134180

135-
MemorySegment idem = allocateUtf8(arena, idempotencyKey);
181+
MemorySegment idem = allocateUtf8(alloc, idempotencyKey);
136182
TargetAbi.idempotency_key_ptr(t, idem);
137183
TargetAbi.idempotency_key_len(t, len(idem));
138184

139-
MemorySegment headersBlob = allocateBytes(arena, encodeHeaderList(headers));
185+
MemorySegment headersBlob = allocateBytes(alloc, encodeHeaderList(headers));
140186
TargetAbi.headers_ptr(t, headersBlob);
141187
TargetAbi.headers_len(t, len(headersBlob));
142188

143189
return t;
144190
}
145191

146-
/** Builds a success-valued {@link NonEmptyValueAbi} struct in {@code arena}. */
147-
static MemorySegment buildSuccessValue(Arena arena, byte[] value) {
148-
MemorySegment v = NonEmptyValueAbi.allocate(arena);
192+
/** Builds a success-valued {@link NonEmptyValueAbi} struct, copying {@code value} zero-hop. */
193+
static MemorySegment buildSuccessValue(SegmentAllocator alloc, dev.restate.common.Slice value) {
194+
MemorySegment v = NonEmptyValueAbi.allocate(alloc);
149195
NonEmptyValueAbi.is_failure(v, 0);
150-
MemorySegment val = allocateBytes(arena, value);
196+
MemorySegment val = allocateSlice(alloc, value);
151197
NonEmptyValueAbi.value_ptr(v, val);
152198
NonEmptyValueAbi.value_len(v, len(val));
153199
NonEmptyValueAbi.failure_ptr(v, MemorySegment.NULL);
154200
NonEmptyValueAbi.failure_len(v, 0L);
155201
return v;
156202
}
157203

158-
/** Builds a failure-valued {@link NonEmptyValueAbi} struct in {@code arena}. */
159-
static MemorySegment buildFailureValue(Arena arena, TerminalException failure) {
160-
MemorySegment v = NonEmptyValueAbi.allocate(arena);
204+
/** Builds a failure-valued {@link NonEmptyValueAbi} struct. */
205+
static MemorySegment buildFailureValue(SegmentAllocator alloc, TerminalException failure) {
206+
MemorySegment v = NonEmptyValueAbi.allocate(alloc);
161207
NonEmptyValueAbi.is_failure(v, 1);
162208
NonEmptyValueAbi.value_ptr(v, MemorySegment.NULL);
163209
NonEmptyValueAbi.value_len(v, 0L);
164-
MemorySegment fail = allocateBytes(arena, encodeFailure(failure));
210+
MemorySegment fail = allocateBytes(alloc, encodeFailure(failure));
165211
NonEmptyValueAbi.failure_ptr(v, fail);
166212
NonEmptyValueAbi.failure_len(v, len(fail));
167213
return v;

0 commit comments

Comments
 (0)