victoria-metrics-0002: add Java unit test for MetricName tag-filter O(T×I) defect

This commit is contained in:
russell@unturf.com 2026-03-29 22:26:59 -04:00
parent a922a7ee9d
commit 657dac6c22

View file

@ -0,0 +1,179 @@
package unit;
import java.util.*;
/**
* VictoriaMetrics0002MetricNameTagFilterTest CWE-407 test for victoria-metrics-0002
*
* Models RemoveTagsOn / RemoveTagsIgnoring / SetTags in lib/storage/metric_name.go:
* slow: hasTag(tags []string, key) O(I) linear scan per metric tag, O(T×I) total
* fast: build map[string]struct{} once O(I) build, then O(T) lookups
*
* Called on every PromQL binary op with on(...)/ignoring(...), every aggregation
* with by(...)/without(...), every label_keep/label_del transform.
*
* Test: for T metric tags and I filter labels, slow does T*I ops per call;
* fast does I (build) + T (lookups) per call. Ratio must be >= 5x at T=50, I=25.
*/
public class VictoriaMetrics0002MetricNameTagFilterTest {
// --- SLOW: O(T×I) per call ---
// hasTag(tags []string, key []byte) linear scan over filter labels for each tag
static class SlowRemoveTags {
long comparisons = 0;
// hasTag: O(I) scan
boolean hasTag(List<String> filterLabels, String key) {
for (String t : filterLabels) {
comparisons++;
if (t.equals(key)) return true;
}
return false;
}
// RemoveTagsOn: keep only tags in onLabels O(T×I)
List<String> removeTagsOn(List<String> metricTags, List<String> onLabels) {
List<String> result = new ArrayList<>();
for (String tag : metricTags) {
if (hasTag(onLabels, tag)) {
result.add(tag);
}
}
return result;
}
// RemoveTagsIgnoring: keep tags NOT in ignoringLabels O(T×I)
List<String> removeTagsIgnoring(List<String> metricTags, List<String> ignoringLabels) {
List<String> result = new ArrayList<>();
for (String tag : metricTags) {
if (!hasTag(ignoringLabels, tag)) {
result.add(tag);
}
}
return result;
}
}
// --- FAST: O(I + T) per call ---
// Build map[string]struct{} once, then O(1) lookups
static class FastRemoveTags {
long ops = 0;
// RemoveTagsOn: build set from onLabels first O(I + T)
List<String> removeTagsOn(List<String> metricTags, List<String> onLabels) {
// Build set O(I)
Set<String> onSet = new HashSet<>();
for (String t : onLabels) {
onSet.add(t);
ops++;
}
// Filter O(T)
List<String> result = new ArrayList<>();
for (String tag : metricTags) {
ops++;
if (onSet.contains(tag)) {
result.add(tag);
}
}
return result;
}
// RemoveTagsIgnoring: build set from ignoringLabels O(I + T)
List<String> removeTagsIgnoring(List<String> metricTags, List<String> ignoringLabels) {
// Build set O(I)
Set<String> ignoreSet = new HashSet<>();
for (String t : ignoringLabels) {
ignoreSet.add(t);
ops++;
}
// Filter O(T)
List<String> result = new ArrayList<>();
for (String tag : metricTags) {
ops++;
if (!ignoreSet.contains(tag)) {
result.add(tag);
}
}
return result;
}
}
// Build N series each with T metric tags, call removeTagsOn N times
static long[] simulate(int numSeries, int numTags, int numFilterLabels, boolean fast) {
List<String> metricTags = new ArrayList<>();
for (int i = 0; i < numTags; i++) metricTags.add("label_" + i);
List<String> filterLabels = new ArrayList<>();
// filterLabels are the last half worst case for slow: always scan to end
for (int i = numTags / 2; i < numTags / 2 + numFilterLabels; i++) {
filterLabels.add("label_" + (i % numTags));
}
SlowRemoveTags slow = new SlowRemoveTags();
FastRemoveTags fastInst = new FastRemoveTags();
List<String> slowResult = null, fastResult = null;
for (int n = 0; n < numSeries; n++) {
if (!fast) {
slowResult = slow.removeTagsIgnoring(metricTags, filterLabels);
} else {
fastResult = fastInst.removeTagsIgnoring(metricTags, filterLabels);
}
}
return new long[]{slow.comparisons, fastInst.ops};
}
public static void main(String[] args) {
// sizes = {T, I} pairs representing (numTags, numFilterLabels)
int[][] cases = {{10, 5}, {20, 10}, {50, 25}, {100, 50}};
int numSeries = 1000;
System.out.println("VictoriaMetrics0002MetricNameTagFilterTest — victoria-metrics-0002");
System.out.println(" Pattern: hasTag() O(T×I) linear scan vs O(1) map lookup");
System.out.println(" Simulation: " + numSeries + " series per test");
System.out.println();
int passed = 0;
int total = 0;
for (int[] tc : cases) {
int T = tc[0], I = tc[1];
List<String> metricTags = new ArrayList<>();
for (int i = 0; i < T; i++) metricTags.add("label_" + i);
// filter labels are labels in the latter half (worst case for scan)
List<String> filterLabels = new ArrayList<>();
for (int i = T / 2; i < T / 2 + I; i++) {
filterLabels.add("label_" + (i % T));
}
SlowRemoveTags slow = new SlowRemoveTags();
FastRemoveTags fast = new FastRemoveTags();
// Run numSeries calls
List<String> slowResult = null, fastResult = null;
for (int n = 0; n < numSeries; n++) {
slowResult = slow.removeTagsIgnoring(metricTags, filterLabels);
fastResult = fast.removeTagsIgnoring(metricTags, filterLabels);
}
boolean sameResult = slowResult != null && fastResult != null &&
slowResult.equals(fastResult);
double ratio = fast.ops > 0 ? (double) slow.comparisons / fast.ops : 1.0;
boolean correctRatio = T >= 20 ? ratio >= 5.0 : ratio >= 2.0;
boolean pass = sameResult && correctRatio;
total++;
if (pass) passed++;
System.out.printf(" T=%-4d I=%-4d slow=%8d fast=%7d ratio=%5.1fx same=%b %s%n",
T, I, slow.comparisons, fast.ops, ratio, sameResult,
pass ? "PASS" : "FAIL");
}
System.out.println();
System.out.printf("Result: %d/%d PASS%n", passed, total);
if (passed < total) System.exit(1);
}
}