Skip to content

xds: Precompute per-route service configs on routing updates - #13183

Open
JoeCqupt wants to merge 2 commits into
grpc:masterfrom
JoeCqupt:feat/xds-route-precompute
Open

JoeCqupt wants to merge 2 commits into
grpc:masterfrom
JoeCqupt:feat/xds-route-precompute

Conversation

@JoeCqupt

@JoeCqupt JoeCqupt commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

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 resulting ConfigOrError in RouteData. 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.

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.
@JoeCqupt

JoeCqupt commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor Author

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.

Configuration Generate and parse (ns/op) Read precomputed config (ns/op) Allocation before → after (B/op)
EMPTY 15.29 0.61 328 → ≈0
TIMEOUT 128.01 0.63 1,000 → ≈0
RETRY 651.39 0.68 3,160 → ≈0
TIMEOUT_RETRY 778.64 0.67 3,472 → ≈0

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 selectConfig() path or end-to-end RPC latency. Snapshot construction costs and retained config memory are not measured. Parsing occurs once per forwarding route per snapshot rebuild.

Benchmark code (xds/src/jmh/java/io/grpc/xds/XdsRouteServiceConfigBenchmark.java):

/*
 * 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 and dependencies blocks in xds/build.gradle:

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant