This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/master by this push:
new 0cec0de68b Fix: `avgHistogramPercentile` / `sumHistogramPercentile`
meter functions reported the smallest finite bucket boundary (#13866)
0cec0de68b is described below
commit 0cec0de68b6767a5328418ecf1115d09458517d7
Author: Wan Kai <[email protected]>
AuthorDate: Sat May 9 23:25:39 2026 +0800
Fix: `avgHistogramPercentile` / `sumHistogramPercentile` meter functions
reported the smallest finite bucket boundary (#13866)
---
docs/en/changes/changes.md | 1 +
.../avg/AvgHistogramPercentileFunction.java | 17 ++++---
.../sum/SumHistogramPercentileFunction.java | 17 ++++---
.../avg/AvgHistogramPercentileFunctionTest.java | 53 ++++++++++++++++++++++
.../sum/SumHistogramPercentileFunctionTest.java | 53 ++++++++++++++++++++++
5 files changed, 127 insertions(+), 14 deletions(-)
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index e4d7f2debd..c0d15e5a5e 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -153,6 +153,7 @@
* Fix: `envoy-ai-gateway` metrics rules, make the metrics value return `0`
when the divisor is `0`.
* Custom `Layer`s can be declared without modifying the OAP source — via an
operator-managed `layer-extensions.yml`, inline `layerDefinitions:` block in a
MAL or LAL rule file, or a plugin extension. UI dashboard templates for new
layers are auto-discovered from the `ui-initialized-templates/` directory.
Recommended ordinal range for external layers is `>= 1000`; conflicting names
or ordinals are reported at boot.
* LAL: support full arithmetic (`+`, `-`, `*`, `/`) on numeric operands and
fix the original bug where `(tag("x") as Integer) + (tag("y") as Integer)` was
treated as string concatenation — expressions like `input_tokens +
output_tokens < 10000` produced the concatenated string `"2589115"` rather than
the integer sum `2704`, so token-threshold conditions never triggered `abort
{}`. Operand types are now inferred from explicit casts (`as Integer` / `as
Long` / `as Float` / `as Double`), ty [...]
+* Fix: `avgHistogramPercentile` / `sumHistogramPercentile` meter functions
reported the smallest finite bucket boundary (e.g. `10` for OTel
`gen_ai_server_request_duration` whose `le` is rewritten from `0.01s` → `10ms`)
for every rank when no samples were observed in any bucket. The percentile
loop's `count >= roof` check matched on the first sorted bucket because both
sides were `0`. `calculate()` now short-circuits to `0` for every rank when the
windowed total is `0`.
#### UI
* Add mobile menu icon and i18n labels for the iOS layer.
diff --git
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgHistogramPercentileFunction.java
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgHistogramPercentileFunction.java
index 7716febfe1..c018a2e7c1 100644
---
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgHistogramPercentileFunction.java
+++
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgHistogramPercentileFunction.java
@@ -235,6 +235,14 @@ public abstract class AvgHistogramPercentileFunction
extends Meter implements Ac
long total;
total = subDataset.sumOfValues();
+ if (total <= 0) {
+ for (int rankIdx = 0; rankIdx < ranks.size();
rankIdx++) {
+ labels.put(PERCENTILE_LABEL_NAME,
String.valueOf(ranks.get(rankIdx)));
+ percentileValues.put(labels, 0L);
+ }
+ return;
+ }
+
int[] roofs = new int[ranks.size()];
for (int i = 0; i < ranks.size(); i++) {
roofs[i] = Math.round(total * ranks.get(i) * 1.0f /
100);
@@ -253,13 +261,8 @@ public abstract class AvgHistogramPercentileFunction
extends Meter implements Ac
int roof = roofs[rankIdx];
if (count >= roof) {
- if (labels.isEmpty()) {
- labels.put(PERCENTILE_LABEL_NAME,
String.valueOf(ranks.get(rankIdx)));
- percentileValues.put(labels,
Long.parseLong(key));
- } else {
- labels.put(PERCENTILE_LABEL_NAME,
String.valueOf(ranks.get(rankIdx)));
- percentileValues.put(labels,
Long.parseLong(key));
- }
+ labels.put(PERCENTILE_LABEL_NAME,
String.valueOf(ranks.get(rankIdx)));
+ percentileValues.put(labels,
Long.parseLong(key));
loopIndex++;
} else {
break;
diff --git
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumHistogramPercentileFunction.java
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumHistogramPercentileFunction.java
index ac2f4c7d05..5d94a5f55f 100644
---
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumHistogramPercentileFunction.java
+++
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumHistogramPercentileFunction.java
@@ -201,6 +201,14 @@ public abstract class SumHistogramPercentileFunction
extends Meter implements Ac
long total;
total = subDataset.sumOfValues();
+ if (total <= 0) {
+ for (int rankIdx = 0; rankIdx < ranks.size();
rankIdx++) {
+ labels.put(PERCENTILE_LABEL_NAME,
String.valueOf(ranks.get(rankIdx)));
+ percentileValues.put(labels, 0L);
+ }
+ return;
+ }
+
int[] roofs = new int[ranks.size()];
for (int i = 0; i < ranks.size(); i++) {
roofs[i] = Math.round(total * ranks.get(i) * 1.0f /
100);
@@ -219,13 +227,8 @@ public abstract class SumHistogramPercentileFunction
extends Meter implements Ac
int roof = roofs[rankIdx];
if (count >= roof) {
- if (labels.isEmpty()) {
- labels.put(PERCENTILE_LABEL_NAME,
String.valueOf(ranks.get(rankIdx)));
- percentileValues.put(labels,
Long.parseLong(key));
- } else {
- labels.put(PERCENTILE_LABEL_NAME,
String.valueOf(ranks.get(rankIdx)));
- percentileValues.put(labels,
Long.parseLong(key));
- }
+ labels.put(PERCENTILE_LABEL_NAME,
String.valueOf(ranks.get(rankIdx)));
+ percentileValues.put(labels,
Long.parseLong(key));
loopIndex++;
} else {
break;
diff --git
a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgHistogramPercentileFunctionTest.java
b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgHistogramPercentileFunctionTest.java
index 60a88f774a..17cdb1ebd6 100644
---
a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgHistogramPercentileFunctionTest.java
+++
b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/avg/AvgHistogramPercentileFunctionTest.java
@@ -255,6 +255,59 @@ public class AvgHistogramPercentileFunctionTest {
);
}
+ @Test
+ public void testFunctionWithNoData() {
+ PercentileFunctionInst inst = new PercentileFunctionInst();
+ inst.accept(
+ MeterEntity.newService("service-test", Layer.GENERAL),
+ new PercentileArgument(
+ new BucketedValues(
+ BUCKETS,
+ new long[] {
+ 0,
+ 0,
+ 0,
+ 0
+ }
+ ),
+ RANKS
+ )
+ );
+
+ inst.calculate();
+ // No samples observed in any bucket — every rank should report 0
+ // rather than collapsing to the smallest bucket boundary.
+ Assertions.assertEquals(new DataTable("{p=50},0|{p=90},0"),
inst.getValue());
+ }
+
+ @Test
+ public void testFunctionWithNoDataAndLabels() {
+ BucketedValues bucketedValues = new BucketedValues(
+ BUCKETS,
+ new long[] {
+ 0,
+ 0,
+ 0,
+ 0
+ }
+ );
+ bucketedValues.getLabels().put("service_name", "ai-gateway");
+ PercentileFunctionInst inst = new PercentileFunctionInst();
+ inst.accept(
+ MeterEntity.newService("service-test", Layer.GENERAL),
+ new PercentileArgument(
+ bucketedValues,
+ RANKS
+ )
+ );
+
+ inst.calculate();
+ Assertions.assertEquals(
+ new
DataTable("{service_name=ai-gateway,p=50},0|{service_name=ai-gateway,p=90},0"),
+ inst.getValue()
+ );
+ }
+
private static class PercentileFunctionInst extends
AvgHistogramPercentileFunction {
@Override
public AcceptableValue<PercentileArgument> createNew() {
diff --git
a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumHistogramPercentileFunctionTest.java
b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumHistogramPercentileFunctionTest.java
index 22f89ca29b..e7bd56da5a 100644
---
a/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumHistogramPercentileFunctionTest.java
+++
b/oap-server/server-core/src/test/java/org/apache/skywalking/oap/server/core/analysis/meter/function/sum/SumHistogramPercentileFunctionTest.java
@@ -245,6 +245,59 @@ public class SumHistogramPercentileFunctionTest {
);
}
+ @Test
+ public void testFunctionWithNoData() {
+ PercentileFunctionInst inst = new PercentileFunctionInst();
+ inst.accept(
+ MeterEntity.newService("service-test", Layer.GENERAL),
+ new PercentileArgument(
+ new BucketedValues(
+ BUCKETS,
+ new long[] {
+ 0,
+ 0,
+ 0,
+ 0
+ }
+ ),
+ RANKS
+ )
+ );
+
+ inst.calculate();
+ // No samples observed in any bucket — every rank should report 0
+ // rather than collapsing to the smallest bucket boundary.
+ Assertions.assertEquals(new DataTable("{p=50},0|{p=90},0"),
inst.getValue());
+ }
+
+ @Test
+ public void testFunctionWithNoDataAndLabels() {
+ BucketedValues bucketedValues = new BucketedValues(
+ BUCKETS,
+ new long[] {
+ 0,
+ 0,
+ 0,
+ 0
+ }
+ );
+ bucketedValues.getLabels().put("service_name", "ai-gateway");
+ PercentileFunctionInst inst = new PercentileFunctionInst();
+ inst.accept(
+ MeterEntity.newService("service-test", Layer.GENERAL),
+ new PercentileArgument(
+ bucketedValues,
+ RANKS
+ )
+ );
+
+ inst.calculate();
+ Assertions.assertEquals(
+ new
DataTable("{service_name=ai-gateway,p=50},0|{service_name=ai-gateway,p=90},0"),
+ inst.getValue()
+ );
+ }
+
private static class PercentileFunctionInst extends
SumHistogramPercentileFunction {
@Override
public AcceptableValue<PercentileArgument> createNew() {