Skip to content

Commit

Permalink
feat(MonitoringSubsystem): provide Micrometer gauges for rendering.world
Browse files Browse the repository at this point in the history
  • Loading branch information
keturn committed Nov 15, 2021
1 parent 23337fa commit 6098b45
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

package org.terasology.engine.core.subsystem.common;

import java.util.Set;

/**
* Describes the set of gauges that may apply to a particular interface.
*/
public class GaugeMapEntry {
public final Class<?> iface;
public final Set<GaugeSpec<?>> gaugeSpecs;

@SafeVarargs
public <T> GaugeMapEntry(Class<T> iface, GaugeSpec<? extends T>... gaugeSpecs) {
this.iface = iface;
this.gaugeSpecs = Set.of(gaugeSpecs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

package org.terasology.engine.core.subsystem.common;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;

import java.util.function.ToDoubleFunction;

import static com.google.common.base.Preconditions.checkArgument;

/**
* The information that defines a Gauge.
* <p>
* The Micrometer API doesn't let you define a Gauge without connecting it to
* some MeterRegistry. This class provides an immutable record of all<sup>*</sup>
* the properties of a Gauge, facilitating a more data-driven approach.
* <p>
* * <i>All the ones we use so far, anyway.</i>
*
* @param <T> the type this gauge reads from
*/
public class GaugeSpec<T> {
public final String name;
public final String description;
public final ToDoubleFunction<T> valueFunction;
public final String baseUnit;

protected final Class<T> subjectType;

public GaugeSpec(String name, String description, ToDoubleFunction<T> valueFunction) {
this(name, description, valueFunction, null);
}

/** @see Gauge.Builder */
public GaugeSpec(String name, String description, ToDoubleFunction<T> valueFunction, String baseUnit) {
this.name = name;
this.description = description;
this.valueFunction = valueFunction;
this.baseUnit = baseUnit;
this.subjectType = getSubjectClass();
}

public Gauge register(MeterRegistry registry, T subject) {
return Gauge.builder(name, subject, valueFunction)
.description(description)
.baseUnit(baseUnit)
.register(registry);
}

/**
* Creates a MeterBinder for this gauge.
* <p>
* This allows us to make things with the same interface as the meters
* provided by {@link io.micrometer.core.instrument.binder}.
*
* @param subject passed to this gauge's {@link #valueFunction}
* @return call to bind this gauge to a MeterRegistry
*/
public MeterBinder binder(T subject) {
return registry -> register(registry, subject);
}

public <U> MeterBinder binderAfterCasting(U subject) {
checkArgument(isInstanceOfType(subject));
T safeSubject = subjectType.cast(subject);
return binder(safeSubject);
}

public boolean isInstanceOfType(Object object) {
return subjectType.isInstance(object);
}

@SafeVarargs
private Class<T> getSubjectClass(T...t) {
// Thank you https://stackoverflow.com/a/40917725 for this amazing kludge
//noinspection unchecked
return (Class<T>) t.getClass().getComponentType();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,38 @@
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import io.micrometer.jmx.JmxConfig;
import io.micrometer.jmx.JmxMeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.engine.config.SystemConfig;
import org.terasology.engine.context.Context;
import org.terasology.engine.core.GameEngine;
import org.terasology.engine.core.Time;
import org.terasology.engine.core.subsystem.EngineSubsystem;
import org.terasology.engine.monitoring.gui.AdvancedMonitor;
import reactor.core.publisher.Flux;
import reactor.function.TupleUtils;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;

import java.time.Duration;
import java.util.List;
import java.util.Set;

public class MonitoringSubsystem implements EngineSubsystem {

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

private static final Logger logger = LoggerFactory.getLogger(MonitoringSubsystem.class);

protected MeterRegistry meterRegistry;
private AdvancedMonitor advancedMonitor;

@Override
Expand All @@ -39,29 +51,23 @@ public void initialise(GameEngine engine, Context rootContext) {
advancedMonitor = new AdvancedMonitor();
advancedMonitor.setVisible(true);
}
meterRegistry = initMeterRegistries();
}

initMicrometerMetrics(rootContext.get(Time.class));
@Override
public void postInitialise(Context context) {
initMeters(context);
}

/**
* Initialize Micrometer metrics and publishers.
*
* @see org.terasology.engine.core.GameScheduler GameScheduler for global Reactor metrics
* @param time provides statistics
* <p>
* Note {@link org.terasology.engine.core.EngineTime EngineTime}
* does not serve the same role as a {@link Clock micrometer Clock}.
* (Not yet. Maybe it should?)
*/
private void initMicrometerMetrics(Time time) {
protected MeterRegistry initMeterRegistries() {
// Register metrics with the built-in global composite registry.
// This makes them available to any agent(s) we add to it.
MeterRegistry meterRegistry = Metrics.globalRegistry;

Gauge.builder("terasology.fps", time::getFps)
.description("framerate")
.baseUnit("Hz")
.register(meterRegistry);
MeterRegistry registry = Metrics.globalRegistry;

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

return registry;
// If we want to make global metrics available to our custom view,
// we add our custom registry to the global composite:
//
// Metrics.addRegistry(DebugOverlay.meterRegistry);
//
// If we want to see JVM metrics there as well:
//
// initAllJvmMetrics(DebugOverlay.meterRegistry);
// allJvmMetrics().forEach(m -> m.bindTo(DebugOverlay.meterRegistry));
}

/** Initialize meters for all the things in this Context. */
protected void initMeters(Context context) {
// We can build meters individually like this:
var time = context.get(Time.class);
Gauge.builder("terasology.fps", time::getFps)
.description("framerate")
.baseUnit("Hz")
.register(meterRegistry);

// But we'd like the code for meters to live closer to the implementation
// of the thing they're monitoring.
//
// Somewhere we get a list of all the things that provide meters.
// Maybe hardcoded, maybe a registry of some kind? Modules will want
// to contribute as well.
var meterMaps = List.of(
org.terasology.engine.rendering.world.Meters.GAUGE_MAP
);

meterMaps.forEach(gaugeMap -> registerForContext(context, gaugeMap));
}

protected void registerForContext(Context context, Iterable<GaugeMapEntry> gaugeMap) {
Flux.fromIterable(gaugeMap)
.map(entry -> Tuples.of(context.get(entry.iface), entry.gaugeSpecs))
.filter(TupleUtils.predicate((subject, specs) -> subject != null))
.doOnDiscard(Tuple2.class, TupleUtils.consumer((iface, gaugeSpecs) ->
logger.debug("Not building gauges for {}, none was in {}", iface, context)))
.subscribe(TupleUtils.consumer(this::registerAll));
}

protected <T> void registerAll(T subject, Set<GaugeSpec<? extends T>> gaugeSpecs) {
Flux.fromIterable(gaugeSpecs)
.filter(spec -> spec.isInstanceOfType(subject))
// Make sure the gauge is right for the specific type.
.map(spec -> spec.binderAfterCasting(subject))
.subscribe(this::registerMeter);
}

public void registerMeter(MeterBinder meterBinder) {
meterBinder.bindTo(meterRegistry);
}

/**
Expand All @@ -95,12 +145,14 @@ public Duration step() {
* have a different agent you want them published through.
*/
@SuppressWarnings("unused")
void initAllJvmMetrics(MeterRegistry registry) {
new ClassLoaderMetrics().bindTo(registry);
new JvmMemoryMetrics().bindTo(registry);
new JvmGcMetrics().bindTo(registry);
new JvmThreadMetrics().bindTo(registry);
new ProcessorMetrics().bindTo(registry);
List<MeterBinder> allJvmMetrics() {
return List.of(
new ClassLoaderMetrics(),
new JvmMemoryMetrics(),
new JvmGcMetrics(),
new JvmThreadMetrics(),
new ProcessorMetrics()
);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

package org.terasology.engine.rendering.world;

import io.micrometer.core.instrument.binder.BaseUnits;
import org.terasology.engine.core.subsystem.common.GaugeMapEntry;
import org.terasology.engine.core.subsystem.common.GaugeSpec;

import java.util.List;

public final class Meters {
public static final String PREFIX = Meters.class.getPackageName();

public static final List<GaugeMapEntry> GAUGE_MAP = List.of(
new GaugeMapEntry(WorldRenderer.class,
new GaugeSpec<WorldRendererImpl>(
PREFIX + ".emptyMeshChunks",
"Empty Mesh Chunks",
wri -> wri.statChunkMeshEmpty,
BaseUnits.OBJECTS),
new GaugeSpec<WorldRendererImpl>(
PREFIX + ".unreadyChunks",
"Unready Chunks",
wri -> wri.statChunkNotReady,
BaseUnits.OBJECTS),
new GaugeSpec<WorldRendererImpl>(
PREFIX + ".triangles",
"Rendered Triangles",
wri -> wri.statRenderedTriangles,
BaseUnits.OBJECTS)
),
new GaugeMapEntry(RenderableWorld.class,
new GaugeSpec<RenderableWorldImpl>(
PREFIX + ".visibleChunks",
"Visible Chunks",
rwi -> rwi.statVisibleChunks,
BaseUnits.OBJECTS),
new GaugeSpec<RenderableWorldImpl>(
PREFIX + ".dirtyChunks",
"Dirty Chunks",
rwi -> rwi.statDirtyChunks,
BaseUnits.OBJECTS),
new GaugeSpec<RenderableWorldImpl>(
PREFIX + ".dirtyChunks",
"Ignored Phases",
rwi -> rwi.statIgnoredPhases)
)
);

private Meters() { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class RenderableWorldImpl implements RenderableWorld {
ViewDistance.MEGA.getChunkDistance().x() * ViewDistance.MEGA.getChunkDistance().y() * ViewDistance.MEGA.getChunkDistance().z();
private static final Vector3fc CHUNK_CENTER_OFFSET = new Vector3f(Chunks.CHUNK_SIZE).div(2);

int statDirtyChunks;
int statVisibleChunks;
int statIgnoredPhases;

private final int maxChunksForShadows =
TeraMath.clamp(CoreRegistry.get(Config.class).getRendering().getMaxChunksUsedForShadowMapping(), 64, 1024);

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

private int statDirtyChunks;
private int statVisibleChunks;
private int statIgnoredPhases;


RenderableWorldImpl(Context context, Camera playerCamera) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public final class WorldRendererImpl implements WorldRenderer {
*/
private static final Logger logger = LoggerFactory.getLogger(WorldRendererImpl.class);
private static final float GROUND_PLANE_HEIGHT_DISPARITY = -0.7f;

int statChunkMeshEmpty;
int statChunkNotReady;
int statRenderedTriangles;

private RenderGraph renderGraph;
private RenderingModuleRegistry renderingModuleRegistry;

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

private float millisecondsSinceRenderingStart;
private float secondsSinceLastFrame;
private int statChunkMeshEmpty;
private int statChunkNotReady;
private int statRenderedTriangles;

private final RenderingConfig renderingConfig;
private final Console console;
Expand Down

0 comments on commit 6098b45

Please sign in to comment.