Skip to content

Commit 9123db5

Browse files
committed
perf: decode BYTES columns lazily
BYTES columns are encoded as Base64 strings. Decoding these are relatively CPU-heavy, especially for large values. Decoding them is not always necessary if the user only needs the Base64 string. Also, the internally used Guava decoder is less efficient than JDK implementations that are available from Java 8 and onwards. This change therefore delays the decoding of BYTES columns until it is actually necessary, and then uses the JDK implementation instead of the Guava version. The JDK implementation in OpenJDK 17 uses approx 1/3 of the CPU cycles of the Guava version.
1 parent a40bda9 commit 9123db5

File tree

6 files changed

+164
-11
lines changed

6 files changed

+164
-11
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud;
18+
19+
import com.google.api.core.InternalApi;
20+
import com.google.protobuf.ByteStringHelper;
21+
22+
@InternalApi
23+
public class ByteArrayHelper {
24+
@InternalApi
25+
public static ByteArray wrap(byte[] bytes) {
26+
return new ByteArray(ByteStringHelper.wrap(bytes));
27+
}
28+
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.api.client.util.ExponentialBackOff;
2727
import com.google.api.gax.retrying.RetrySettings;
2828
import com.google.cloud.ByteArray;
29+
import com.google.cloud.ByteArrayHelper;
2930
import com.google.cloud.Date;
3031
import com.google.cloud.Timestamp;
3132
import com.google.cloud.spanner.Type.StructField;
@@ -55,6 +56,7 @@
5556
import java.math.BigDecimal;
5657
import java.util.AbstractList;
5758
import java.util.ArrayList;
59+
import java.util.Base64;
5860
import java.util.BitSet;
5961
import java.util.Collections;
6062
import java.util.Iterator;
@@ -353,6 +355,39 @@ private boolean isMergeable(KindCase kind) {
353355
}
354356
}
355357

