victoria-metrics-0002: add Java unit test for MetricName tag-filter O(T×I) defect
This commit is contained in:
parent
a922a7ee9d
commit
657dac6c22
1 changed files with 179 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue