Repository navigation
Conversation
Keep the change focused on route config precomputation and its unit test. Preserve benchmark source and results in the PR discussion instead of adding JMH build configuration to the xds module.
|
Benchmark evidence for the per-route config precomputation. The benchmark and its JMH build configuration are excluded from the final PR diff; the source and reproduction instructions are preserved here. Environment: Apple M3 Pro, macOS 15.6.1, JDK 17.0.17, JMH 1.36. Single thread, 2 forks, 3 × 1-second warmup iterations, 5 × 1-second measurement iterations, 256 MB heap, GC profiler enabled.
For timeout + retry, this removes approximately 0.78 μs and 3,472 bytes of allocation per config-handling operation in this microbenchmark. This isolates config generation/parsing versus reading an already parsed object: it does not measure the complete Benchmark code ( /*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.xds;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.util.Durations;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.Status.Code;
import io.grpc.internal.AutoConfiguredLoadBalancerFactory;
import io.grpc.internal.ScParser;
import io.grpc.xds.VirtualHost.Route.RouteAction;
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
/**
* Compares per-RPC method config generation/parsing with reading the precomputed route config.
* This isolates config handling; it does not measure routing, interceptors, or transport latency.
* Run with JMH's {@code -prof gc} to compare allocation per operation.
*/
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class XdsRouteServiceConfigBenchmark {
@Param({"EMPTY", "TIMEOUT", "RETRY", "TIMEOUT_RETRY"})
public String configType;
private ServiceConfigParser parser;
private Long timeoutNanos;
private RetryPolicy retryPolicy;
private XdsNameResolver.RouteData route;
@Setup
public void setUp() {
parser = new ScParser(true, 5, 5, new AutoConfiguredLoadBalancerFactory("pick_first"));
timeoutNanos = configType.contains("TIMEOUT") ? TimeUnit.SECONDS.toNanos(5) : null;
retryPolicy = configType.contains("RETRY")
? RetryPolicy.create(3, ImmutableList.of(Code.UNAVAILABLE),
Durations.fromMillis(100), Durations.fromSeconds(1), null)
: null;
route = new XdsNameResolver.RouteData(
RouteMatch.withPathExactOnly("/service/method"),
RouteAction.forCluster("cluster", ImmutableList.of(), timeoutNanos, retryPolicy, false),
new XdsNameResolver.PassthroughClientInterceptor(),
parser.parseServiceConfig(
XdsNameResolver.generateServiceConfigWithMethodConfig(timeoutNanos, retryPolicy)));
if (route.parsedServiceConfig.getError() != null) {
throw new IllegalStateException(route.parsedServiceConfig.getError().toString());
}
}
@Benchmark
public Object generateAndParsePerCall() {
return parser.parseServiceConfig(
XdsNameResolver.generateServiceConfigWithMethodConfig(timeoutNanos, retryPolicy))
.getConfig();
}
@Benchmark
public Object precomputedConfig() {
return route.parsedServiceConfig.getConfig();
}
}To run locally, add the code above and these entries to the existing plugins {
id "me.champeau.jmh"
}
dependencies {
jmhCompileOnly sourceSets.thirdparty.output
}./gradlew -PskipAndroid=true -PskipCodegen=true :grpc-xds:jmhJar --console=plain
java -jar xds/build/libs/grpc-xds-1.86.0-SNAPSHOT-jmh.jar XdsRouteServiceConfigBenchmark \
-wi 3 -w 1s -i 5 -r 1s -f 2 -t 1 -prof gc \
-jvmArgs '-Xms256m -Xmx256m' \
-rf json -rff route-service-config-jmh.json |
XdsNameResolver.selectConfig()currently generates and parses the method service config for every RPC, even when the selected route's timeout and retry policy have not changed.Move this work to
updateRoutes()and store the resultingConfigOrErrorinRouteData. RPCs selecting the same route reuse the parsed config until the routing snapshot is replaced. Each rebuilt snapshot uses the current route timeout, default timeout and retry policy. Parse failures are retained and returned when the affected route is selected, preserving the existing error handling and cluster release behavior.Add a unit test verifying that repeated selections of the same route reuse the same config instance. The final diff contains only the resolver implementation and its unit test.