358+
static class LazyByteArray implements Serializable {
359+
private static final Base64.Decoder DECODER = Base64.getDecoder();
360+
private final String base64String;
361+
private final transient AbstractLazyInitializer<ByteArray> byteArray =
362+
new AbstractLazyInitializer<ByteArray>() {
363+
@Override
364+
protected ByteArray initialize() {
365+
return ByteArrayHelper.wrap(DECODER.decode(base64String));
366+
}
367+
};
368+
369+
LazyByteArray(String base64String) {
370+
this.base64String = base64String;
371+
}
372+
373+
ByteArray getByteArray() {
374+
try {
375+
return byteArray.get();
376+
} catch (Throwable t) {
377+
throw SpannerExceptionFactory.asSpannerException(t);
378+
}
379+
}
380+
381+
String getBase64String() {
382+
return base64String;
383+
}
384+
385+
@Override
386+
public String toString() {
387+
return getBase64String();
388+
}
389+
}
390+
356391
static class GrpcStruct extends Struct implements Serializable {
357392
private final Type type;
358393
private final List<Object> rowData;
@@ -395,7 +430,12 @@ private Object writeReplace() {
395430
builder.set(fieldName).to(Value.pgJsonb((String) value));
396431
break;
397432
case BYTES:
398-
builder.set(fieldName).to((ByteArray) value);
433+
builder
434+
.set(fieldName)
435+
.to(
436+
value instanceof LazyByteArray
437+
? ((LazyByteArray) value).getByteArray()
438+
: (ByteArray) value);
399439
break;
400440
case TIMESTAMP:
401441
builder.set(fieldName).to((Timestamp) value);
@@ -511,7 +551,7 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot
511551
return proto.getStringValue();
512552
case BYTES:
513553
checkType(fieldType, proto, KindCase.STRING_VALUE);
514-
return ByteArray.fromBase64(proto.getStringValue());
554+
return new LazyByteArray(proto.getStringValue());
515555
case TIMESTAMP:
516556
checkType(fieldType, proto, KindCase.STRING_VALUE);
517557
return Timestamp.parseTimestamp(proto.getStringValue());
@@ -619,7 +659,8 @@ protected BigDecimal getBigDecimalInternal(int columnIndex) {
619659

620660
@Override
621661
protected String getStringInternal(int columnIndex) {
622-
return (String) rowData.get(columnIndex);
662+
Object value = rowData.get(columnIndex);
663+
return value == null ? null : value.toString();
623664
}
624665

625666
@Override
@@ -634,7 +675,7 @@ protected String getPgJsonbInternal(int columnIndex) {
634675

635676
@Override
636677
protected ByteArray getBytesInternal(int columnIndex) {
637-
return (ByteArray) rowData.get(columnIndex);
678+
return ((LazyByteArray) rowData.get(columnIndex)).getByteArray();
638679
}
639680

640681
@Override
@@ -785,9 +826,10 @@ protected List<String> getPgJsonbListInternal(int columnIndex) {
785826
}
786827

787828
@Override
788-
@SuppressWarnings("unchecked") // We know ARRAY<BYTES> produces a List<ByteArray>.
829+
@SuppressWarnings("unchecked") // We know ARRAY<BYTES> produces a List<LazyByteArray>.
789830
protected List<ByteArray> getBytesListInternal(int columnIndex) {
790-
return Collections.unmodifiableList((List<ByteArray>) rowData.get(columnIndex));
831+
return Lists.transform(
832+
(List<LazyByteArray>) rowData.get(columnIndex), l -> l == null ? null : l.getByteArray());
791833
}
792834

793835
@Override

google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,20 @@ public BigDecimal getBigDecimal(String columnName) {
170170
public String getString(int columnIndex) {
171171
checkNonNullOfTypes(
172172
columnIndex,
173-
Arrays.asList(Type.string(), Type.pgNumeric()),
173+
Arrays.asList(Type.string(), Type.pgNumeric(), Type.bytes()),
174174
columnIndex,
175-
"STRING, NUMERIC");
175+
"STRING, NUMERIC, BYTES");
176176
return getStringInternal(columnIndex);
177177
}
178178

179179
@Override
180180
public String getString(String columnName) {
181181
int columnIndex = getColumnIndex(columnName);
182182
checkNonNullOfTypes(
183-
columnIndex, Arrays.asList(Type.string(), Type.pgNumeric()), columnName, "STRING, NUMERIC");
183+
columnIndex,
184+
Arrays.asList(Type.string(), Type.pgNumeric(), Type.bytes()),
185+
columnName,
186+
"STRING, NUMERIC, BYTES");
184187
return getStringInternal(columnIndex);
185188
}
186189

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.protobuf;
18+
19+
import com.google.api.core.InternalApi;
20+
21+
@InternalApi
22+
public class ByteStringHelper {
23+
24+
@InternalApi
25+
public static ByteString wrap(byte[] bytes) {
26+
return ByteString.wrap(bytes);
27+
}
28+
}

google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.google.cloud.Date;
2828
import com.google.cloud.Timestamp;
2929
import com.google.common.base.Throwables;
30+
import com.google.common.collect.ImmutableList;
3031
import java.lang.reflect.InvocationTargetException;
3132
import java.lang.reflect.Method;
3233
import java.math.BigDecimal;
@@ -215,7 +216,7 @@ public static Collection<Object[]> parameters() {
215216
"getStringInternal",
216217
"1.23",
217218
"getString",
218-
Collections.singletonList("getValue")
219+
ImmutableList.of("getValue", "getBigDecimal")
219220
},
220221
{
221222
Type.string(),
@@ -229,7 +230,7 @@ public static Collection<Object[]> parameters() {
229230
"getBytesInternal",
230231
ByteArray.copyFrom(new byte[] {0}),
231232
"getBytes",
232-
Collections.singletonList("getValue")
233+
ImmutableList.of("getValue", "getString")
233234
},
234235
{
235236
Type.json(),

google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.google.api.core.ApiFutures;
3838
import com.google.api.gax.grpc.testing.LocalChannelProvider;
3939
import com.google.api.gax.retrying.RetrySettings;
40+
import com.google.cloud.ByteArray;
4041
import com.google.cloud.NoCredentials;
4142
import com.google.cloud.Timestamp;
4243
import com.google.cloud.spanner.AbstractResultSet.GrpcStreamIterator;
@@ -54,6 +55,7 @@
5455
import com.google.common.collect.ImmutableList;
5556
import com.google.common.util.concurrent.SettableFuture;
5657
import com.google.protobuf.AbstractMessage;
58+
import com.google.protobuf.ListValue;
5759
import com.google.spanner.v1.CommitRequest;
5860
import com.google.spanner.v1.DeleteSessionRequest;
5961
import com.google.spanner.v1.ExecuteBatchDmlRequest;
@@ -75,8 +77,10 @@
7577
import io.grpc.inprocess.InProcessServerBuilder;
7678
import java.io.IOException;
7779
import java.util.ArrayList;
80+
import java.util.Base64;
7881
import java.util.Collections;
7982
import java.util.List;
83+
import java.util.Random;
8084
import java.util.Set;
8185
import java.util.concurrent.ExecutionException;
8286
import java.util.concurrent.ExecutorService;
@@ -2381,4 +2385,51 @@ public void testAnalyzeUpdateStatement() {
23812385
ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
23822386
assertEquals(QueryMode.PLAN, request.getQueryMode());
23832387
}
2388+
2389+
@Test
2390+
public void testByteArray() {
2391+
Random random = new Random();
2392+
byte[] bytes = new byte[random.nextInt(20_000)];
2393+
int numRows = 10;
2394+
List<ListValue> rows = new ArrayList<>(numRows);
2395+
for (int i = 0; i < numRows; i++) {
2396+
random.nextBytes(bytes);
2397+
rows.add(
2398+
ListValue.newBuilder()
2399+
.addValues(
2400+
com.google.protobuf.Value.newBuilder()
2401+
.setStringValue(Base64.getEncoder().encodeToString(bytes))
2402+
.build())
2403+
.build());
2404+
}
2405+
Statement statement = Statement.of("select * from foo");
2406+
mockSpanner.putStatementResult(
2407+
StatementResult.query(
2408+
statement,
2409+
com.google.spanner.v1.ResultSet.newBuilder()
2410+
.setMetadata(
2411+
ResultSetMetadata.newBuilder()
2412+
.setRowType(
2413+
StructType.newBuilder()
2414+
.addFields(
2415+
Field.newBuilder()
2416+
.setType(Type.newBuilder().setCode(TypeCode.BYTES).build())
2417+
.setName("f1")
2418+
.build())
2419+
.build())
2420+
.build())
2421+
.addAllRows(rows)
2422+
.build()));
2423+
DatabaseClient client =
2424+
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
2425+
try (ResultSet resultSet = client.singleUse().executeQuery(statement)) {
2426+
while (resultSet.next()) {
2427+
String base64String = resultSet.getString(0);
2428+
ByteArray byteArray = resultSet.getBytes(0);
2429+
// Use the 'old' ByteArray.fromBase64(..) method that uses the Guava encoder to ensure that
2430+
// the two encoders (JDK and Guava) return the same values.
2431+
assertEquals(ByteArray.fromBase64(base64String), byteArray);
2432+
}
2433+
}
2434+
}
23842435
}

0 commit comments

Comments
 (0)