• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20015

13 Oct 2025 07:57AM UTC coverage: 88.57% (+0.02%) from 88.552%
#20015

push

github

web-flow
xds: ORCA to LRS propagation changes (#12203)

Implements gRFC A85 (https://github.com/grpc/proposal/pull/454).

34925 of 39432 relevant lines covered (88.57%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.5
/../xds/src/main/java/io/grpc/xds/client/LoadStatsManager2.java
1
/*
2
 * Copyright 2021 The gRPC Authors
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 io.grpc.xds.client;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20
import static com.google.common.base.Preconditions.checkState;
21

22
import com.google.common.annotations.VisibleForTesting;
23
import com.google.common.base.Stopwatch;
24
import com.google.common.base.Supplier;
25
import com.google.common.collect.Sets;
26
import io.grpc.Internal;
27
import io.grpc.Status;
28
import io.grpc.internal.GrpcUtil;
29
import io.grpc.xds.client.Stats.BackendLoadMetricStats;
30
import io.grpc.xds.client.Stats.ClusterStats;
31
import io.grpc.xds.client.Stats.DroppedRequests;
32
import io.grpc.xds.client.Stats.UpstreamLocalityStats;
33
import java.util.ArrayList;
34
import java.util.Collections;
35
import java.util.HashMap;
36
import java.util.HashSet;
37
import java.util.List;
38
import java.util.Map;
39
import java.util.Set;
40
import java.util.concurrent.ConcurrentHashMap;
41
import java.util.concurrent.ConcurrentMap;
42
import java.util.concurrent.TimeUnit;
43
import java.util.concurrent.atomic.AtomicLong;
44
import javax.annotation.Nullable;
45
import javax.annotation.concurrent.ThreadSafe;
46

47
/**
48
 * Manages client side traffic stats. Drop stats are maintained in cluster (with edsServiceName)
49
 * granularity and load stats (request counts) are maintained in locality granularity.
50
 */
51
@ThreadSafe
52
@Internal
53
public final class LoadStatsManager2 {
54
  // Recorders for drops of each cluster:edsServiceName.
55
  private final Map<String, Map<String, ReferenceCounted<ClusterDropStats>>> allDropStats =
1✔
56
      new HashMap<>();
57
  // Recorders for loads of each cluster:edsServiceName:locality.
58
  private final Map<String, Map<String,
1✔
59
      Map<Locality, ReferenceCounted<ClusterLocalityStats>>>> allLoadStats = new HashMap<>();
60
  private final Supplier<Stopwatch> stopwatchSupplier;
61
  public static boolean isEnabledOrcaLrsPropagation =
1✔
62
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_ORCA_LRS_PROPAGATION", false);
1✔
63

64
  @VisibleForTesting
65
  public LoadStatsManager2(Supplier<Stopwatch> stopwatchSupplier) {
1✔
66
    this.stopwatchSupplier = checkNotNull(stopwatchSupplier, "stopwatchSupplier");
1✔
67
  }
1✔
68

69
  /**
70
   * Gets or creates the stats object for recording drops for the specified cluster with
71
   * edsServiceName. The returned object is reference counted and the caller should use {@link
72
   * ClusterDropStats#release()} to release its <i>hard</i> reference when it is safe to discard
73
   * future stats for the cluster.
74
   */
75
  @VisibleForTesting
76
  public synchronized ClusterDropStats getClusterDropStats(
77
      String cluster, @Nullable String edsServiceName) {
78
    if (!allDropStats.containsKey(cluster)) {
1✔
79
      allDropStats.put(cluster, new HashMap<String, ReferenceCounted<ClusterDropStats>>());
1✔
80
    }
81
    Map<String, ReferenceCounted<ClusterDropStats>> perClusterCounters = allDropStats.get(cluster);
1✔
82
    if (!perClusterCounters.containsKey(edsServiceName)) {
1✔
83
      perClusterCounters.put(
1✔
84
          edsServiceName,
85
          ReferenceCounted.wrap(new ClusterDropStats(
1✔
86
              cluster, edsServiceName, stopwatchSupplier.get())));
1✔
87
    }
88
    ReferenceCounted<ClusterDropStats> ref = perClusterCounters.get(edsServiceName);
1✔
89
    ref.retain();
1✔
90
    return ref.get();
1✔
91
  }
92

93
  private synchronized void releaseClusterDropCounter(
94
      String cluster, @Nullable String edsServiceName) {
95
    checkState(allDropStats.containsKey(cluster)
1✔
96
            && allDropStats.get(cluster).containsKey(edsServiceName),
1✔
97
        "stats for cluster %s, edsServiceName %s do not exist", cluster, edsServiceName);
98
    ReferenceCounted<ClusterDropStats> ref = allDropStats.get(cluster).get(edsServiceName);
1✔
99
    ref.release();
1✔
100
  }
1✔
101

102
  /**
103
   * Gets or creates the stats object for recording loads for the specified locality (in the
104
   * specified cluster with edsServiceName) with the specified backend metric propagation
105
   * configuration. The returned object is reference counted and the caller should
106
   * use {@link ClusterLocalityStats#release} to release its <i>hard</i> reference
107
   * when it is safe to discard the future stats for the locality.
108
   */
109
  @VisibleForTesting
110
  public synchronized ClusterLocalityStats getClusterLocalityStats(
111
      String cluster, @Nullable String edsServiceName, Locality locality) {
112
    return getClusterLocalityStats(cluster, edsServiceName, locality, null);
1✔
113
  }
114

115
  public synchronized ClusterLocalityStats getClusterLocalityStats(
116
      String cluster, @Nullable String edsServiceName, Locality locality,
117
      @Nullable BackendMetricPropagation backendMetricPropagation) {
118
    if (!allLoadStats.containsKey(cluster)) {
1✔
119
      allLoadStats.put(
1✔
120
          cluster,
121
          new HashMap<String, Map<Locality, ReferenceCounted<ClusterLocalityStats>>>());
122
    }
123
    Map<String, Map<Locality, ReferenceCounted<ClusterLocalityStats>>> perClusterCounters =
1✔
124
        allLoadStats.get(cluster);
1✔
125
    if (!perClusterCounters.containsKey(edsServiceName)) {
1✔
126
      perClusterCounters.put(
1✔
127
          edsServiceName, new HashMap<Locality, ReferenceCounted<ClusterLocalityStats>>());
128
    }
129
    Map<Locality, ReferenceCounted<ClusterLocalityStats>> localityStats =
1✔
130
        perClusterCounters.get(edsServiceName);
1✔
131
    if (!localityStats.containsKey(locality)) {
1✔
132
      localityStats.put(
1✔
133
          locality,
134
          ReferenceCounted.wrap(new ClusterLocalityStats(cluster, edsServiceName,
1✔
135
              locality, stopwatchSupplier.get(), backendMetricPropagation)));
1✔
136
    }
137
    ReferenceCounted<ClusterLocalityStats> ref = localityStats.get(locality);
1✔
138
    ref.retain();
1✔
139
    return ref.get();
1✔
140
  }
141

142
  private synchronized void releaseClusterLocalityLoadCounter(
143
      String cluster, @Nullable String edsServiceName, Locality locality) {
144
    checkState(allLoadStats.containsKey(cluster)
1✔
145
            && allLoadStats.get(cluster).containsKey(edsServiceName)
1✔
146
            && allLoadStats.get(cluster).get(edsServiceName).containsKey(locality),
1✔
147
        "stats for cluster %s, edsServiceName %s, locality %s not exits",
148
        cluster, edsServiceName, locality);
149
    ReferenceCounted<ClusterLocalityStats> ref =
1✔
150
        allLoadStats.get(cluster).get(edsServiceName).get(locality);
1✔
151
    ref.release();
1✔
152
  }
1✔
153

154
  /**
155
   * Gets the traffic stats (drops and loads) as a list of {@link ClusterStats} recorded for the
156
   * specified cluster since the previous call of this method or {@link
157
   * #getAllClusterStatsReports}. A {@link ClusterStats} includes stats for a specific cluster with
158
   * edsServiceName.
159
   */
160
  public synchronized List<ClusterStats> getClusterStatsReports(String cluster) {
161
    if (!allDropStats.containsKey(cluster) && !allLoadStats.containsKey(cluster)) {
1✔
162
      return Collections.emptyList();
1✔
163
    }
164
    Map<String, ReferenceCounted<ClusterDropStats>> clusterDropStats = allDropStats.get(cluster);
1✔
165
    Map<String, Map<Locality, ReferenceCounted<ClusterLocalityStats>>> clusterLoadStats =
1✔
166
        allLoadStats.get(cluster);
1✔
167
    Map<String, ClusterStats.Builder> statsReportBuilders = new HashMap<>();
1✔
168
    // Populate drop stats.
169
    if (clusterDropStats != null) {
1✔
170
      Set<String> toDiscard = new HashSet<>();
1✔
171
      for (String edsServiceName : clusterDropStats.keySet()) {
1✔
172
        ClusterStats.Builder builder = ClusterStats.newBuilder().clusterName(cluster);
1✔
173
        if (edsServiceName != null) {
1✔
174
          builder.clusterServiceName(edsServiceName);
1✔
175
        }
176
        ReferenceCounted<ClusterDropStats> ref = clusterDropStats.get(edsServiceName);
1✔
177
        if (ref.getReferenceCount() == 0) {  // stats object no longer needed after snapshot
1✔
178
          toDiscard.add(edsServiceName);
1✔
179
        }
180
        ClusterDropStatsSnapshot dropStatsSnapshot = ref.get().snapshot();
1✔
181
        long totalCategorizedDrops = 0L;
1✔
182
        for (Map.Entry<String, Long> entry : dropStatsSnapshot.categorizedDrops.entrySet()) {
1✔
183
          builder.addDroppedRequests(DroppedRequests.create(entry.getKey(), entry.getValue()));
1✔
184
          totalCategorizedDrops += entry.getValue();
1✔
185
        }
1✔
186
        builder.totalDroppedRequests(
1✔
187
            totalCategorizedDrops + dropStatsSnapshot.uncategorizedDrops);
1✔
188
        builder.loadReportIntervalNano(dropStatsSnapshot.durationNano);
1✔
189
        statsReportBuilders.put(edsServiceName, builder);
1✔
190
      }
1✔
191
      clusterDropStats.keySet().removeAll(toDiscard);
1✔
192
    }
193
    // Populate load stats for all localities in the cluster.
194
    if (clusterLoadStats != null) {
1✔
195
      Set<String> toDiscard = new HashSet<>();
1✔
196
      for (String edsServiceName : clusterLoadStats.keySet()) {
1✔
197
        ClusterStats.Builder builder = statsReportBuilders.get(edsServiceName);
1✔
198
        if (builder == null) {
1✔
199
          builder = ClusterStats.newBuilder().clusterName(cluster);
1✔
200
          if (edsServiceName != null) {
1✔
201
            builder.clusterServiceName(edsServiceName);
1✔
202
          }
203
          statsReportBuilders.put(edsServiceName, builder);
1✔
204
        }
205
        Map<Locality, ReferenceCounted<ClusterLocalityStats>> localityStats =
1✔
206
            clusterLoadStats.get(edsServiceName);
1✔
207
        Set<Locality> localitiesToDiscard = new HashSet<>();
1✔
208
        for (Locality locality : localityStats.keySet()) {
1✔
209
          ReferenceCounted<ClusterLocalityStats> ref = localityStats.get(locality);
1✔
210
          ClusterLocalityStatsSnapshot snapshot = ref.get().snapshot();
1✔
211
          // Only discard stats object after all in-flight calls under recording had finished.
212
          if (ref.getReferenceCount() == 0 && snapshot.callsInProgress == 0) {
1✔
213
            localitiesToDiscard.add(locality);
1✔
214
          }
215
          UpstreamLocalityStats upstreamLocalityStats = UpstreamLocalityStats.create(
1✔
216
              locality, snapshot.callsIssued, snapshot.callsSucceeded, snapshot.callsFailed,
1✔
217
              snapshot.callsInProgress, snapshot.loadMetricStatsMap);
1✔
218
          builder.addUpstreamLocalityStats(upstreamLocalityStats);
1✔
219
          // Use the max (drops/loads) recording interval as the overall interval for the
220
          // cluster's stats. In general, they should be mostly identical.
221
          builder.loadReportIntervalNano(
1✔
222
              Math.max(builder.loadReportIntervalNano(), snapshot.durationNano));
1✔
223
        }
1✔
224
        localityStats.keySet().removeAll(localitiesToDiscard);
1✔
225
        if (localityStats.isEmpty()) {
1✔
226
          toDiscard.add(edsServiceName);
1✔
227
        }
228
      }
1✔
229
      clusterLoadStats.keySet().removeAll(toDiscard);
1✔
230
    }
231
    List<ClusterStats> res = new ArrayList<>();
1✔
232
    for (ClusterStats.Builder builder : statsReportBuilders.values()) {
1✔
233
      res.add(builder.build());
1✔
234
    }
1✔
235
    return Collections.unmodifiableList(res);
1✔
236
  }
237

238
  /**
239
   * Gets the traffic stats (drops and loads) as a list of {@link ClusterStats} recorded for all
240
   * clusters since the previous call of this method or {@link #getClusterStatsReports} for each
241
   * specific cluster. A {@link ClusterStats} includes stats for a specific cluster with
242
   * edsServiceName.
243
   */
244
  synchronized List<ClusterStats> getAllClusterStatsReports() {
245
    Set<String> allClusters = Sets.union(allDropStats.keySet(), allLoadStats.keySet());
1✔
246
    List<ClusterStats> res = new ArrayList<>();
1✔
247
    for (String cluster : allClusters) {
1✔
248
      res.addAll(getClusterStatsReports(cluster));
1✔
249
    }
1✔
250
    return Collections.unmodifiableList(res);
1✔
251
  }
252

253
  /**
254
   * Recorder for dropped requests. One instance per cluster with edsServiceName.
255
   */
256
  @ThreadSafe
257
  public final class ClusterDropStats {
258
    private final String clusterName;
259
    @Nullable
260
    private final String edsServiceName;
261
    private final AtomicLong uncategorizedDrops = new AtomicLong();
1✔
262
    private final ConcurrentMap<String, AtomicLong> categorizedDrops = new ConcurrentHashMap<>();
1✔
263
    private final Stopwatch stopwatch;
264

265
    private ClusterDropStats(
266
        String clusterName, @Nullable String edsServiceName, Stopwatch stopwatch) {
1✔
267
      this.clusterName = checkNotNull(clusterName, "clusterName");
1✔
268
      this.edsServiceName = edsServiceName;
1✔
269
      this.stopwatch = checkNotNull(stopwatch, "stopwatch");
1✔
270
      stopwatch.reset().start();
1✔
271
    }
1✔
272

273
    /**
274
     * Records a dropped request with the specified category.
275
     */
276
    public void recordDroppedRequest(String category) {
277
      // There is a race between this method and snapshot(), causing one drop recorded but may not
278
      // be included in any snapshot. This is acceptable and the race window is extremely small.
279
      AtomicLong counter = categorizedDrops.putIfAbsent(category, new AtomicLong(1L));
1✔
280
      if (counter != null) {
1✔
281
        counter.getAndIncrement();
1✔
282
      }
283
    }
1✔
284

285
    /**
286
     * Records a dropped request without category.
287
     */
288
    public void recordDroppedRequest() {
289
      uncategorizedDrops.getAndIncrement();
1✔
290
    }
1✔
291

292
    /**
293
     * Release the <i>hard</i> reference for this stats object (previously obtained via {@link
294
     * LoadStatsManager2#getClusterDropStats}). The object may still be recording
295
     * drops after this method, but there is no guarantee drops recorded after this point will
296
     * be included in load reports.
297
     */
298
    public void release() {
299
      LoadStatsManager2.this.releaseClusterDropCounter(clusterName, edsServiceName);
1✔
300
    }
1✔
301

302
    private ClusterDropStatsSnapshot snapshot() {
303
      Map<String, Long> drops = new HashMap<>();
1✔
304
      for (Map.Entry<String, AtomicLong> entry : categorizedDrops.entrySet()) {
1✔
305
        drops.put(entry.getKey(), entry.getValue().get());
1✔
306
      }
1✔
307
      categorizedDrops.clear();
1✔
308
      long duration = stopwatch.elapsed(TimeUnit.NANOSECONDS);
1✔
309
      stopwatch.reset().start();
1✔
310
      return new ClusterDropStatsSnapshot(drops, uncategorizedDrops.getAndSet(0), duration);
1✔
311
    }
312
  }
313

314
  private static final class ClusterDropStatsSnapshot {
315
    private final Map<String, Long> categorizedDrops;
316
    private final long uncategorizedDrops;
317
    private final long durationNano;
318

319
    private ClusterDropStatsSnapshot(
320
        Map<String, Long> categorizedDrops, long uncategorizedDrops, long durationNano) {
1✔
321
      this.categorizedDrops = Collections.unmodifiableMap(
1✔
322
          checkNotNull(categorizedDrops, "categorizedDrops"));
1✔
323
      this.uncategorizedDrops = uncategorizedDrops;
1✔
324
      this.durationNano = durationNano;
1✔
325
    }
1✔
326
  }
327

328
  /**
329
   * Recorder for client loads. One instance per locality (in cluster with edsService).
330
   */
331
  @ThreadSafe
332
  public final class ClusterLocalityStats {
333
    private final String clusterName;
334
    @Nullable
335
    private final String edsServiceName;
336
    private final Locality locality;
337
    private final Stopwatch stopwatch;
338
    @Nullable
339
    private final BackendMetricPropagation backendMetricPropagation;
340
    private final AtomicLong callsInProgress = new AtomicLong();
1✔
341
    private final AtomicLong callsSucceeded = new AtomicLong();
1✔
342
    private final AtomicLong callsFailed = new AtomicLong();
1✔
343
    private final AtomicLong callsIssued = new AtomicLong();
1✔
344
    private Map<String, BackendLoadMetricStats> loadMetricStatsMap = new HashMap<>();
1✔
345

346
    private ClusterLocalityStats(
347
        String clusterName, @Nullable String edsServiceName, Locality locality,
348
        Stopwatch stopwatch, BackendMetricPropagation backendMetricPropagation) {
1✔
349
      this.clusterName = checkNotNull(clusterName, "clusterName");
1✔
350
      this.edsServiceName = edsServiceName;
1✔
351
      this.locality = checkNotNull(locality, "locality");
1✔
352
      this.stopwatch = checkNotNull(stopwatch, "stopwatch");
1✔
353
      this.backendMetricPropagation = backendMetricPropagation;
1✔
354
      stopwatch.reset().start();
1✔
355
    }
1✔
356

357
    /**
358
     * Records a request being issued.
359
     */
360
    public void recordCallStarted() {
361
      callsIssued.getAndIncrement();
1✔
362
      callsInProgress.getAndIncrement();
1✔
363
    }
1✔
364

365
    /**
366
     * Records a request finished with the given status.
367
     */
368
    public void recordCallFinished(Status status) {
369
      callsInProgress.getAndDecrement();
1✔
370
      if (status.isOk()) {
1✔
371
        callsSucceeded.getAndIncrement();
1✔
372
      } else {
373
        callsFailed.getAndIncrement();
1✔
374
      }
375
    }
1✔
376

377
    /**
378
     * Records all custom named backend load metric stats for per-call load reporting. For each
379
     * metric key {@code name}, creates a new {@link BackendLoadMetricStats} with a finished
380
     * requests counter of 1 and the {@code value} if the key is not present in the map. Otherwise,
381
     * increments the finished requests counter and adds the {@code value} to the existing
382
     * {@link BackendLoadMetricStats}.
383
     * Metrics are filtered based on the backend metric propagation configuration if configured.
384
     */
385
    public synchronized void recordBackendLoadMetricStats(Map<String, Double> namedMetrics) {
386
      if (!isEnabledOrcaLrsPropagation) {
1✔
387
        namedMetrics.forEach((name, value) -> updateLoadMetricStats(name, value));
1✔
388
        return;
1✔
389
      }
390

391
      namedMetrics.forEach((name, value) -> {
1✔
392
        if (backendMetricPropagation.shouldPropagateNamedMetric(name)) {
1✔
393
          updateLoadMetricStats("named_metrics." + name, value);
1✔
394
        }
395
      });
1✔
396
    }
1✔
397

398
    private void updateLoadMetricStats(String metricName, double value) {
399
      if (!loadMetricStatsMap.containsKey(metricName)) {
1✔
400
        loadMetricStatsMap.put(metricName, new BackendLoadMetricStats(1, value));
1✔
401
      } else {
402
        loadMetricStatsMap.get(metricName).addMetricValueAndIncrementRequestsFinished(value);
1✔
403
      }
404
    }
1✔
405

406
    /**
407
     * Records top-level ORCA metrics (CPU, memory, application utilization) for per-call load
408
     * reporting. Metrics are filtered based on the backend metric propagation configuration
409
     * if configured.
410
     *
411
     * @param cpuUtilization CPU utilization metric value
412
     * @param memUtilization Memory utilization metric value
413
     * @param applicationUtilization Application utilization metric value
414
     */
415
    public synchronized void recordTopLevelMetrics(double cpuUtilization, double memUtilization,
416
        double applicationUtilization) {
417
      if (backendMetricPropagation.propagateCpuUtilization && cpuUtilization > 0) {
1✔
418
        updateLoadMetricStats("cpu_utilization", cpuUtilization);
1✔
419
      }
420
      if (backendMetricPropagation.propagateMemUtilization && memUtilization > 0) {
1✔
421
        updateLoadMetricStats("mem_utilization", memUtilization);
×
422
      }
423
      if (backendMetricPropagation.propagateApplicationUtilization && applicationUtilization > 0) {
1✔
424
        updateLoadMetricStats("application_utilization", applicationUtilization);
1✔
425
      }
426
    }
1✔
427

428
    /**
429
     * Release the <i>hard</i> reference for this stats object (previously obtained via {@link
430
     * LoadStatsManager2#getClusterLocalityStats}). The object may still be
431
     * recording loads after this method, but there is no guarantee loads recorded after this
432
     * point will be included in load reports.
433
     */
434
    public void release() {
435
      LoadStatsManager2.this.releaseClusterLocalityLoadCounter(
1✔
436
          clusterName, edsServiceName, locality);
437
    }
1✔
438

439
    private ClusterLocalityStatsSnapshot snapshot() {
440
      long duration = stopwatch.elapsed(TimeUnit.NANOSECONDS);
1✔
441
      stopwatch.reset().start();
1✔
442
      Map<String, BackendLoadMetricStats> loadMetricStatsMapCopy;
443
      synchronized (this) {
1✔
444
        loadMetricStatsMapCopy = Collections.unmodifiableMap(loadMetricStatsMap);
1✔
445
        loadMetricStatsMap = new HashMap<>();
1✔
446
      }
1✔
447
      return new ClusterLocalityStatsSnapshot(callsSucceeded.getAndSet(0), callsInProgress.get(),
1✔
448
          callsFailed.getAndSet(0), callsIssued.getAndSet(0), duration, loadMetricStatsMapCopy);
1✔
449
    }
450
  }
451

452
  private static final class ClusterLocalityStatsSnapshot {
453
    private final long callsSucceeded;
454
    private final long callsInProgress;
455
    private final long callsFailed;
456
    private final long callsIssued;
457
    private final long durationNano;
458
    private final Map<String, BackendLoadMetricStats> loadMetricStatsMap;
459

460
    private ClusterLocalityStatsSnapshot(
461
        long callsSucceeded, long callsInProgress, long callsFailed, long callsIssued,
462
        long durationNano, Map<String, BackendLoadMetricStats> loadMetricStatsMap) {
1✔
463
      this.callsSucceeded = callsSucceeded;
1✔
464
      this.callsInProgress = callsInProgress;
1✔
465
      this.callsFailed = callsFailed;
1✔
466
      this.callsIssued = callsIssued;
1✔
467
      this.durationNano = durationNano;
1✔
468
      this.loadMetricStatsMap = Collections.unmodifiableMap(
1✔
469
          checkNotNull(loadMetricStatsMap, "loadMetricStatsMap"));
1✔
470
    }
1✔
471
  }
472
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc