Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2014-2025 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spectator.atlas;

import com.netflix.spectator.api.Id;
import com.netflix.spectator.api.NoopRegistry;
import com.netflix.spectator.api.Registry;
import com.netflix.spectator.atlas.impl.Parser;
import com.netflix.spectator.atlas.impl.Query;
import com.netflix.spectator.atlas.impl.QueryIndex;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* Exercises the hot matching path used by the LWC bridge: an index of many subscription
* queries matched against a stream of ids. The dominant cost in profiles is
* {@link QueryIndex#forEachMatch} walking the sorted keys of each id and comparing them
* against the keys stored in the index.
*
* <pre>
* Benchmark Mode Cnt Score Error Units
* QueryIndexMatch.match avgt 5 ...
* </pre>
*/
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class QueryIndexMatch {

private static final String[] KEYS = {
"nf.app", "nf.cluster", "nf.asg", "nf.region", "nf.zone", "nf.stack",
"device", "country", "status", "statistic"
};

private static String val(Random r) {
char c = (char) ('a' + r.nextInt(8));
return String.valueOf(c);
}

private QueryIndex<Integer> idx;
private List<Id> ids;

@Setup
public void setup() {
Registry registry = new NoopRegistry();
Random r = new Random(42);

// Build a realistic set of subscriptions: each anchored on the name plus a couple of
// additional dimension clauses (mix of :eq and :re), combined with :and.
idx = QueryIndex.newInstance(registry);
int numQueries = 5_000;
for (int i = 0; i < numQueries; ++i) {
StringBuilder sb = new StringBuilder();
sb.append("name,m").append(r.nextInt(50)).append(",:eq");
int extra = 1 + r.nextInt(3);
for (int j = 0; j < extra; ++j) {
String k = KEYS[r.nextInt(KEYS.length)];
if (r.nextInt(4) == 0) {
sb.append(',').append(k).append(',').append(val(r)).append(",:re,:and");
} else {
sb.append(',').append(k).append(',').append(val(r)).append(",:eq,:and");
}
}
Query q = Parser.parseQuery(sb.toString());
idx.add(q, i);
}

// Build a stream of ids with a name and several sorted tags, matching the shape of
// datapoints flowing through the bridge.
ids = new ArrayList<>();
int numIds = 200;
for (int i = 0; i < numIds; ++i) {
Id id = Id.create("m" + r.nextInt(50));
int numTags = 4 + r.nextInt(5);
for (int j = 0; j < numTags; ++j) {
id = id.withTag(KEYS[r.nextInt(KEYS.length)], val(r));
}
ids.add(id);
}
}

@Benchmark
public void match(Blackhole bh) {
for (int i = 0; i < ids.size(); ++i) {
idx.forEachMatch(ids.get(i), bh::consume);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,37 @@ private static <V> QueryIndex<V> empty(CacheSupplier<V> cacheSupplier) {
* This allows the {@link Id} to be traversed in order while performing the lookup.
*/
private static int compare(String k1, String k2) {
return compare(k1, k2, "name".equals(k2));
}

/**
* Variant of {@link #compare(String, String)} where the caller has already determined
* whether {@code k2} is the {@code name} key. In the hot matching path {@code k2} is the
* fixed key for a node, so this check can be hoisted out of the per-tag loop rather than
* recomputed on every comparison.
*/
private static int compare(String k1, String k2, boolean k2IsName) {
if ("name".equals(k1)) {
return "name".equals(k2) ? 0 : -1;
} else if ("name".equals(k2)) {
return 1;
return k2IsName ? 0 : -1;
} else {
return k1.compareTo(k2);
return k2IsName ? 1 : k1.compareTo(k2);
}
}

/**
* Compare a tag key at a given position against the fixed key for a node while traversing
* an id. Only position 0 holds the synthesized {@code name} key, so it needs the full
* name-first comparison. Positions {@code >= 1} come from the tag list, which is sorted by
* {@link String#compareTo}, so the name-first special-casing can be skipped there: even if
* a tag is literally keyed {@code name}, any {@code keyRef} that sorts before {@code name}
* cannot appear after that entry in the sorted scan, so a plain comparison never skips a
* match.
*/
private static int compareTagKey(String k, String keyRef, boolean keyRefIsName, int position) {
if (position == 0) {
return compare(k, keyRef, keyRefIsName);
} else {
return keyRefIsName ? 1 : k.compareTo(keyRef);
}
}

Expand Down Expand Up @@ -450,11 +475,15 @@ private void forEachMatch(Id tags, int i, Consumer<T> consumer) {

boolean keyPresent = false;

// keyRef is fixed for this node, so the "name" check only needs to be done once
// here rather than on every comparison within the loop below.
final boolean keyRefIsName = "name".equals(keyRef);

final int tagsSize = tags.size();
for (int j = i; j < tagsSize; ++j) {
String k = tags.getKey(j);
String v = tags.getValue(j);
int cmp = compare(k, keyRef);
int cmp = compareTagKey(k, keyRef, keyRefIsName, j);
if (cmp == 0) {
final int nextPos = j + 1;
keyPresent = true;
Expand Down