Skip to content

Commit e20e58c

Browse files
committed
xds: pre-parse custom metric names in WRR load balancer
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 - 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 7561d0b commit e20e58c

4 files changed

Lines changed: 202 additions & 117 deletions

File tree

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

Lines changed: 45 additions & 27 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;
@@ -50,7 +51,6 @@
5051
import java.util.HashSet;
5152
import java.util.List;
5253
import java.util.Objects;
53-
import java.util.OptionalDouble;
5454
import java.util.Random;
5555
import java.util.Set;
5656
import java.util.concurrent.ScheduledExecutorService;
@@ -239,7 +239,7 @@ protected void updateOverallBalancingState() {
239239
private SubchannelPicker createReadyPicker(Collection<ChildLbState> activeList) {
240240
WeightedRoundRobinPicker picker = new WeightedRoundRobinPicker(ImmutableList.copyOf(activeList),
241241
config.enableOobLoadReport, config.errorUtilizationPenalty, sequence,
242-
config.metricNamesForComputingUtilization);
242+
config.parsedMetricNamesForComputingUtilization);
243243
updateWeight(picker);
244244
return picker;
245245
}
@@ -329,15 +329,15 @@ public void addSubchannel(WrrSubchannel wrrSubchannel) {
329329
}
330330

331331
public OrcaReportListener getOrCreateOrcaListener(float errorUtilizationPenalty,
332-
ImmutableList<String> metricNamesForComputingUtilization) {
332+
ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization) {
333333
if (orcaReportListener != null
334334
&& orcaReportListener.errorUtilizationPenalty == errorUtilizationPenalty
335-
&& orcaReportListener.metricNamesForComputingUtilization
336-
.equals(metricNamesForComputingUtilization)) {
335+
&& orcaReportListener.parsedMetricNamesForComputingUtilization
336+
.equals(parsedMetricNamesForComputingUtilization)) {
337337
return orcaReportListener;
338338
}
339339
orcaReportListener =
340-
new OrcaReportListener(errorUtilizationPenalty, metricNamesForComputingUtilization);
340+
new OrcaReportListener(errorUtilizationPenalty, parsedMetricNamesForComputingUtilization);
341341
return orcaReportListener;
342342
}
343343

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

