diff --git a/defects/victoria-metrics/unit/VictoriaMetrics0002MetricNameTagFilterTest.java b/defects/victoria-metrics/unit/VictoriaMetrics0002MetricNameTagFilterTest.java new file mode 100644 index 000000000..ff3b53b0f --- /dev/null +++ b/defects/victoria-metrics/unit/VictoriaMetrics0002MetricNameTagFilterTest.java @@ -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 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 removeTagsOn(List metricTags, List onLabels) { + List 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 removeTagsIgnoring(List metricTags, List ignoringLabels) { + List 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 removeTagsOn(List metricTags, List onLabels) { + // Build set — O(I) + Set onSet = new HashSet<>(); + for (String t : onLabels) { + onSet.add(t); + ops++; + } + // Filter — O(T) + List 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 removeTagsIgnoring(List metricTags, List ignoringLabels) { + // Build set — O(I) + Set ignoreSet = new HashSet<>(); + for (String t : ignoringLabels) { + ignoreSet.add(t); + ops++; + } + // Filter — O(T) + List 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 metricTags = new ArrayList<>(); + for (int i = 0; i < numTags; i++) metricTags.add("label_" + i); + + List 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 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 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 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 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); + } +}