Skip to content

Commit 21ea737

Browse files
Animus maps
- Merge Animus cost, network and pressure maps
2 parents 0bfe5f0 + 8dbd5ec commit 21ea737

35 files changed

Lines changed: 1321 additions & 8 deletions
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package art.arcane.react.content.feature;
2+
3+
import art.arcane.react.React;
4+
import art.arcane.react.api.metric.ReactMetric;
5+
import art.arcane.react.api.metric.internal.PublishedMetricStore;
6+
import art.arcane.react.core.controller.IntegrationController;
7+
import art.arcane.react.core.integration.ThirdPartyMetricRegistry;
8+
9+
import java.util.ArrayList;
10+
import java.util.Comparator;
11+
import java.util.List;
12+
import java.util.Locale;
13+
14+
/**
15+
* Reads the metrics Animus publishes through the public {@code ReactMetricSource} API. Keys are
16+
* discovered by prefix so Animus decides which subsystems and packet kinds fit inside the
17+
* per-source metric budget.
18+
*/
19+
final class AnimusMetricSeries {
20+
static final String SOURCE_ID = "animus";
21+
static final String NET_BYTES_PER_SECOND = "animus.net.bytes_per_second";
22+
static final String NET_PACKETS_PER_SECOND = "animus.net.packets_per_second";
23+
static final String NET_PLAYER_PEAK_BYTES_PER_SECOND = "animus.net.player_peak.bytes_per_second";
24+
static final String NET_KIND_PREFIX = "animus.net.kind.";
25+
static final String NET_KIND_SUFFIX = ".bytes_per_second";
26+
static final String COST_PREFIX = "animus.cost.";
27+
static final String COST_SUFFIX = "_ms";
28+
static final String COST_TOTAL_NAME = "total";
29+
static final String PRESSURE_PREFIX = "animus.pressure.";
30+
private static final long CACHE_TTL_MS = 250L;
31+
private static volatile CachedSnapshot cached = new CachedSnapshot(Snapshot.empty(), 0L);
32+
33+
private AnimusMetricSeries() {
34+
}
35+
36+
static Snapshot snapshot() {
37+
long now = System.currentTimeMillis();
38+
CachedSnapshot current = cached;
39+
if (now >= current.capturedAtMs() && now - current.capturedAtMs() < CACHE_TTL_MS) {
40+
return current.snapshot();
41+
}
42+
43+
IntegrationController controller = React.controller(IntegrationController.class);
44+
ThirdPartyMetricRegistry registry = controller == null ? null : controller.getThirdPartyMetrics();
45+
Snapshot snapshot = fromStore(registry == null ? null : registry.store(), now);
46+
cached = new CachedSnapshot(snapshot, now);
47+
return snapshot;
48+
}
49+
50+
static Snapshot fromStore(PublishedMetricStore store, long now) {
51+
if (store == null) {
52+
return Snapshot.empty();
53+
}
54+
55+
List<ReactMetric> declared = store.declared(SOURCE_ID);
56+
if (declared.isEmpty()) {
57+
return Snapshot.empty();
58+
}
59+
60+
List<Entry> costs = new ArrayList<>();
61+
List<Entry> packetKinds = new ArrayList<>();
62+
double bytesPerSecond = Double.NaN;
63+
double packetsPerSecond = Double.NaN;
64+
double playerPeakBytesPerSecond = Double.NaN;
65+
double peakPressure = Double.NaN;
66+
67+
for (ReactMetric metric : declared) {
68+
String key = metric.key();
69+
PublishedMetricStore.Reading reading = store.reading(key, now);
70+
if (reading == null || !Double.isFinite(reading.value())) {
71+
continue;
72+
}
73+
74+
double value = Math.max(0D, reading.value());
75+
switch (key) {
76+
case NET_BYTES_PER_SECOND -> bytesPerSecond = value;
77+
case NET_PACKETS_PER_SECOND -> packetsPerSecond = value;
78+
case NET_PLAYER_PEAK_BYTES_PER_SECOND -> playerPeakBytesPerSecond = value;
79+
default -> {
80+
String costName = segment(key, COST_PREFIX, COST_SUFFIX);
81+
if (costName != null) {
82+
if (!COST_TOTAL_NAME.equals(costName) && value > 0D) {
83+
costs.add(new Entry(costName, displayName(costName), value));
84+
}
85+
continue;
86+
}
87+
88+
String kindName = segment(key, NET_KIND_PREFIX, NET_KIND_SUFFIX);
89+
if (kindName != null) {
90+
if (value > 0D) {
91+
packetKinds.add(new Entry(kindName, displayName(kindName), value));
92+
}
93+
continue;
94+
}
95+
96+
if (segment(key, PRESSURE_PREFIX, "") != null) {
97+
double clamped = Math.min(100D, value);
98+
peakPressure = Double.isNaN(peakPressure) ? clamped : Math.max(peakPressure, clamped);
99+
}
100+
}
101+
}
102+
}
103+
104+
costs.sort(ENTRY_ORDER);
105+
packetKinds.sort(ENTRY_ORDER);
106+
return new Snapshot(
107+
true,
108+
List.copyOf(costs),
109+
List.copyOf(packetKinds),
110+
bytesPerSecond,
111+
packetsPerSecond,
112+
playerPeakBytesPerSecond,
113+
peakPressure
114+
);
115+
}
116+
117+
private static final Comparator<Entry> ENTRY_ORDER = Comparator
118+
.comparingDouble(Entry::value).reversed()
119+
.thenComparing(Entry::id);
120+
121+
private static String segment(String key, String prefix, String suffix) {
122+
if (key == null || !key.startsWith(prefix) || !key.endsWith(suffix)) {
123+
return null;
124+
}
125+
126+
int end = key.length() - suffix.length();
127+
if (end <= prefix.length()) {
128+
return null;
129+
}
130+
131+
String name = key.substring(prefix.length(), end);
132+
return name.indexOf('.') >= 0 ? null : name;
133+
}
134+
135+
static String displayName(String id) {
136+
if (id == null || id.isBlank()) {
137+
return "";
138+
}
139+
140+
String[] words = id.replace('_', ' ').replace('-', ' ').trim().split("\\s+");
141+
StringBuilder display = new StringBuilder(id.length());
142+
for (String word : words) {
143+
if (word.isEmpty()) {
144+
continue;
145+
}
146+
if (!display.isEmpty()) {
147+
display.append(' ');
148+
}
149+
display.append(word.substring(0, 1).toUpperCase(Locale.ROOT));
150+
display.append(word.substring(1));
151+
}
152+
return display.isEmpty() ? id : display.toString();
153+
}
154+
155+
private record CachedSnapshot(Snapshot snapshot, long capturedAtMs) {
156+
}
157+
158+
record Entry(String id, String displayName, double value) {
159+
}
160+
161+
record Snapshot(
162+
boolean declared,
163+
List<Entry> costs,
164+
List<Entry> packetKinds,
165+
double bytesPerSecond,
166+
double packetsPerSecond,
167+
double playerPeakBytesPerSecond,
168+
double peakPressure
169+
) {
170+
static Snapshot empty() {
171+
return new Snapshot(false, List.of(), List.of(), Double.NaN, Double.NaN, Double.NaN, Double.NaN);
172+
}
173+
174+
double totalCostMs() {
175+
double total = 0D;
176+
for (Entry cost : costs) {
177+
total += cost.value();
178+
}
179+
return total;
180+
}
181+
}
182+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package art.arcane.react.content.feature;
2+
3+
import art.arcane.react.api.feature.CapabilityGatedFeature;
4+
import art.arcane.react.localization.catalog.RendererMessages;
5+
import art.arcane.react.util.data.TinyColor;
6+
import art.arcane.volmlib.util.format.Form;
7+
import art.arcane.volmlib.util.localization.TextKey;
8+
import org.bukkit.entity.Player;
9+
10+
import java.util.Map;
11+
import java.util.Set;
12+
13+
@art.arcane.react.util.project.config.ConfigDescription("Configuration for the Animus Cost Share Pie Map feature.")
14+
public class FeatureAnimusCostSharePieMap extends FeatureIrisChunkSharePieBase implements CapabilityGatedFeature {
15+
public static final String ID = "animus-cost-share-pie-map";
16+
private static final double MICROS_PER_MILLI = 1000D;
17+
18+
public FeatureAnimusCostSharePieMap() {
19+
super(ID);
20+
}
21+
22+
@Override
23+
protected TextKey title() {
24+
return RendererMessages.TITLE_ANIMUS_COST_SHARE;
25+
}
26+
27+
@Override
28+
protected TinyColor headerColor() {
29+
return new TinyColor(58, 142, 158);
30+
}
31+
32+
@Override
33+
protected Map<String, Long> collectBuckets(Player viewer) {
34+
return costBuckets(AnimusMetricSeries.snapshot());
35+
}
36+
37+
static Map<String, Long> costBuckets(AnimusMetricSeries.Snapshot snapshot) {
38+
Map<String, Long> buckets = newCounterMap();
39+
for (AnimusMetricSeries.Entry cost : snapshot.costs()) {
40+
long micros = Math.round(cost.value() * MICROS_PER_MILLI);
41+
if (micros > 0L) {
42+
buckets.merge(cost.displayName(), micros, Long::sum);
43+
}
44+
}
45+
return buckets;
46+
}
47+
48+
@Override
49+
protected TextKey totalUnit() {
50+
return RendererMessages.PIE_UNIT_MS_PER_SECOND;
51+
}
52+
53+
@Override
54+
protected String compact(long value) {
55+
return formatMillis(value);
56+
}
57+
58+
static String formatMillis(long micros) {
59+
double millis = Math.max(0L, micros) / MICROS_PER_MILLI;
60+
if (millis >= 100D) {
61+
return Form.f(millis, 0);
62+
}
63+
return Form.f(millis, millis >= 10D ? 1 : 2);
64+
}
65+
66+
@Override
67+
public Set<String> requiredCapabilities() {
68+
return Set.of(AnimusMetricSeries.SOURCE_ID);
69+
}
70+
}

0 commit comments

Comments
 (0)