Skip to content

Commit 9a8cce8

Browse files
authored
test: add a default benchmark for measuring baselines. (#2739)
* Adding a benchmark (along with usual considerations) for measuring the latencies of some Java Client APIs. For any new feature, we usually first define the baseline by running benchmarks (without the feature). * Adding few utility methods to measure the p99/p95/p50 latencies. * Added benchmarking best-practices to avoid hotspot (by randomising keys), avoid cold start, etc.
1 parent 2f6182e commit 9a8cce8

File tree

3 files changed

+474
-0
lines changed

3 files changed

+474
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2024 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.spanner;
18+
19+
import java.time.Duration;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.stream.Collectors;
24+
25+
public abstract class AbstractLatencyBenchmark {
26+
27+
/** Utility to print latency numbers. It computes metrics such as Average, P50, P95 and P99. */
28+
public void printResults(List<Duration> results) {
29+
if (results == null) {
30+
return;
31+
}
32+
List<Duration> orderedResults = new ArrayList<>(results);
33+
Collections.sort(orderedResults);
34+
System.out.println();
35+
System.out.printf("Total number of queries: %d\n", orderedResults.size());
36+
System.out.printf("Avg: %fs\n", avg(results));
37+
System.out.printf("P50: %fs\n", percentile(50, orderedResults));
38+
System.out.printf("P95: %fs\n", percentile(95, orderedResults));
39+
System.out.printf("P99: %fs\n", percentile(99, orderedResults));
40+
}
41+
42+
private double percentile(int percentile, List<Duration> orderedResults) {
43+
int index = percentile * orderedResults.size() / 100;
44+
Duration value = orderedResults.get(index);
45+
Double convertedValue = convertDurationToFractionInSeconds(value);
46+
return convertedValue;
47+
}
48+
49+
/** Returns the average duration in seconds from a list of duration values. */
50+
private double avg(List<Duration> results) {
51+
return results.stream()
52+
.collect(Collectors.averagingDouble(this::convertDurationToFractionInSeconds));
53+
}
54+
55+
private double convertDurationToFractionInSeconds(Duration duration) {
56+
long seconds = duration.getSeconds();
57+
long nanos = duration.getNano();
58+
double fraction = (double) nanos / 1_000_000_000;
59+
double value = seconds + fraction;
60+
return value;
61+
}
62+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2024 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.spanner;
18+
19+
import com.google.api.gax.rpc.ServerStream;
20+
import com.google.common.collect.ImmutableList;
21+
import com.google.common.util.concurrent.ListenableFuture;
22+
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
23+
import com.google.rpc.Code;
24+
import com.google.spanner.v1.BatchWriteResponse;
25+
import java.time.Duration;
26+
import java.util.ArrayList;
27+
import java.util.LinkedList;
28+
import java.util.List;
29+
import java.util.concurrent.Future;
30+
import java.util.concurrent.TimeUnit;
31+
import java.util.concurrent.TimeoutException;
32+
import org.junit.AfterClass;
33+
import org.junit.BeforeClass;
34+
import org.junit.Test;
35+
import org.junit.experimental.categories.Category;
36+
import org.junit.runner.RunWith;
37+
import org.junit.runners.JUnit4;
38+
39+
/**
40+
* Hosts a bunch of utility methods/scripts that can be used while performing benchmarks to load
41+
* data, report latency metrics, etc.
42+
*
43+
* <p>Table schema used here: CREATE TABLE FOO ( id INT64 NOT NULL, BAZ INT64, BAR INT64, ) PRIMARY
44+
* KEY(id);
45+
*/
46+
@Category(SlowTest.class)
47+
@RunWith(JUnit4.class)
48+
public class BenchmarkingUtilityScripts {
49+
50+
// TODO(developer): Add your values here for PROJECT_ID, INSTANCE_ID, DATABASE_ID
51+
// TODO(developer): By default these values are blank and should be replaced before a run.
52+
private static final String PROJECT_ID = "";
53+
private static final String INSTANCE_ID = "";
54+
private static final String DATABASE_ID = "";
55+
private static final String SERVER_URL = "https://staging-wrenchworks.sandbox.googleapis.com";
56+
private static DatabaseClient client;
57+
private static Spanner spanner;
58+
59+
@BeforeClass
60+
public static void beforeClass() {
61+
final SpannerOptions.Builder optionsBuilder =
62+
SpannerOptions.newBuilder()
63+
.setProjectId(PROJECT_ID)
64+
.setAutoThrottleAdministrativeRequests();
65+
if (!SERVER_URL.isEmpty()) {
66+
optionsBuilder.setHost(SERVER_URL);
67+
}
68+
final SpannerOptions options = optionsBuilder.build();
69+
spanner = options.getService();
70+
client = spanner.getDatabaseClient(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DATABASE_ID));
71+
72+
// Delete all existing data from the table
73+
client.write(ImmutableList.of(Mutation.delete("FOO", KeySet.all())));
74+
}
75+
76+
@AfterClass
77+
public static void afterClass() {
78+
spanner.close();
79+
}
80+
81+
/**
82+
* A utility which bulk inserts 10^6 records into the database in batches. The method assumes that
83+
* the instance/database/table is already created. It does not perform any admin operations.
84+
*
85+
* <p>Table schema used here: CREATE TABLE FOO ( id INT64 NOT NULL, BAZ INT64, BAR INT64, )
86+
* PRIMARY KEY(id);
87+
*/
88+
@Test
89+
public void bulkInsertTestData() {
90+
int key = 0;
91+
List<MutationGroup> mutationGroups = new ArrayList<>();
92+
for (int batch = 0; batch < 100; batch++) {
93+
List<Mutation> mutations = new LinkedList<>();
94+
for (int i = 0; i < 10000; i++) {
95+
mutations.add(
96+
Mutation.newInsertBuilder("FOO")
97+
.set("id")
98+
.to(key)
99+
.set("BAZ")
100+
.to(1)
101+
.set("BAR")
102+
.to(2)
103+
.build());
104+
key++;
105+
}
106+
mutationGroups.add(MutationGroup.of(mutations));
107+
}
108+
ServerStream<BatchWriteResponse> responses = client.batchWriteAtLeastOnce(mutationGroups);
109+
for (BatchWriteResponse response : responses) {
110+
if (response.getStatus().getCode() == Code.OK_VALUE) {
111+
System.out.printf(
112+
"Mutation group indexes %s have been applied with commit timestamp %s",
113+
response.getIndexesList(), response.getCommitTimestamp());
114+
} else {
115+
System.out.printf(
116+
"Mutation group indexes %s could not be applied with error code %s and "
117+
+ "error message %s",
118+
response.getIndexesList(),
119+
Code.forNumber(response.getStatus().getCode()),
120+
response.getStatus().getMessage());
121+
}
122+
}
123+
}
124+
125+
/** Collects all results from a collection of future objects. */
126+
public static List<Duration> collectResults(
127+
final ListeningScheduledExecutorService service,
128+
final List<ListenableFuture<List<Duration>>> results,
129+
final int numOperations,
130+
final Duration timeoutDuration)
131+
throws Exception {
132+
service.shutdown();
133+
if (!service.awaitTermination(timeoutDuration.toMinutes(), TimeUnit.MINUTES)) {
134+
throw new TimeoutException();
135+
}
136+
List<Duration> allResults = new ArrayList<>(numOperations);
137+
for (Future<List<Duration>> result : results) {
138+
allResults.addAll(result.get());
139+
}
140+
return allResults;
141+
}
142+
}

0 commit comments

Comments
 (0)