Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(MonitoringSubsystem): provide Micrometer gauges for rendering.world #4950

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
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,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,62 +6,66 @@
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 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
public String getName() {
return "Monitoring";
}

@Override
public void preInitialise(Context rootContext) {
// FIXME: `@Share` is not implemented for EngineSubsystems?
rootContext.put(MonitoringSubsystem.class, this);
}

@Override
public void initialise(GameEngine engine, Context rootContext) {
if (rootContext.get(SystemConfig.class).monitoringEnabled.get()) {
advancedMonitor = new AdvancedMonitor();
advancedMonitor.setVisible(true);
}

initMicrometerMetrics(rootContext.get(Time.class));
meterRegistry = initMeterRegistries();
}

/**
* 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 +81,61 @@ 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. */
public 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) {
for (GaugeMapEntry entry : gaugeMap) {
var subject = context.get(entry.iface);
if (subject != null) {
registerAll(subject, entry.gaugeSpecs);
} else {
logger.warn("Not building gauges for {}, none was in {}", entry.iface, context);
}
}
}

public <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) {
logger.debug("Binding {} to {}", meterBinder, meterRegistry);
meterBinder.bindTo(meterRegistry);
}

/**
Expand All @@ -95,12 +146,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
Expand Up @@ -3,10 +3,13 @@
package org.terasology.engine.rendering.nui.layers.ingame.metrics;

import com.google.common.base.Preconditions;
import org.terasology.engine.context.Context;
import org.terasology.engine.core.subsystem.common.MonitoringSubsystem;
import org.terasology.engine.entitySystem.systems.BaseComponentSystem;
import org.terasology.engine.entitySystem.systems.RegisterSystem;
import org.terasology.gestalt.module.sandbox.API;
import org.terasology.engine.registry.In;
import org.terasology.engine.registry.Share;
import org.terasology.gestalt.module.sandbox.API;

import java.util.ArrayList;

Expand All @@ -25,6 +28,12 @@
@API
public class DebugMetricsSystem extends BaseComponentSystem {

@In
MonitoringSubsystem monitoringSubsystem;

@In
Context context;

private final MetricsMode defaultMode = new NullMetricsMode();
private ArrayList<MetricsMode> modes;
private MetricsMode currentMode;
Expand All @@ -43,8 +52,21 @@ public void initialise() {
register(new HeapAllocationMode());
register(new RenderingExecTimeMeansMode("\n- Rendering - Execution Time: Running Means - Sorted Alphabetically -"));
currentMode = defaultMode;

if (monitoringSubsystem == null) {
monitoringSubsystem = context.get(MonitoringSubsystem.class);
}
}

@Override
public void preBegin() {
monitoringSubsystem.initMeters(context);
}

@Override
public void shutdown() {
// FIXME: Remove all the meters we added to the global registry.
}

/**
* Adds a MetricsMode instance to the set. Use the toggle() and getCurrentMode() methods to iterate over the set and
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
Loading