Skip to content

Commit 324fce7

Browse files
authored
xds: pre-parse custom metric names in WRR load balancer (grpc#12773)
Introduce ParsedMetricName in MetricReportUtils to pre-parse configured custom metric names into Enums and key Strings on config initialization in WeightedRoundRobinLoadBalancerConfig, avoiding String parsing operations in the data path. This has been done by a combination of a few things - Streams -> loop - OptionalDouble -> double : We decided to take a hit here because it provides semantic correctness over using sentinels. - Pre parsing instead of hot path substring OrcaReportListener now utilizes pre-parsed ParsedMetricName objects during getCustomMetricUtilization to prevent OptionalDouble heap allocations on the hot path. Updated test coverage in MetricReportUtilsTest and WeightedRoundRobinLoadBalancerTest. # JMH Benchmark Report: MetricReportUtils Optimization We performed a benchmark comparison of four different custom metric resolution implementations in the Weighted Round Robin (WRR) load balancer. ## Benchmark Results | Benchmark Variant | Average Latency | Normalized Heap Allocations | Speedup | | :------------------------------------ | :-------------- | :-------------------------- | :-------- | | **Baseline (`String` + Streams)** | 174.46 ns/op | 704.00 B/op | 1x | | **`ParsedMetricName` + Streams** | 148.95 ns/op | 608.00 B/op | ~1.1x | | **`String` + Loop** | 81.61 ns/op | 240.00 B/op | ~2.1x | | **`ParsedMetricName` + Loop** | 52.92 ns/op | 144.00 B/op | ~3.2x | | **`ParsedMetricName` + Unboxed Loop** | **43.76 ns/op** | **≈ 0.00 B/op** | **~4.0x** | ---
1 parent c27ab63 commit 324fce7

5 files changed

Lines changed: 212 additions & 111 deletions

File tree

xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import io.grpc.util.ForwardingSubchannel;
4242
import io.grpc.util.MultiChildLoadBalancer;
4343
import io.grpc.xds.internal.MetricReportUtils;
44+
import io.grpc.xds.internal.MetricReportUtils.ParsedMetricName;
4445
import io.grpc.xds.orca.OrcaOobUtil;
4546
import io.grpc.xds.orca.OrcaOobUtil.OrcaOobReportListener;
4647
import io.grpc.xds.orca.OrcaPerRequestUtil;
@@ -239,7 +240,7 @@ protected void updateOverallBalancingState() {
239240
private SubchannelPicker createReadyPicker(Collection<ChildLbState> activeList) {
240241
WeightedRoundRobinPicker picker = new WeightedRoundRobinPicker(ImmutableList.copyOf(activeList),
241242
config.enableOobLoadReport, config.errorUtilizationPenalty, sequence,
242-
config.metricNamesForComputingUtilization);
243+
config.parsedMetricNamesForComputingUtilization);
243244
updateWeight(picker);
244245
return picker;
245246
}
@@ -329,15 +330,15 @@ public void addSubchannel(WrrSubchannel wrrSubchannel) {
329330
}
330331

331332
public OrcaReportListener getOrCreateOrcaListener(float errorUtilizationPenalty,
332-
ImmutableList<String> metricNamesForComputingUtilization) {
333+
ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization) {
333334
if (orcaReportListener != null
334335
&& orcaReportListener.errorUtilizationPenalty == errorUtilizationPenalty
335-
&& orcaReportListener.metricNamesForComputingUtilization
336-
.equals(metricNamesForComputingUtilization)) {
336+
&& orcaReportListener.parsedMetricNamesForComputingUtilization
337+
.equals(parsedMetricNamesForComputingUtilization)) {
337338
return orcaReportListener;
338339
}
339340
orcaReportListener =
340-
new OrcaReportListener(errorUtilizationPenalty, metricNamesForComputingUtilization);
341+
new OrcaReportListener(errorUtilizationPenalty, parsedMetricNamesForComputingUtilization);
341342
return orcaReportListener;
342343
}
343344

@@ -362,17 +363,17 @@ public void updateBalancingState(ConnectivityState newState, SubchannelPicker ne
362363

363364
final class OrcaReportListener implements OrcaPerRequestReportListener, OrcaOobReportListener {
364365
private final float errorUtilizationPenalty;
365-
private final ImmutableList<String> metricNamesForComputingUtilization;
366+
private final ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization;
366367

367368
OrcaReportListener(float errorUtilizationPenalty,
368-
ImmutableList<String> metricNamesForComputingUtilization) {
369+
ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization) {
369370
this.errorUtilizationPenalty = errorUtilizationPenalty;
370-
this.metricNamesForComputingUtilization = metricNamesForComputingUtilization;
371+
this.parsedMetricNamesForComputingUtilization = parsedMetricNamesForComputingUtilization;
371372
}
372373

373374
@Override
374375
public void onLoadReport(MetricReport report) {
375-
double utilization = getUtilization(report, metricNamesForComputingUtilization);
376+
double utilization = getUtilization(report);
376377

377378
double newWeight = 0;
378379
if (utilization > 0 && report.getQps() > 0) {
@@ -398,8 +399,8 @@ public void onLoadReport(MetricReport report) {
398399
* if application utilization is > 0, it is returned. If neither are present, the CPU
399400
* utilization is returned.
400401
*/
401-
private double getUtilization(MetricReport report, ImmutableList<String> metricNames) {
402-
OptionalDouble customUtil = getCustomMetricUtilization(report, metricNames);
402+
private double getUtilization(MetricReport report) {
403+
OptionalDouble customUtil = getCustomMetricUtilization(report);
403404
if (customUtil.isPresent()) {
404405
return customUtil.getAsDouble();
405406
}
@@ -411,19 +412,23 @@ private double getUtilization(MetricReport report, ImmutableList<String> metricN
411412
}
412413

413414
/**
414-
* Returns the maximum utilization value among the specified metric names.
415+
* Returns the maximum utilization value among the parsed metric names.
415416
* Returns OptionalDouble.empty() if NONE of the specified metrics are present in the report,
416-
* or if all present metrics are NaN.
417-
* Returns OptionalDouble.of(maxUtil) if at least one non-NaN metric is present.
417+
* or if all present metrics are NaN or non positive.
418418
*/
419-
private OptionalDouble getCustomMetricUtilization(MetricReport report,
420-
ImmutableList<String> metricNames) {
421-
return metricNames.stream()
422-
.map(name -> MetricReportUtils.getMetric(report, name))
423-
.filter(OptionalDouble::isPresent)
424-
.mapToDouble(OptionalDouble::getAsDouble)
425-
.filter(d -> !Double.isNaN(d) && d > 0)
426-
.max();
419+
private OptionalDouble getCustomMetricUtilization(MetricReport report) {
420+
OptionalDouble max = OptionalDouble.empty();
421+
for (int i = 0; i < parsedMetricNamesForComputingUtilization.size(); i++) {
422+
OptionalDouble opt = MetricReportUtils.getMetricValue(report,
423+
parsedMetricNamesForComputingUtilization.get(i));
424+
if (opt.isPresent()) {
425+
double d = opt.getAsDouble();
426+
if (!Double.isNaN(d) && d > 0 && (!max.isPresent() || d > max.getAsDouble())) {
427+
max = opt;
428+
}
429+
}
430+
}
431+
return max;
427432
}
428433
}
429434
}
@@ -446,7 +451,7 @@ private void createAndApplyOrcaListeners() {
446451
if (config.enableOobLoadReport) {
447452
OrcaOobUtil.setListener(weightedSubchannel,
448453
wChild.getOrCreateOrcaListener(config.errorUtilizationPenalty,
449-
config.metricNamesForComputingUtilization),
454+
config.parsedMetricNamesForComputingUtilization),
450455
OrcaOobUtil.OrcaReportingConfig.newBuilder()
451456
.setReportInterval(config.oobReportingPeriodNanos, TimeUnit.NANOSECONDS).build());
452457
} else {
@@ -516,7 +521,7 @@ static final class WeightedRoundRobinPicker extends SubchannelPicker {
516521

517522
WeightedRoundRobinPicker(List<ChildLbState> children, boolean enableOobLoadReport,
518523
float errorUtilizationPenalty, AtomicInteger sequence,
519-
ImmutableList<String> metricNamesForComputingUtilization) {
524+
ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization) {
520525
checkNotNull(children, "children");
521526
Preconditions.checkArgument(!children.isEmpty(), "empty child list");
522527
this.children = children;
@@ -526,7 +531,7 @@ static final class WeightedRoundRobinPicker extends SubchannelPicker {
526531
WeightedChildLbState wChild = (WeightedChildLbState) child;
527532
pickers.add(wChild.getCurrentPicker());
528533
reportListeners.add(wChild.getOrCreateOrcaListener(errorUtilizationPenalty,
529-
metricNamesForComputingUtilization));
534+
parsedMetricNamesForComputingUtilization));
530535
}
531536
this.pickers = pickers;
532537
this.reportListeners = reportListeners;
@@ -767,7 +772,7 @@ static final class WeightedRoundRobinLoadBalancerConfig {
767772
final long oobReportingPeriodNanos;
768773
final long weightUpdatePeriodNanos;
769774
final float errorUtilizationPenalty;
770-
final ImmutableList<String> metricNamesForComputingUtilization;
775+
final ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization;
771776

772777
public static Builder newBuilder() {
773778
return new Builder();
@@ -783,7 +788,20 @@ private WeightedRoundRobinLoadBalancerConfig(long blackoutPeriodNanos,
783788
this.oobReportingPeriodNanos = oobReportingPeriodNanos;
784789
this.weightUpdatePeriodNanos = weightUpdatePeriodNanos;
785790
this.errorUtilizationPenalty = errorUtilizationPenalty;
786-
this.metricNamesForComputingUtilization = metricNamesForComputingUtilization;
791+
792+
ImmutableList.Builder<ParsedMetricName> builder = ImmutableList.builder();
793+
if (metricNamesForComputingUtilization != null) {
794+
for (int i = 0; i < metricNamesForComputingUtilization.size(); i++) {
795+
String metricName = metricNamesForComputingUtilization.get(i);
796+
ParsedMetricName parsed = MetricReportUtils.ParsedMetricName.parse(metricName);
797+
if (parsed.getMetricType() != MetricReportUtils.MetricType.INVALID) {
798+
builder.add(parsed);
799+
} else {
800+
log.log(Level.FINE, "Invalid custom metric name configured and ignored: " + metricName);
801+
}
802+
}
803+
}
804+
this.parsedMetricNamesForComputingUtilization = builder.build();
787805
}
788806

789807
@Override
@@ -799,15 +817,15 @@ public boolean equals(Object o) {
799817
&& this.weightUpdatePeriodNanos == that.weightUpdatePeriodNanos
800818
// Float.compare considers NaNs equal
801819
&& Float.compare(this.errorUtilizationPenalty, that.errorUtilizationPenalty) == 0
802-
&& Objects.equals(this.metricNamesForComputingUtilization,
803-
that.metricNamesForComputingUtilization);
820+
&& Objects.equals(this.parsedMetricNamesForComputingUtilization,
821+
that.parsedMetricNamesForComputingUtilization);
804822
}
805823

806824
@Override
807825
public int hashCode() {
808826
return Objects.hash(blackoutPeriodNanos, weightExpirationPeriodNanos, enableOobLoadReport,
809827
oobReportingPeriodNanos, weightUpdatePeriodNanos, errorUtilizationPenalty,
810-
metricNamesForComputingUtilization);
828+
parsedMetricNamesForComputingUtilization);
811829
}
812830

813831
static final class Builder {

xds/src/main/java/io/grpc/xds/internal/MetricReportUtils.java

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,52 +16,104 @@
1616

1717
package io.grpc.xds.internal;
1818

19+
import com.google.auto.value.AutoValue;
1920
import io.grpc.services.MetricReport;
20-
import java.util.Map;
21+
import java.util.Optional;
2122
import java.util.OptionalDouble;
2223

24+
2325
/**
2426
* Utilities for parsing and resolving metrics from {@link MetricReport}.
2527
*/
2628
public final class MetricReportUtils {
2729

2830
private MetricReportUtils() {}
2931

32+
public enum MetricType {
33+
CPU_UTILIZATION,
34+
APPLICATION_UTILIZATION,
35+
MEMORY_UTILIZATION,
36+
UTILIZATION,
37+
NAMED_METRICS,
38+
INVALID
39+
}
40+
41+
@AutoValue
42+
public abstract static class ParsedMetricName {
43+
public abstract MetricType getMetricType();
44+
45+
public abstract Optional<String> getKey();
46+
47+
public static ParsedMetricName create(MetricType metricType, Optional<String> key) {
48+
return new AutoValue_MetricReportUtils_ParsedMetricName(metricType, key);
49+
}
50+
51+
/**
52+
* Pre-parses a custom metric name into a {@link ParsedMetricName}.
53+
*
54+
* @param name The custom metric name to parse.
55+
* @return The parsed metric name.
56+
*/
57+
public static ParsedMetricName parse(String name) {
58+
if (name.equals("cpu_utilization")) {
59+
return create(MetricType.CPU_UTILIZATION, Optional.empty());
60+
}
61+
if (name.equals("application_utilization")) {
62+
return create(MetricType.APPLICATION_UTILIZATION, Optional.empty());
63+
}
64+
if (name.equals("mem_utilization")) {
65+
return create(MetricType.MEMORY_UTILIZATION, Optional.empty());
66+
}
67+
if (name.startsWith("utilization.")) {
68+
return create(MetricType.UTILIZATION, Optional.of(name.substring("utilization.".length())));
69+
}
70+
if (name.startsWith("named_metrics.")) {
71+
return create(MetricType.NAMED_METRICS,
72+
Optional.of(name.substring("named_metrics.".length())));
73+
}
74+
return create(MetricType.INVALID, Optional.empty());
75+
}
76+
77+
}
78+
3079
/**
31-
* Resolves a metric value from the report based on the given metric name.
32-
* The logic checks for specific prefixes to determine where to look up the metric:
33-
* <ul>
34-
* <li>"cpu_utilization" -> getCpuUtilization()</li>
35-
* <li>"application_utilization" -> getApplicationUtilization()</li>
36-
* <li>"mem_utilization" -> getMemoryUtilization()</li>
37-
* <li>"utilization." -> lookup in utilizationMetrics</li>
38-
* <li>"named_metrics." -> lookup in namedMetrics</li>
39-
* </ul>
80+
* Resolves a custom metric value for `parsedMetric`
81+
* Returns OptionalDouble.empty() if the metric is absent or invalid.
4082
*
4183
* @param report The metric report to query.
42-
* @param metricName The name of the custom metric to look up.
43-
* @return The value of the metric if found, or empty if not found.
84+
* @param parsedMetric The parsed metric to lookup.
85+
* @return The metric value wrapped in an OptionalDouble, or empty if absent.
4486
*/
45-
public static OptionalDouble getMetric(MetricReport report, String metricName) {
46-
if (metricName.equals("cpu_utilization")) {
47-
return OptionalDouble.of(report.getCpuUtilization());
48-
} else if (metricName.equals("application_utilization")) {
49-
return OptionalDouble.of(report.getApplicationUtilization());
50-
} else if (metricName.equals("mem_utilization")) {
51-
return OptionalDouble.of(report.getMemoryUtilization());
52-
} else if (metricName.startsWith("utilization.")) {
53-
Map<String, Double> map = report.getUtilizationMetrics();
54-
Double val = map.get(metricName.substring("utilization.".length()));
55-
if (val != null) {
56-
return OptionalDouble.of(val);
57-
}
58-
} else if (metricName.startsWith("named_metrics.")) {
59-
Map<String, Double> map = report.getNamedMetrics();
60-
Double val = map.get(metricName.substring("named_metrics.".length()));
61-
if (val != null) {
62-
return OptionalDouble.of(val);
63-
}
87+
88+
public static OptionalDouble getMetricValue(MetricReport report, ParsedMetricName parsedMetric) {
89+
switch (parsedMetric.getMetricType()) {
90+
case CPU_UTILIZATION:
91+
return OptionalDouble.of(report.getCpuUtilization());
92+
case APPLICATION_UTILIZATION:
93+
return OptionalDouble.of(report.getApplicationUtilization());
94+
case MEMORY_UTILIZATION:
95+
return OptionalDouble.of(report.getMemoryUtilization());
96+
case UTILIZATION:
97+
if (parsedMetric.getKey().isPresent()) {
98+
String key = parsedMetric.getKey().get();
99+
Double val = report.getUtilizationMetrics().get(key);
100+
if (val != null) {
101+
return OptionalDouble.of(val);
102+
}
103+
}
104+
return OptionalDouble.empty();
105+
case NAMED_METRICS:
106+
if (parsedMetric.getKey().isPresent()) {
107+
String key = parsedMetric.getKey().get();
108+
Double val = report.getNamedMetrics().get(key);
109+
if (val != null) {
110+
return OptionalDouble.of(val);
111+
}
112+
}
113+
return OptionalDouble.empty();
114+
case INVALID:
115+
default:
116+
return OptionalDouble.empty();
64117
}
65-
return OptionalDouble.empty();
66118
}
67119
}

xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerProviderTest.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.grpc.internal.FakeClock;
3030
import io.grpc.internal.JsonParser;
3131
import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig;
32+
import io.grpc.xds.internal.MetricReportUtils.ParsedMetricName;
3233
import java.io.IOException;
3334
import java.util.Map;
3435
import org.junit.Test;
@@ -112,16 +113,19 @@ public void parseLoadBalancingConfigDefaultValues() throws IOException {
112113
}
113114

114115
@Test
115-
public void parseLoadBalancingConfigCustomMetrics() throws IOException {
116+
public void parseLoadBalancingConfigCustomMetricsIgnoresInvalid() throws IOException {
116117
System.setProperty("GRPC_EXPERIMENTAL_WRR_CUSTOM_METRICS", "true");
117118
try {
118-
String lbConfig = "{\"metricNamesForComputingUtilization\" : [\"foo\", \"bar\"]}";
119+
String lbConfig =
120+
"{\"metricNamesForComputingUtilization\" : "
121+
+ "[\"utilization.foo\", \"invalid_name\", \"named_metrics.bar\"]}";
119122
ConfigOrError configOrError = provider.parseLoadBalancingPolicyConfig(
120123
parseJsonObject(lbConfig));
121124
assertThat(configOrError.getConfig()).isNotNull();
122125
WeightedRoundRobinLoadBalancerConfig config =
123126
(WeightedRoundRobinLoadBalancerConfig) configOrError.getConfig();
124-
assertThat(config.metricNamesForComputingUtilization).containsExactly("foo", "bar");
127+
assertThat(config.parsedMetricNamesForComputingUtilization).containsExactly(
128+
ParsedMetricName.parse("utilization.foo"), ParsedMetricName.parse("named_metrics.bar"));
125129
} finally {
126130
System.clearProperty("GRPC_EXPERIMENTAL_WRR_CUSTOM_METRICS");
127131
}

0 commit comments

Comments
 (0)