Skip to content

Commit dfeea35

Browse files
authored
[Dataflow Streaming] Reuse ByteStringOutputStream buffers to reduce GC overhead (#36165)
1 parent 117042c commit dfeea35

5 files changed

Lines changed: 186 additions & 14 deletions

File tree

runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
package org.apache.beam.runners.dataflow.worker;
1919

2020
import static org.apache.beam.runners.dataflow.util.Structs.getString;
21-
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
2221

2322
import com.google.auto.service.AutoService;
2423
import java.io.IOException;
@@ -72,15 +71,29 @@ class WindmillSink<T> extends Sink<WindowedValue<T>> {
7271
this.context = context;
7372
}
7473

74+
private static ByteString encodeMetadata(
75+
ByteStringOutputStream stream,
76+
Coder<Collection<? extends BoundedWindow>> windowsCoder,
77+
Collection<? extends BoundedWindow> windows,
78+
PaneInfo paneInfo)
79+
throws IOException {
80+
try {
81+
PaneInfoCoder.INSTANCE.encode(paneInfo, stream);
82+
windowsCoder.encode(windows, stream, Coder.Context.OUTER);
83+
return stream.toByteStringAndReset();
84+
} catch (Exception e) {
85+
stream.reset();
86+
throw e;
87+
}
88+
}
89+
7590
public static ByteString encodeMetadata(
7691
Coder<Collection<? extends BoundedWindow>> windowsCoder,
7792
Collection<? extends BoundedWindow> windows,
7893
PaneInfo paneInfo)
7994
throws IOException {
8095
ByteStringOutputStream stream = new ByteStringOutputStream();
81-
PaneInfoCoder.INSTANCE.encode(paneInfo, stream);
82-
windowsCoder.encode(windows, stream, Coder.Context.OUTER);
83-
return stream.toByteString();
96+
return encodeMetadata(stream, windowsCoder, windows, paneInfo);
8497
}
8598

8699
public static PaneInfo decodeMetadataPane(ByteString metadata) throws IOException {
@@ -153,15 +166,15 @@ private WindmillStreamWriter(String destinationName) {
153166
}
154167

155168
private <EncodeT> ByteString encode(Coder<EncodeT> coder, EncodeT object) throws IOException {
156-
checkState(
157-
stream.size() == 0,
158-
"Expected output stream to be empty but had %s",
159-
stream.toByteString());
169+
if (stream.size() != 0) {
170+
throw new IllegalStateException(
171+
"Expected output stream to be empty but had " + stream.toByteString());
172+
}
160173
try {
161174
coder.encode(object, stream, Coder.Context.OUTER);
162175
return stream.toByteStringAndReset();
163176
} catch (Exception e) {
164-
stream.toByteStringAndReset();
177+
stream.reset();
165178
throw e;
166179
}
167180
}
@@ -171,7 +184,8 @@ private <EncodeT> ByteString encode(Coder<EncodeT> coder, EncodeT object) throws
171184
public long add(WindowedValue<T> data) throws IOException {
172185
ByteString key, value;
173186
ByteString id = ByteString.EMPTY;
174-
ByteString metadata = encodeMetadata(windowsCoder, data.getWindows(), data.getPaneInfo());
187+
ByteString metadata =
188+
encodeMetadata(stream, windowsCoder, data.getWindows(), data.getPaneInfo());
175189
if (valueCoder instanceof KvCoder) {
176190
KvCoder kvCoder = (KvCoder) valueCoder;
177191
KV kv = (KV) data.getValue();

runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateUtil.java

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,78 @@
1818
package org.apache.beam.runners.dataflow.worker.windmill.state;
1919

2020
import java.io.IOException;
21+
import java.lang.ref.SoftReference;
2122
import org.apache.beam.runners.core.StateNamespace;
2223
import org.apache.beam.runners.core.StateTag;
2324
import org.apache.beam.sdk.util.ByteStringOutputStream;
2425
import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString;
2526
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting;
27+
import org.checkerframework.checker.nullness.qual.Nullable;
2628

2729
class WindmillStateUtil {
2830

31+
private static final ThreadLocal<@Nullable RefHolder> threadLocalRefHolder = new ThreadLocal<>();
32+
2933
/** Encodes the given namespace and address as {@code &lt;namespace&gt;+&lt;address&gt;}. */
3034
@VisibleForTesting
3135
static ByteString encodeKey(StateNamespace namespace, StateTag<?> address) {
36+
RefHolder refHolder = getRefHolderFromThreadLocal();
37+
// Use ByteStringOutputStream rather than concatenation and String.format. We build these keys
38+
// a lot, and this leads to better performance results. See associated benchmarks.
39+
ByteStringOutputStream stream;
40+
boolean releaseThreadLocal;
41+
if (refHolder.inUse) {
42+
// If the thread local stream is already in use, create a new one
43+
stream = new ByteStringOutputStream();
44+
releaseThreadLocal = false;
45+
} else {
46+
stream = getByteStringOutputStream(refHolder);
47+
refHolder.inUse = true;
48+
releaseThreadLocal = true;
49+
}
3250
try {
33-
// Use ByteStringOutputStream rather than concatenation and String.format. We build these keys
34-
// a lot, and this leads to better performance results. See associated benchmarks.
35-
ByteStringOutputStream stream = new ByteStringOutputStream();
3651
// stringKey starts and ends with a slash. We separate it from the
3752
// StateTag ID by a '+' (which is guaranteed not to be in the stringKey) because the
3853
// ID comes from the user.
3954
namespace.appendTo(stream);
4055
stream.append('+');
4156
address.appendTo(stream);
42-
return stream.toByteString();
57+
return stream.toByteStringAndReset();
4358
} catch (IOException e) {
4459
throw new RuntimeException(e);
60+
} finally {
61+
stream.reset();
62+
if (releaseThreadLocal) {
63+
refHolder.inUse = false;
64+
}
65+
}
66+
}
67+
68+
private static class RefHolder {
69+
public SoftReference<@Nullable ByteStringOutputStream> streamRef =
70+
new SoftReference<>(new ByteStringOutputStream());
71+
72+
// Boolean is true when the thread local stream is already in use by the current thread.
73+
// Used to avoid reusing the same stream from nested calls if any.
74+
public boolean inUse = false;
75+
}
76+
77+
private static RefHolder getRefHolderFromThreadLocal() {
78+
@Nullable RefHolder refHolder = threadLocalRefHolder.get();
79+
if (refHolder == null) {
80+
refHolder = new RefHolder();
81+
threadLocalRefHolder.set(refHolder);
82+
}
83+
return refHolder;
84+
}
85+
86+
private static ByteStringOutputStream getByteStringOutputStream(RefHolder refHolder) {
87+
@Nullable
88+
ByteStringOutputStream stream = refHolder.streamRef == null ? null : refHolder.streamRef.get();
89+
if (stream == null) {
90+
stream = new ByteStringOutputStream();
91+
refHolder.streamRef = new SoftReference<>(stream);
4592
}
93+
return stream;
4694
}
4795
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.runners.dataflow.worker.windmill.state;
19+
20+
import static org.junit.Assert.assertEquals;
21+
22+
import java.io.IOException;
23+
import org.apache.beam.runners.core.StateNamespace;
24+
import org.apache.beam.runners.core.StateNamespaceForTest;
25+
import org.apache.beam.runners.core.StateTag;
26+
import org.apache.beam.runners.core.StateTags;
27+
import org.apache.beam.sdk.coders.VarIntCoder;
28+
import org.apache.beam.sdk.state.SetState;
29+
import org.apache.beam.sdk.state.StateSpec;
30+
import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString;
31+
import org.junit.Test;
32+
import org.junit.runner.RunWith;
33+
import org.junit.runners.JUnit4;
34+
35+
@RunWith(JUnit4.class)
36+
public class WindmillStateUtilTest {
37+
38+
@Test
39+
public void testEncodeKey() {
40+
StateNamespaceForTest namespace = new StateNamespaceForTest("key");
41+
StateTag<SetState<Integer>> foo = StateTags.set("foo", VarIntCoder.of());
42+
ByteString bytes = WindmillStateUtil.encodeKey(namespace, foo);
43+
assertEquals("key+ufoo", bytes.toStringUtf8());
44+
}
45+
46+
@Test
47+
public void testEncodeKeyNested() {
48+
// Hypothetical case where a namespace/tag encoding depends on a call to encodeKey
49+
// This tests if thread locals in WindmillStateUtil are not reused with nesting
50+
StateNamespaceForTest namespace1 = new StateNamespaceForTest("key");
51+
StateTag<SetState<Integer>> tag1 = StateTags.set("foo", VarIntCoder.of());
52+
StateTag<SetState<Integer>> tag2 =
53+
new StateTag<SetState<Integer>>() {
54+
@Override
55+
public void appendTo(Appendable sb) throws IOException {
56+
WindmillStateUtil.encodeKey(namespace1, tag1);
57+
sb.append("tag2");
58+
}
59+
60+
@Override
61+
public String getId() {
62+
return "";
63+
}
64+
65+
@Override
66+
public StateSpec<SetState<Integer>> getSpec() {
67+
return null;
68+
}
69+
70+
@Override
71+
public SetState<Integer> bind(StateBinder binder) {
72+
return null;
73+
}
74+
};
75+
76+
StateNamespace namespace2 =
77+
new StateNamespaceForTest("key") {
78+
@Override
79+
public void appendTo(Appendable sb) throws IOException {
80+
WindmillStateUtil.encodeKey(namespace1, tag1);
81+
sb.append("namespace2");
82+
}
83+
};
84+
ByteString bytes = WindmillStateUtil.encodeKey(namespace2, tag2);
85+
assertEquals("namespace2+tag2", bytes.toStringUtf8());
86+
}
87+
}

sdks/java/core/src/main/java/org/apache/beam/sdk/util/ByteStringOutputStream.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ public ByteString toByteStringAndReset() {
158158
return rval;
159159
}
160160

161+
/*
162+
* Resets the output stream to be re-used possibly re-using any existing buffers.
163+
*/
164+
public void reset() {
165+
if (size() == 0) {
166+
return;
167+
}
168+
toByteStringAndReset();
169+
}
170+
161171
/**
162172
* Creates a byte string with the given size containing the prefix of the contents of this output
163173
* stream.

sdks/java/core/src/test/java/org/apache/beam/sdk/util/ByteStringOutputStreamTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,19 @@ public void appendEquivalentToOutputStreamWriterChar() throws IOException {
223223
}
224224
}
225225

226+
@Test
227+
public void testReset() throws IOException {
228+
try (ByteStringOutputStream stream = new ByteStringOutputStream()) {
229+
stream.reset();
230+
assertEquals(ByteString.EMPTY, stream.toByteString());
231+
stream.append("test");
232+
stream.reset();
233+
assertEquals(ByteString.EMPTY, stream.toByteString());
234+
stream.reset();
235+
assertEquals(ByteString.EMPTY, stream.toByteString());
236+
}
237+
}
238+
226239
// Grow the elements based upon an approximation of the fibonacci sequence.
227240
private static int next(int current) {
228241
double a = Math.max(1, current * (1 + Math.sqrt(5)) / 2.0);

0 commit comments

Comments
 (0)