java-topology/defects/victoria-metrics/unit/VictoriaMetricsStreamAggrAlgorithm.java

171 lines
7.1 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.*;
/**
* victoria-metrics-0001 — streamaggr: slices.Contains O(L×K) inside Push() hot loop
*
* Models lib/streamaggr/streamaggr.go:
* func (a *aggregator) Push(tss []prompb.TimeSeries, ...) {
* for _, ts := range tss {
* labels.Labels = dropSeriesLabels(labels.Labels, ts.Labels, dropLabels)
* getInputOutputLabels(..., a.by, a.without)
* }
* }
*
* dropSeriesLabels — for each label, calls slices.Contains(labelNames, label.Name) → O(K)
* getInputOutputLabels — for each label, calls slices.Contains(without/by, label.Name) → O(K)
*
* Defective: O(N × L × K) total comparisons
* Fixed: O(N × L) using pre-built map[string]struct{} (built once, O(K))
*
* Ratio >= 10× at N=1000, L=20, K=20
*
* No JUnit. Uses assert. Prints N/N PASS.
*
* Compile: javac -d . VictoriaMetricsStreamAggrAlgorithm.java
* Run: java -ea -cp . unit.VictoriaMetricsStreamAggrAlgorithm
*/
public class VictoriaMetricsStreamAggrAlgorithm {
static int passed = 0;
static int total = 0;
static void check(String desc, boolean cond) {
total++;
if (cond) {
passed++;
System.out.println("PASS: " + desc);
} else {
System.out.println("FAIL: " + desc);
throw new AssertionError("FAIL: " + desc);
}
}
// -----------------------------------------------------------------------
// Slow: models dropSeriesLabels + getInputOutputLabels with slices.Contains
// For each of N series, for each of L labels, scans K-element filter list
// Returns total comparison operations performed.
// -----------------------------------------------------------------------
static long pushSlow(int N, int L, List<String> filterList) {
long ops = 0;
int K = filterList.size();
// Simulate N time series, each with L labels named "label_0".."label_{L-1}"
// filterList contains the "by" or "without" set (some subset of label names)
for (int s = 0; s < N; s++) {
for (int l = 0; l < L; l++) {
String labelName = "label_" + l;
// slices.Contains: O(K) linear scan
for (int k = 0; k < K; k++) {
ops++;
if (filterList.get(k).equals(labelName)) {
break; // found — early exit (worst case: not found, full K ops)
}
}
}
}
return ops;
}
// -----------------------------------------------------------------------
// Fast: pre-build map[string]struct{} once, then O(1) per label per series
// Returns total comparison operations performed.
// -----------------------------------------------------------------------
static long pushFast(int N, int L, List<String> filterList) {
long ops = 0;
// Build set once — O(K)
Set<String> filterSet = new HashSet<>(filterList);
ops += filterList.size(); // cost of building the set
// Now O(1) per label lookup
for (int s = 0; s < N; s++) {
for (int l = 0; l < L; l++) {
String labelName = "label_" + l;
ops++; // O(1) hash lookup
filterSet.contains(labelName);
}
}
return ops;
}
// -----------------------------------------------------------------------
// Correctness: both methods produce the same classification of labels
// -----------------------------------------------------------------------
static List<String> classifyLabelsSlice(List<String> labels, List<String> byList) {
List<String> output = new ArrayList<>();
for (String label : labels) {
boolean inBy = false;
for (String b : byList) {
if (b.equals(label)) { inBy = true; break; }
}
if (inBy) output.add(label); // output = "by" labels
}
return output;
}
static List<String> classifyLabelsMap(List<String> labels, List<String> byList) {
Set<String> bySet = new HashSet<>(byList);
List<String> output = new ArrayList<>();
for (String label : labels) {
if (bySet.contains(label)) output.add(label);
}
return output;
}
public static void main(String[] args) {
System.out.println("victoria-metrics-0001: streamaggr label filter O(N×L×K) → O(N×L)");
System.out.println();
// --- Correctness ---
List<String> labels = Arrays.asList("label_0","label_1","label_2","label_3","label_4");
List<String> byList = Arrays.asList("label_1","label_3");
List<String> slowResult = classifyLabelsSlice(labels, byList);
List<String> fastResult = classifyLabelsMap(labels, byList);
check("correctness: same labels classified", slowResult.equals(fastResult));
check("correctness: 2 labels in 'by' set", slowResult.size() == 2);
check("correctness: label_1 in output", slowResult.contains("label_1"));
check("correctness: label_3 in output", slowResult.contains("label_3"));
check("correctness: label_0 not in output", !slowResult.contains("label_0"));
// --- Performance ---
// N=1000 series, L=20 labels each, K=20 filter entries (worst case: none match → full scan)
int N = 1000, L = 20, K = 20;
// Build a filter list of 20 entries NOT matching any of our L labels
// (worst case — label not found means full K comparisons every time)
List<String> filterList = new ArrayList<>();
for (int k = 0; k < K; k++) filterList.add("filter_" + k);
long slowOps = pushSlow(N, L, filterList);
long fastOps = pushFast(N, L, filterList);
double ratio = (double) slowOps / fastOps;
System.out.println(" N=" + N + " series, L=" + L + " labels, K=" + K + " filters");
System.out.println(" Slow ops: " + slowOps);
System.out.println(" Fast ops: " + fastOps);
System.out.printf (" Ratio: %.1fx%n", ratio);
System.out.println();
check("ops: slow is O(N*L*K) = " + (N*L*K), slowOps == (long) N * L * K);
check("ops: fast is ~O(N*L+K) < 2*N*L", fastOps <= (long) 2 * N * L + K);
check("speedup >= 10x at N=1000/L=20/K=20", ratio >= 10.0);
// Scale: larger cluster
N = 10000; L = 30; K = 20;
long slowOps2 = pushSlow(N, L, filterList); // reuse same filterList (K=20 entries)
List<String> filterList2 = new ArrayList<>();
for (int k = 0; k < K; k++) filterList2.add("filter_" + k);
long fastOps2 = pushFast(N, L, filterList2);
double ratio2 = (double) slowOps2 / fastOps2;
System.out.println(" Scale test: N=" + N + " series, L=" + L + " labels, K=" + K);
System.out.println(" Slow ops: " + slowOps2);
System.out.println(" Fast ops: " + fastOps2);
System.out.printf (" Ratio: %.1fx%n", ratio2);
System.out.println();
check("scale: speedup >= 10x at N=10000/L=30/K=20", ratio2 >= 10.0);
System.out.println(passed + "/" + total + " PASS");
}
}