363363
final class OrcaReportListener implements OrcaPerRequestReportListener, OrcaOobReportListener {
364364
private final float errorUtilizationPenalty;
365-
private final ImmutableList<String> metricNamesForComputingUtilization;
365+
private final ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization;
366366

367367
OrcaReportListener(float errorUtilizationPenalty,
368-
ImmutableList<String> metricNamesForComputingUtilization) {
368+
ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization) {
369369
this.errorUtilizationPenalty = errorUtilizationPenalty;
370-
this.metricNamesForComputingUtilization = metricNamesForComputingUtilization;
370+
this.parsedMetricNamesForComputingUtilization = parsedMetricNamesForComputingUtilization;
371371
}
372372

373373
@Override
374374
public void onLoadReport(MetricReport report) {
375-
double utilization = getUtilization(report, metricNamesForComputingUtilization);
375+
double utilization = getUtilization(report);
376376

377377
double newWeight = 0;
378378
if (utilization > 0 && report.getQps() > 0) {
@@ -398,10 +398,10 @@ public void onLoadReport(MetricReport report) {
398398
* if application utilization is > 0, it is returned. If neither are present, the CPU
399399
* utilization is returned.
400400
*/
401-
private double getUtilization(MetricReport report, ImmutableList<String> metricNames) {
402-
OptionalDouble customUtil = getCustomMetricUtilization(report, metricNames);
403-
if (customUtil.isPresent()) {
404-
return customUtil.getAsDouble();
401+
private double getUtilization(MetricReport report) {
402+
double customUtil = getCustomMetricUtilization(report);
403+
if (customUtil >= 0) {
404+
return customUtil;
405405
}
406406
double appUtil = report.getApplicationUtilization();
407407
if (appUtil > 0) {
@@ -412,20 +412,23 @@ private double getUtilization(MetricReport report, ImmutableList<String> metricN
412412

413413
/**
414414
* Returns the maximum utilization value among the specified metric names.
415-
* Returns OptionalDouble.empty() if NONE of the specified metrics are present in the report,
415+
* Returns -1 if NONE of the specified metrics are present in the report,
416416
* or if all present metrics are NaN.
417-
* Returns OptionalDouble.of(maxUtil) if at least one non-NaN metric is present.
418417
*/
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();
418+
private double getCustomMetricUtilization(MetricReport report) {
419+
double max = -1.0;
420+
for (int i = 0; i < parsedMetricNamesForComputingUtilization.size(); i++) {
421+
double d = MetricReportUtils.getMetricValue(report,
422+
parsedMetricNamesForComputingUtilization.get(i));
423+
if (!Double.isNaN(d) && d > 0 && d > max) {
424+
max = d;
425+
}
426+
}
427+
return max;
427428
}
429+
428430
}
431+
429432
}
430433

431434
private final class UpdateWeightTask implements Runnable {
@@ -446,7 +449,7 @@ private void createAndApplyOrcaListeners() {
446449
if (config.enableOobLoadReport) {
447450
OrcaOobUtil.setListener(weightedSubchannel,
448451
wChild.getOrCreateOrcaListener(config.errorUtilizationPenalty,
449-
config.metricNamesForComputingUtilization),
452+
config.parsedMetricNamesForComputingUtilization),
450453
OrcaOobUtil.OrcaReportingConfig.newBuilder()
451454
.setReportInterval(config.oobReportingPeriodNanos, TimeUnit.NANOSECONDS).build());
452455
} else {
@@ -516,7 +519,7 @@ static final class WeightedRoundRobinPicker extends SubchannelPicker {
516519

517520
WeightedRoundRobinPicker(List<ChildLbState> children, boolean enableOobLoadReport,
518521
float errorUtilizationPenalty, AtomicInteger sequence,
519-
ImmutableList<String> metricNamesForComputingUtilization) {
522+
ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization) {
520523
checkNotNull(children, "children");
521524
Preconditions.checkArgument(!children.isEmpty(), "empty child list");
522525
this.children = children;
@@ -526,7 +529,7 @@ static final class WeightedRoundRobinPicker extends SubchannelPicker {
526529
WeightedChildLbState wChild = (WeightedChildLbState) child;
527530
pickers.add(wChild.getCurrentPicker());
528531
reportListeners.add(wChild.getOrCreateOrcaListener(errorUtilizationPenalty,
529-
metricNamesForComputingUtilization));
532+
parsedMetricNamesForComputingUtilization));
530533
}
531534
this.pickers = pickers;
532535
this.reportListeners = reportListeners;
@@ -768,6 +771,7 @@ static final class WeightedRoundRobinLoadBalancerConfig {
768771
final long weightUpdatePeriodNanos;
769772
final float errorUtilizationPenalty;
770773
final ImmutableList<String> metricNamesForComputingUtilization;
774+
final ImmutableList<ParsedMetricName> parsedMetricNamesForComputingUtilization;
771775

772776
public static Builder newBuilder() {
773777
return new Builder();
@@ -784,6 +788,20 @@ private WeightedRoundRobinLoadBalancerConfig(long blackoutPeriodNanos,
784788
this.weightUpdatePeriodNanos = weightUpdatePeriodNanos;
785789
this.errorUtilizationPenalty = errorUtilizationPenalty;
786790
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

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

Lines changed: 85 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
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.OptionalDouble;
21+
import java.util.Optional;
22+
2223

2324
/**
2425
* Utilities for parsing and resolving metrics from {@link MetricReport}.
@@ -27,41 +28,92 @@ public final class MetricReportUtils {
2728

2829
private MetricReportUtils() {}
2930

31+
public enum MetricType {
32+
CPU_UTILIZATION,
33+
APPLICATION_UTILIZATION,
34+
MEMORY_UTILIZATION,
35+
UTILIZATION,
36+
NAMED_METRICS,
37+
INVALID
38+
}
39+
40+
@AutoValue
41+
public abstract static class ParsedMetricName {
42+
public abstract MetricType getMetricType();
43+
44+
public abstract Optional<String> getKey();
45+
46+
public static ParsedMetricName create(MetricType metricType, Optional<String> key) {
47+
return new AutoValue_MetricReportUtils_ParsedMetricName(metricType, key);
48+
}
49+
50+
/**
51+
* Pre-parses a custom metric name into a {@link ParsedMetricName}.
52+
*
53+
* @param name The custom metric name to parse.
54+
* @return The parsed metric name.
55+
*/
56+
public static ParsedMetricName parse(String name) {
57+
if (name.equals("cpu_utilization")) {
58+
return create(MetricType.CPU_UTILIZATION, Optional.empty());
59+
}
60+
if (name.equals("application_utilization")) {
61+
return create(MetricType.APPLICATION_UTILIZATION, Optional.empty());
62+
}
63+
if (name.equals("mem_utilization")) {
64+
return create(MetricType.MEMORY_UTILIZATION, Optional.empty());
65+
}
66+
if (name.startsWith("utilization.")) {
67+
return create(MetricType.UTILIZATION, Optional.of(name.substring("utilization.".length())));
68+
}
69+
if (name.startsWith("named_metrics.")) {
70+
return create(MetricType.NAMED_METRICS,
71+
Optional.of(name.substring("named_metrics.".length())));
72+
}
73+
return create(MetricType.INVALID, Optional.empty());
74+
}
75+
76+
}
77+
3078
/**
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>
79+
* Resolves a custom metric value for `parsedMetric`
80+
* Returns -1.0 if the metric is absent or invalid.
4081
*
4182
* @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.
83+
* @param parsedMetric The parsed metric to lookup.
84+
* @return The metric value or -1.0 if absent.
4485
*/
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-
}
86+
87+
public static double getMetricValue(MetricReport report, ParsedMetricName parsedMetric) {
88+
switch (parsedMetric.getMetricType()) {
89+
case CPU_UTILIZATION:
90+
return report.getCpuUtilization();
91+
case APPLICATION_UTILIZATION:
92+
return report.getApplicationUtilization();
93+
case MEMORY_UTILIZATION:
94+
return report.getMemoryUtilization();
95+
case UTILIZATION:
96+
if (parsedMetric.getKey().isPresent()) {
97+
String key = parsedMetric.getKey().get();
98+
Double val = report.getUtilizationMetrics().get(key);
99+
if (val != null) {
100+
return val;
101+
}
102+
}
103+
return -1.0;
104+
case NAMED_METRICS:
105+
if (parsedMetric.getKey().isPresent()) {
106+
String key = parsedMetric.getKey().get();
107+
Double val = report.getNamedMetrics().get(key);
108+
if (val != null) {
109+
return val;
110+
}
111+
}
112+
return -1.0;
113+
case INVALID:
114+
return -1.0;
115+
default:
116+
return -1.0;
64117
}
65-
return OptionalDouble.empty();
66118
}
67119
}

0 commit comments

Comments
 (0)