Skip to content

Commit 6098b45

Browse files
committed
feat(MonitoringSubsystem): provide Micrometer gauges for rendering.world
1 parent 23337fa commit 6098b45

6 files changed

Lines changed: 235 additions & 27 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2021 The Terasology Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package org.terasology.engine.core.subsystem.common;
5+
6+
import java.util.Set;
7+
8+
/**
9+
* Describes the set of gauges that may apply to a particular interface.
10+
*/
11+
public class GaugeMapEntry {
12+
public final Class<?> iface;
13+
public final Set<GaugeSpec<?>> gaugeSpecs;
14+
15+
@SafeVarargs
16+
public <T> GaugeMapEntry(Class<T> iface, GaugeSpec<? extends T>... gaugeSpecs) {
17+
this.iface = iface;
18+
this.gaugeSpecs = Set.of(gaugeSpecs);
19+
}
20+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2021 The Terasology Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package org.terasology.engine.core.subsystem.common;
5+
6+
import io.micrometer.core.instrument.Gauge;
7+
import io.micrometer.core.instrument.MeterRegistry;
8+
import io.micrometer.core.instrument.binder.MeterBinder;
9+
10+
import java.util.function.ToDoubleFunction;
11+
12+
import static com.google.common.base.Preconditions.checkArgument;
13+
14+
/**
15+
* The information that defines a Gauge.
16+
* <p>
17+
* The Micrometer API doesn't let you define a Gauge without connecting it to
18+
* some MeterRegistry. This class provides an immutable record of all<sup>*</sup>
19+
* the properties of a Gauge, facilitating a more data-driven approach.
20+
* <p>
21+
* * <i>All the ones we use so far, anyway.</i>
22+
*
23+
* @param <T> the type this gauge reads from
24+
*/
25+
public class GaugeSpec<T> {
26+
public final String name;
27+
public final String description;
28+
public final ToDoubleFunction<T> valueFunction;
29+
public final String baseUnit;
30+
31+
protected final Class<T> subjectType;
32+
33+
public GaugeSpec(String name, String description, ToDoubleFunction<T> valueFunction) {
34+
this(name, description, valueFunction, null);
35+
}
36+
37+
/** @see Gauge.Builder */
38+
public GaugeSpec(String name, String description, ToDoubleFunction<T> valueFunction, String baseUnit) {
39+
this.name = name;
40+
this.description = description;
41+
this.valueFunction = valueFunction;
42+
this.baseUnit = baseUnit;
43+
this.subjectType = getSubjectClass();
44+
}
45+
46+
public Gauge register(MeterRegistry registry, T subject) {
47+
return Gauge.builder(name, subject, valueFunction)
48+
.description(description)
49+
.baseUnit(baseUnit)
50+
.register(registry);
51+
}
52+
53+
/**
54+
* Creates a MeterBinder for this gauge.
55+
* <p>
56+
* This allows us to make things with the same interface as the meters
57+
* provided by {@link io.micrometer.core.instrument.binder}.
58+
*
59+
* @param subject passed to this gauge's {@link #valueFunction}
60+
* @return call to bind this gauge to a MeterRegistry
61+
*/
62+
public MeterBinder binder(T subject) {
63+
return registry -> register(registry, subject);
64+
}
65+
66+
public <U> MeterBinder binderAfterCasting(U subject) {
67+
checkArgument(isInstanceOfType(subject));
68+
T safeSubject = subjectType.cast(subject);
69+
return binder(safeSubject);
70+
}
71+
72+
public boolean isInstanceOfType(Object object) {
73+
return subjectType.isInstance(object);
74+
}
75+
76+
@SafeVarargs
77+
private Class<T> getSubjectClass(T...t) {
78+
// Thank you https://stackoverflow.com/a/40917725 for this amazing kludge
79+
//noinspection unchecked
80+
return (Class<T>) t.getClass().getComponentType();
81+
}
82+
}

engine/src/main/java/org/terasology/engine/core/subsystem/common/MonitoringSubsystem.java

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,38 @@
66
import io.micrometer.core.instrument.Gauge;
77
import io.micrometer.core.instrument.MeterRegistry;
88
import io.micrometer.core.instrument.Metrics;
9+
import io.micrometer.core.instrument.binder.MeterBinder;
910
import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
1011
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
1112
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
1213
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
1314
import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
1415
import io.micrometer.jmx.JmxConfig;
1516
import io.micrometer.jmx.JmxMeterRegistry;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
1619
import org.terasology.engine.config.SystemConfig;
1720
import org.terasology.engine.context.Context;
1821
import org.terasology.engine.core.GameEngine;
1922
import org.terasology.engine.core.Time;
2023
import org.terasology.engine.core.subsystem.EngineSubsystem;
2124
import org.terasology.engine.monitoring.gui.AdvancedMonitor;
25+
import reactor.core.publisher.Flux;
26+
import reactor.function.TupleUtils;
27+
import reactor.util.function.Tuple2;
28+
import reactor.util.function.Tuples;
2229

2330
import java.time.Duration;
31+
import java.util.List;
32+
import java.util.Set;
2433

2534
public class MonitoringSubsystem implements EngineSubsystem {
2635

2736
public static final Duration JMX_INTERVAL = Duration.ofSeconds(5);
2837

38+
private static final Logger logger = LoggerFactory.getLogger(MonitoringSubsystem.class);
39+
40+
protected MeterRegistry meterRegistry;
2941
private AdvancedMonitor advancedMonitor;
3042

3143
@Override
@@ -39,29 +51,23 @@ public void initialise(GameEngine engine, Context rootContext) {
3951
advancedMonitor = new AdvancedMonitor();
4052
advancedMonitor.setVisible(true);
4153
}
54+
meterRegistry = initMeterRegistries();
55+
}
4256

43-
initMicrometerMetrics(rootContext.get(Time.class));
57+
@Override
58+
public void postInitialise(Context context) {
59+
initMeters(context);
4460
}
4561

4662
/**
4763
* Initialize Micrometer metrics and publishers.
4864
*
4965
* @see org.terasology.engine.core.GameScheduler GameScheduler for global Reactor metrics
50-
* @param time provides statistics
51-
* <p>
52-
* Note {@link org.terasology.engine.core.EngineTime EngineTime}
53-
* does not serve the same role as a {@link Clock micrometer Clock}.
54-
* (Not yet. Maybe it should?)
5566
*/
56-
private void initMicrometerMetrics(Time time) {
67+
protected MeterRegistry initMeterRegistries() {
5768
// Register metrics with the built-in global composite registry.
5869
// This makes them available to any agent(s) we add to it.
59-
MeterRegistry meterRegistry = Metrics.globalRegistry;
60-
61-
Gauge.builder("terasology.fps", time::getFps)
62-
.description("framerate")
63-
.baseUnit("Hz")
64-
.register(meterRegistry);
70+
MeterRegistry registry = Metrics.globalRegistry;
6571

6672
// Publish the global metrics registry on a JMX server.
6773
MeterRegistry jmxMeterRegistry = new JmxMeterRegistry(new JmxConfig() {
@@ -77,14 +83,58 @@ public Duration step() {
7783
}, Clock.SYSTEM);
7884
Metrics.addRegistry(jmxMeterRegistry);
7985

86+
return registry;
8087
// If we want to make global metrics available to our custom view,
8188
// we add our custom registry to the global composite:
8289
//
8390
// Metrics.addRegistry(DebugOverlay.meterRegistry);
8491
//
8592
// If we want to see JVM metrics there as well:
8693
//
87-
// initAllJvmMetrics(DebugOverlay.meterRegistry);
94+
// allJvmMetrics().forEach(m -> m.bindTo(DebugOverlay.meterRegistry));
95+
}
96+
97+
/** Initialize meters for all the things in this Context. */
98+
protected void initMeters(Context context) {
99+
// We can build meters individually like this:
100+
var time = context.get(Time.class);
101+
Gauge.builder("terasology.fps", time::getFps)
102+
.description("framerate")
103+
.baseUnit("Hz")
104+
.register(meterRegistry);
105+
106+
// But we'd like the code for meters to live closer to the implementation
107+
// of the thing they're monitoring.
108+
//
109+
// Somewhere we get a list of all the things that provide meters.
110+
// Maybe hardcoded, maybe a registry of some kind? Modules will want
111+
// to contribute as well.
112+
var meterMaps = List.of(
113+
org.terasology.engine.rendering.world.Meters.GAUGE_MAP
114+
);
115+
116+
meterMaps.forEach(gaugeMap -> registerForContext(context, gaugeMap));
117+
}
118+
119+
protected void registerForContext(Context context, Iterable<GaugeMapEntry> gaugeMap) {
120+
Flux.fromIterable(gaugeMap)
121+
.map(entry -> Tuples.of(context.get(entry.iface), entry.gaugeSpecs))
122+
.filter(TupleUtils.predicate((subject, specs) -> subject != null))
123+
.doOnDiscard(Tuple2.class, TupleUtils.consumer((iface, gaugeSpecs) ->
124+
logger.debug("Not building gauges for {}, none was in {}", iface, context)))
125+
.subscribe(TupleUtils.consumer(this::registerAll));
126+
}
127+
128+
protected <T> void registerAll(T subject, Set<GaugeSpec<? extends T>> gaugeSpecs) {
129+
Flux.fromIterable(gaugeSpecs)
130+
.filter(spec -> spec.isInstanceOfType(subject))
131+
// Make sure the gauge is right for the specific type.
132+
.map(spec -> spec.binderAfterCasting(subject))
133+
.subscribe(this::registerMeter);
134+
}
135+
136+
public void registerMeter(MeterBinder meterBinder) {
137+
meterBinder.bindTo(meterRegistry);
88138
}
89139

90140
/**
@@ -95,12 +145,14 @@ public Duration step() {
95145
* have a different agent you want them published through.
96146
*/
97147
@SuppressWarnings("unused")
98-
void initAllJvmMetrics(MeterRegistry registry) {
99-
new ClassLoaderMetrics().bindTo(registry);
100-
new JvmMemoryMetrics().bindTo(registry);
101-
new JvmGcMetrics().bindTo(registry);
102-
new JvmThreadMetrics().bindTo(registry);
103-
new ProcessorMetrics().bindTo(registry);
148+
List<MeterBinder> allJvmMetrics() {
149+
return List.of(
150+
new ClassLoaderMetrics(),
151+
new JvmMemoryMetrics(),
152+
new JvmGcMetrics(),
153+
new JvmThreadMetrics(),
154+
new ProcessorMetrics()
155+
);
104156
}
105157

106158
@Override
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2021 The Terasology Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package org.terasology.engine.rendering.world;
5+
6+
import io.micrometer.core.instrument.binder.BaseUnits;
7+
import org.terasology.engine.core.subsystem.common.GaugeMapEntry;
8+
import org.terasology.engine.core.subsystem.common.GaugeSpec;
9+
10+
import java.util.List;
11+
12+
public final class Meters {
13+
public static final String PREFIX = Meters.class.getPackageName();
14+
15+
public static final List<GaugeMapEntry> GAUGE_MAP = List.of(
16+
new GaugeMapEntry(WorldRenderer.class,
17+
new GaugeSpec<WorldRendererImpl>(
18+
PREFIX + ".emptyMeshChunks",
19+
"Empty Mesh Chunks",
20+
wri -> wri.statChunkMeshEmpty,
21+
BaseUnits.OBJECTS),
22+
new GaugeSpec<WorldRendererImpl>(
23+
PREFIX + ".unreadyChunks",
24+
"Unready Chunks",
25+
wri -> wri.statChunkNotReady,
26+
BaseUnits.OBJECTS),
27+
new GaugeSpec<WorldRendererImpl>(
28+
PREFIX + ".triangles",
29+
"Rendered Triangles",
30+
wri -> wri.statRenderedTriangles,
31+
BaseUnits.OBJECTS)
32+
),
33+
new GaugeMapEntry(RenderableWorld.class,
34+
new GaugeSpec<RenderableWorldImpl>(
35+
PREFIX + ".visibleChunks",
36+
"Visible Chunks",
37+
rwi -> rwi.statVisibleChunks,
38+
BaseUnits.OBJECTS),
39+
new GaugeSpec<RenderableWorldImpl>(
40+
PREFIX + ".dirtyChunks",
41+
"Dirty Chunks",
42+
rwi -> rwi.statDirtyChunks,
43+
BaseUnits.OBJECTS),
44+
new GaugeSpec<RenderableWorldImpl>(
45+
PREFIX + ".dirtyChunks",
46+
"Ignored Phases",
47+
rwi -> rwi.statIgnoredPhases)
48+
)
49+
);
50+
51+
private Meters() { }
52+
}

engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorldImpl.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ class RenderableWorldImpl implements RenderableWorld {
5050
ViewDistance.MEGA.getChunkDistance().x() * ViewDistance.MEGA.getChunkDistance().y() * ViewDistance.MEGA.getChunkDistance().z();
5151
private static final Vector3fc CHUNK_CENTER_OFFSET = new Vector3f(Chunks.CHUNK_SIZE).div(2);
5252

53+
int statDirtyChunks;
54+
int statVisibleChunks;
55+
int statIgnoredPhases;
56+
5357
private final int maxChunksForShadows =
5458
TeraMath.clamp(CoreRegistry.get(Config.class).getRendering().getMaxChunksUsedForShadowMapping(), 64, 1024);
5559

@@ -71,10 +75,6 @@ class RenderableWorldImpl implements RenderableWorld {
7175
private final Config config = CoreRegistry.get(Config.class);
7276
private final RenderingConfig renderingConfig = config.getRendering();
7377

74-
private int statDirtyChunks;
75-
private int statVisibleChunks;
76-
private int statIgnoredPhases;
77-
7878

7979
RenderableWorldImpl(Context context, Camera playerCamera) {
8080

engine/src/main/java/org/terasology/engine/rendering/world/WorldRendererImpl.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public final class WorldRendererImpl implements WorldRenderer {
6969
*/
7070
private static final Logger logger = LoggerFactory.getLogger(WorldRendererImpl.class);
7171
private static final float GROUND_PLANE_HEIGHT_DISPARITY = -0.7f;
72+
73+
int statChunkMeshEmpty;
74+
int statChunkNotReady;
75+
int statRenderedTriangles;
76+
7277
private RenderGraph renderGraph;
7378
private RenderingModuleRegistry renderingModuleRegistry;
7479

@@ -88,9 +93,6 @@ public final class WorldRendererImpl implements WorldRenderer {
8893

8994
private float millisecondsSinceRenderingStart;
9095
private float secondsSinceLastFrame;
91-
private int statChunkMeshEmpty;
92-
private int statChunkNotReady;
93-
private int statRenderedTriangles;
9496

9597
private final RenderingConfig renderingConfig;
9698
private final Console console;

0 commit comments

Comments
 (0)