Created metrics for tracking CPU and GPU time

Rewrote artemis-odb-contrib/contrib-plugin-profiler
Profiler was too restrictive and privated many useful fields and had static methods, yuk
Profiler plugin will always run and the key will only hide the UI (and set it as GPU system)
Profiler plugin will maybe be modified in the future to be more extensible
Extended profiler plugin to support tagging systems as GPU systems
I'd like to know which systems are slow and track CPU/GPU bottlenecks on mobile
This commit is contained in:
Collin Smith 2020-06-11 01:19:49 -07:00
parent b1a3411073
commit fcd4fa607f
16 changed files with 1083 additions and 8 deletions

View File

@ -449,6 +449,8 @@ public class Client extends Game {
.append(Gdx.graphics.getFramesPerSecond())
.append('\n').append("Ping: ").append(Riiablo.metrics.ping).append(" ms")
.append('\n').append("RTT: ").append(Riiablo.metrics.rtt).append(" ms")
.append('\n').append(String.format("CPU: %.1f ms", Riiablo.metrics.cpu))
.append('\n').append(String.format("GPU: %.1f ms", Riiablo.metrics.gpu))
;
fps.setText(font, builder.toString());
int drawFpsMethod = this.drawFpsMethod;

View File

@ -1,6 +1,8 @@
package com.riiablo;
public class Metrics {
public float cpu;
public float gpu;
public long ping;
public long rtt;

View File

@ -4,18 +4,22 @@ import com.artemis.ComponentMapper;
import com.artemis.annotations.All;
import com.artemis.annotations.Wire;
import com.artemis.systems.IteratingSystem;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Array;
import com.riiablo.Riiablo;
import com.riiablo.camera.IsometricCamera;
import com.riiablo.engine.client.component.Hovered;
import com.riiablo.engine.client.component.Label;
import com.riiablo.engine.server.component.Position;
import com.riiablo.map.RenderSystem;
import com.riiablo.profiler.GpuSystem;
@GpuSystem
@All({Hovered.class, Label.class, Position.class})
public class LabelManager extends IteratingSystem {
protected ComponentMapper<Label> mLabel;

View File

@ -6,10 +6,12 @@ import com.artemis.annotations.All;
import com.artemis.annotations.Exclude;
import com.artemis.annotations.Wire;
import com.artemis.utils.IntBag;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.utils.Align;
import com.riiablo.Riiablo;
import com.riiablo.camera.IsometricCamera;
import com.riiablo.codec.excel.MonStats;
@ -18,8 +20,10 @@ import com.riiablo.engine.server.component.Monster;
import com.riiablo.engine.server.component.Position;
import com.riiablo.graphics.PaletteIndexedBatch;
import com.riiablo.graphics.PaletteIndexedColorDrawable;
import com.riiablo.profiler.GpuSystem;
import com.riiablo.widget.Label;
@GpuSystem
@All({Monster.class, Hovered.class, Position.class})
@Exclude(com.riiablo.engine.client.component.Label.class)
public class MonsterLabelManager extends BaseEntitySystem {

View File

@ -2,13 +2,17 @@ package com.riiablo.engine.client.debug;
import com.artemis.BaseSystem;
import com.artemis.annotations.Wire;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.riiablo.camera.IsometricCamera;
import com.riiablo.map.Box2DPhysics;
import com.riiablo.map.RenderSystem;
import com.riiablo.profiler.GpuSystem;
@GpuSystem
public class Box2DDebugger extends BaseSystem {
private static final float BOX2D_ZOOM_FACTOR = 0.04419419f;

View File

@ -4,10 +4,12 @@ import com.artemis.ComponentMapper;
import com.artemis.annotations.All;
import com.artemis.annotations.Wire;
import com.artemis.systems.IteratingSystem;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Align;
import com.riiablo.Riiablo;
import com.riiablo.camera.IsometricCamera;
import com.riiablo.engine.server.component.PathWrapper;
@ -15,7 +17,9 @@ import com.riiablo.engine.server.component.Position;
import com.riiablo.graphics.PaletteIndexedBatch;
import com.riiablo.map.DS1;
import com.riiablo.map.RenderSystem;
import com.riiablo.profiler.GpuSystem;
@GpuSystem
@All({PathWrapper.class, Position.class})
public class PathDebugger extends IteratingSystem {
private final float BOX_SIZE = 8;

View File

@ -4,10 +4,12 @@ import com.artemis.ComponentMapper;
import com.artemis.annotations.All;
import com.artemis.annotations.Wire;
import com.artemis.systems.IteratingSystem;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Align;
import com.riiablo.Riiablo;
import com.riiablo.camera.IsometricCamera;
import com.riiablo.engine.server.component.Pathfind;
@ -16,7 +18,9 @@ import com.riiablo.graphics.PaletteIndexedBatch;
import com.riiablo.map.RenderSystem;
import com.riiablo.map.pfa.GraphPath;
import com.riiablo.map.pfa.Point2;
import com.riiablo.profiler.GpuSystem;
@GpuSystem
@All({Pathfind.class, Position.class})
public class PathfindDebugger extends IteratingSystem {
private final float BOX_SIZE = 8;

View File

@ -2,10 +2,14 @@ package com.riiablo.engine.client.debug;
import com.artemis.BaseSystem;
import com.artemis.annotations.Wire;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.riiablo.camera.IsometricCamera;
import com.riiablo.map.RenderSystem;
import com.riiablo.profiler.GpuSystem;
@GpuSystem
public class RenderSystemDebugger extends BaseSystem {
protected RenderSystem renderer;

View File

@ -1,11 +1,16 @@
package com.riiablo.map;
import java.util.Arrays;
import java.util.Comparator;
import org.apache.commons.lang3.StringUtils;
import com.artemis.Aspect;
import com.artemis.BaseEntitySystem;
import com.artemis.ComponentMapper;
import com.artemis.EntitySubscription;
import com.artemis.annotations.All;
import com.artemis.utils.IntBag;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
@ -20,6 +25,7 @@ import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Bits;
import com.badlogic.gdx.utils.Pools;
import com.riiablo.Riiablo;
import com.riiablo.camera.IsometricCamera;
import com.riiablo.codec.Animation;
@ -41,13 +47,10 @@ import com.riiablo.engine.server.component.Position;
import com.riiablo.graphics.BlendMode;
import com.riiablo.graphics.PaletteIndexedBatch;
import com.riiablo.map.DT1.Tile;
import com.riiablo.profiler.GpuSystem;
import com.riiablo.util.DebugUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.Comparator;
@GpuSystem
@All(AnimationWrapper.class)
public class RenderSystem extends BaseEntitySystem {
private static final String TAG = "RenderSystem";

View File

@ -0,0 +1,10 @@
package com.riiablo.profiler;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface GpuSystem {}

View File

@ -0,0 +1,83 @@
package com.riiablo.profiler;
import com.artemis.BaseSystem;
import com.artemis.SystemInvocationStrategy;
import com.artemis.utils.Bag;
import com.artemis.utils.ImmutableBag;
/**
* {@link SystemInvocationStrategy} that will create a profiler for all systems that don't already
* have one Can be used in addition to or instead of {@link com.artemis.annotations.Profile}
* annotation
* <p>
* In addition creates {@link SystemProfiler} with name "Frame" for total frame time It can be
* accessed with {@link SystemProfiler#get(String)}
*
* @author piotr-j
* @author Daan van Yperen
*/
public class ProfilerInvocationStrategy extends SystemInvocationStrategy {
private boolean initialized = false;
protected SystemProfiler frameProfiler;
protected SystemProfiler[] profilers;
@Override
protected void process() {
if (!initialized) {
initialize();
initialized = true;
}
frameProfiler.start();
processProfileSystems(systems);
frameProfiler.stop();
}
private void processProfileSystems(Bag<BaseSystem> systems) {
final Object[] systemsData = systems.getData();
for (int i = 0, s = systems.size(); s > i; i++) {
if (disabled.get(i))
continue;
updateEntityStates();
processProfileSystem(profilers[i], (BaseSystem) systemsData[i]);
}
updateEntityStates();
}
private void processProfileSystem(SystemProfiler profiler, BaseSystem system) {
if (profiler != null) profiler.start();
system.process();
if (profiler != null) profiler.stop();
}
@Override
protected void initialize() {
createFrameProfiler();
createSystemProfilers();
}
private void createSystemProfilers() {
final ImmutableBag<BaseSystem> systems = world.getSystems();
profilers = new SystemProfiler[systems.size()];
for (int i = 0; i < systems.size(); i++) {
profilers[i] = createSystemProfiler(systems.get(i));
}
}
private SystemProfiler createSystemProfiler(BaseSystem system) {
SystemProfiler old = SystemProfiler.getFor(system);
if (old == null) {
old = SystemProfiler.createFor(system, world);
}
return old;
}
private void createFrameProfiler() {
frameProfiler = SystemProfiler.create("Frame");
frameProfiler.setColor(1, 1, 1, 1);
}
}

View File

@ -0,0 +1,26 @@
package com.riiablo.profiler;
import com.artemis.ArtemisPlugin;
import com.artemis.WorldConfigurationBuilder;
/**
* Artemis system profiler.
*
* Tracks performance of artemis systems and displays it in a line graph. Overhead is insignificant
* while closed.
*
* Does not require {@see @com.artemis.annotations.Profile} on systems.
*
* Open/Close with P by default.
*
* @author piotr-j (Plugin)
* @author Daan van Yperen (Integration)
*/
public class ProfilerPlugin implements ArtemisPlugin {
@Override
public void setup(WorldConfigurationBuilder b) {
b.register(new ProfilerInvocationStrategy());
b.dependsOn(WorldConfigurationBuilder.Priority.LOWEST + 1000, ProfilerSystem.class);
}
}

View File

@ -0,0 +1,145 @@
package com.riiablo.profiler;
import com.artemis.BaseSystem;
import com.artemis.annotations.Wire;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.riiablo.Riiablo;
/**
* Example profiling system.
*
* @author piotr-j
* @author Daan van Yperen
* @see ProfilerPlugin
*/
@Wire
public class ProfilerSystem extends BaseSystem {
public static final int DEFAULT_PROFILER_KEY = Input.Keys.P;
OrthographicCamera camera;
ShapeRenderer renderer;
Stage stage;
Skin skin;
private int key = DEFAULT_PROFILER_KEY;
SystemProfilerGUI gui;
private boolean f3ButtonDown;
@Override
protected void initialize() {
camera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
camera.setToOrtho(false);
camera.update();
renderer = new ShapeRenderer();
stage = new Stage();
stage.getBatch().setProjectionMatrix(camera.combined);
skin = new Skin(Gdx.files.classpath("net/mostlyoriginal/plugin/profiler/skin/uiskin.json"));
// setup some static config like colors etc
SystemProfilerGUI.GRAPH_H_LINE.set(Color.ORANGE);
gui = new SystemProfilerGUI(skin, "default");
gui.setResizeBorder(8);
gui.show(stage);
gui.setWidth(Gdx.graphics.getWidth());
}
@Override
protected void processSystem() {
Riiablo.metrics.cpu = 0;
Riiablo.metrics.gpu = 0;
if (!isEnabled() || !isConfigured()) {
return;
}
final SystemProfiler[] profilers = SystemProfiler.get().items;
for (int i = 0, s = SystemProfiler.size(); i < s; i++) {
SystemProfiler profiler = profilers[i];
if (profiler.system == null || !profiler.system.isEnabled()) continue;
if (profiler.gpu) {
Riiablo.metrics.gpu += profiler.getMovingAvg();
} else {
Riiablo.metrics.cpu += profiler.getMovingAvg();
}
}
checkActivationButton();
if (gui.isVisible()) {
processInput();
render();
}
}
private boolean isConfigured() {
return gui.getParent() != null;
}
private void render() {
stage.act(world.delta);
stage.draw();
renderer.setProjectionMatrix(camera.combined);
renderer.begin(ShapeRenderer.ShapeType.Line);
gui.updateAndRender(world.delta, renderer);
renderer.end();
}
private void checkActivationButton() {
if (Gdx.input.isKeyPressed(key)) {
if (!f3ButtonDown) {
if (!gui.isVisible()) {
gui.setHeight(Gdx.graphics.getHeight() / 2);
gui.setVisible(true);
SystemProfiler.getFor(this).gpu = true;
} else if (gui.getHeight() != Gdx.graphics.getHeight()) {
gui.setHeight(Gdx.graphics.getHeight());
} else {
gui.setVisible(false);
SystemProfiler.getFor(this).gpu = false;
}
}
f3ButtonDown = true;
} else f3ButtonDown = false;
}
private boolean leftMouseDown;
/**
* Emulate stage input to maintain pre-existing input processor.
*/
private void processInput() {
if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) {
if (!leftMouseDown) {
leftMouseDown = true;
stage.touchDown(Gdx.input.getX(), Gdx.input.getY(), 0, Input.Buttons.LEFT);
} else {
stage.touchDragged(Gdx.input.getX(), Gdx.input.getY(), 0);
}
} else if (leftMouseDown) {
leftMouseDown = false;
stage.touchUp(Gdx.input.getX(), Gdx.input.getY(), 0, Input.Buttons.LEFT);
}
}
@Override
protected void dispose() {
SystemProfiler.dispose();
}
public int getKey() {
return key;
}
public void setKey(int key) {
this.key = key;
}
}

View File

@ -0,0 +1,374 @@
package com.riiablo.profiler;
import com.artemis.BaseSystem;
import com.artemis.World;
import com.artemis.utils.ArtemisProfiler;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.TimeUtils;
import com.riiablo.util.ClassUtils;
/**
* {@link ArtemisProfiler} implementation, {@link SystemProfiler#dispose()} should be called to
* clean static references as needed
*
* @author piotr-j
*/
public class SystemProfiler implements ArtemisProfiler {
/**
* Samples to store per system, only changes before initialization have effect
*/
public static int SAMPLES = 60 * 5;
private static boolean RUNNING = true;
private static Array<SystemProfiler> profilers = new Array<>(SystemProfiler.class);
private static ObjectMap<String, SystemProfiler> profilerByName = new ObjectMap<>();
/**
* Add manually created profiler
*
* @param profiler to add
*
* @return added profiler
*/
public static SystemProfiler add(SystemProfiler profiler) {
if (profiler.added) return profiler;
profiler.added = true;
profilers.add(profiler);
profilerByName.put(profiler.getName(), profiler);
return profiler;
}
/**
* @param name of the profiler
*
* @return profiler registered with given name or null
*/
public static SystemProfiler get(String name) {
return profilerByName.get(name, null);
}
/**
* @param index of profiler to get, no bounds check!
*
* @return profiler with given index
*/
public static SystemProfiler get(int index) {
return profilers.get(index);
}
/**
* Get profiler for given system
*
* @return profiler or null
*/
public static SystemProfiler getFor(BaseSystem system) {
Object[] items = profilers.items;
for (int i = 0; i < profilers.size; i++) {
SystemProfiler profiler = (SystemProfiler) items[i];
if (profiler.system == system) {
return profiler;
}
}
return null;
}
/**
* Create a profiler with given name
*
* @param name of the profiler
*/
public static SystemProfiler create(String name) {
return SystemProfiler.add(new SystemProfiler(name));
}
/**
* Create a profiler for given system
*
* @param system to profiler
* @param world to init profiler with
*
* @return created profiler
*/
public static SystemProfiler createFor(BaseSystem system, World world) {
return SystemProfiler.add(new SystemProfiler(system, world));
}
/**
* @return {@link Array} with all registered profilers
*/
public static Array<SystemProfiler> get() {
return profilers;
}
/**
* @return number of registered profilers
*/
public static int size() {
return profilers.size;
}
/**
* Pause all profilers
*/
public static void pause() {
RUNNING = false;
}
/**
* Resume all profilers
*/
public static void resume() {
RUNNING = true;
}
/**
* @return if profilers are running
*/
public static boolean isRunning() {
return RUNNING;
}
/**
* Clear registered profilers, should be called when {@link World} is disposed
*/
public static void dispose() {
profilers.clear();
profilerByName.clear();
}
/*
If this profiler was already added via SystemProfiler.add()
*/
private boolean added;
protected long[] times = new long[SAMPLES];
protected int index;
protected long max;
protected int lastMaxCounter;
protected long localMax;
protected long localMaxIndex;
protected int samples;
protected long total;
protected Color color;
protected String name;
protected BaseSystem system;
protected boolean gpu;
public SystemProfiler() {}
/**
* Create not initialized profiler, must be initialized with {@link
* SystemProfiler#initialize(BaseSystem, World)}
*
* @param name of the profiler
*/
public SystemProfiler(String name) {
this.name = name;
}
/**
* Create profiler with default name and initialize it
*
* @param system to profiler
* @param world to init with
*/
public SystemProfiler(BaseSystem system, World world) {
this(null, system, world);
}
/**
* Create profiler with specified name and initialize it
*
* @param name of the profiler
* @param system to profiler
* @param world to init with
*/
public SystemProfiler(String name, BaseSystem system, World world) {
this.name = name;
initialize(system, world);
}
@Override
public void initialize(BaseSystem baseSystem, World world) {
system = baseSystem;
gpu = ClassUtils.hasAnnotation(baseSystem.getClass(), GpuSystem.class);
if (name == null) {
name = toString();
}
if (color == null) {
calculateColor(toString().hashCode(), color = new Color());
}
SystemProfiler.add(this);
}
private long startTime;
@Override
public void start() {
startTime = TimeUtils.nanoTime();
}
@Override
public void stop() {
long time = TimeUtils.nanoTime() - startTime;
if (RUNNING) sample(time);
}
public long getAverage() {
return samples == 0 ? 0 : total / Math.min(times.length, samples);
}
public float getMax() {
return max / 1000000f;
}
public float getLocalMax() {
return localMax / 1000000f;
}
public float getMovingAvg() {
return getAverage() / 1000000f;
}
/**
* Create a sample with specified time
*
* @param time in nanoseconds
*/
public void sample(long time) {
lastMaxCounter++;
if (time > max || lastMaxCounter > 2000) {
max = time;
lastMaxCounter = 0;
}
if (time > localMax || index == localMaxIndex) {
localMax = time;
localMaxIndex = index;
}
total -= times[index];
samples++;
times[index] = time;
total += time;
if (++index == times.length) {
index = 0;
}
}
/**
* Add time to current sample
*
* @param time in nanoseconds
*/
public void add(long time) {
times[index] += time;
total += time;
}
public int getCurrentSampleIndex() {
return index;
}
public int getTotalSamples() {
return samples;
}
public long[] getSampleData() {
return times;
}
@Override
public String toString() {
return name != null ? name :
system != null ? system.getClass().getSimpleName() : "<dummy>";
}
public String getName() {
return name;
}
/**
* @return color assigned to this profiler, maybe null if it is not initialized
*/
public Color getColor() {
return color;
}
public void setColor(float r, float g, float b, float a) {
if (color == null) {
color = new Color();
}
color.set(r, g, b, a);
}
boolean drawGraph = true;
public boolean getDrawGraph() {
return drawGraph;
}
public void setDrawGraph(boolean drawGraph) {
this.drawGraph = drawGraph;
}
/**
* Calculates semi unique color from given hash
*/
public static Color calculateColor(int hash, Color color) {
float hue = (hash % 333) / 333f;
float saturation = ((hash % 271) / 271f) * 0.2f + 0.8f;
float brightness = ((hash % 577) / 577f) * 0.1f + 0.9f;
int r = 0, g = 0, b = 0;
if (saturation == 0) {
r = g = b = (int) (brightness * 255.0f + 0.5f);
} else {
float h = (hue - (float) Math.floor(hue)) * 6.0f;
float f = h - (float) Math.floor(h);
float p = brightness * (1.0f - saturation);
float q = brightness * (1.0f - saturation * f);
float t = brightness * (1.0f - (saturation * (1.0f - f)));
switch ((int) h) {
case 0:
r = (int) (brightness * 255.0f + 0.5f);
g = (int) (t * 255.0f + 0.5f);
b = (int) (p * 255.0f + 0.5f);
break;
case 1:
r = (int) (q * 255.0f + 0.5f);
g = (int) (brightness * 255.0f + 0.5f);
b = (int) (p * 255.0f + 0.5f);
break;
case 2:
r = (int) (p * 255.0f + 0.5f);
g = (int) (brightness * 255.0f + 0.5f);
b = (int) (t * 255.0f + 0.5f);
break;
case 3:
r = (int) (p * 255.0f + 0.5f);
g = (int) (q * 255.0f + 0.5f);
b = (int) (brightness * 255.0f + 0.5f);
break;
case 4:
r = (int) (t * 255.0f + 0.5f);
g = (int) (p * 255.0f + 0.5f);
b = (int) (brightness * 255.0f + 0.5f);
break;
case 5:
r = (int) (brightness * 255.0f + 0.5f);
g = (int) (p * 255.0f + 0.5f);
b = (int) (q * 255.0f + 0.5f);
break;
}
}
return color.set(r / 255f, g / 255f, b / 255f, 1);
}
}

View File

@ -0,0 +1,405 @@
package com.riiablo.profiler;
import java.util.Comparator;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.Window;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Sort;
/**
* Gui for SystemProfilers implemented in Scene2d
*
* Graph must be renderer separately with shape renderer, after stage.draw()
*
* Certain static values can be changed before creation to modify behaviour
*
* Dynamic modification of profilers is not supported
*
* See example implementation of how this class can be used
*
* @author piotr-j
*/
public class SystemProfilerGUI extends Window {
public static final Color GRAPH_V_LINE = new Color(0.6f, 0.6f, 0.6f, 1);
public static final Color GRAPH_H_LINE = new Color(0.25f, 0.25f, 0.25f, 1);
public static float FADE_TIME = 0.3f;
public static float PRECISION = 0.01f;
public static String FORMAT = "%.2f";
public static String STYLE_SMALL = "default";
/**
* Min width of label with values
*/
public static float MIN_LABEL_WIDTH = 75;
public static float GRAPH_MIN_WIDTH = 300;
public static float GRAPH_MIN_HEIGHT = 200;
/**
* How many systems to graph at most
*/
public static int DRAW_MAX_COUNT = 15;
/**
* How often should text update
*/
public static float REFRESH_RATE = 0.25f;
protected Skin skin;
protected Table profilerLabels;
protected Graph graph;
protected Table profilersTable;
protected Array<ProfilerRow> rows = new Array<>();
public SystemProfilerGUI(Skin skin, String style) {
super("Profiler", skin, style);
this.skin = skin;
setVisible(false);
setResizable(true);
setResizeBorder(12);
TextButton closeButton = new TextButton("X", skin);
getTitleTable().add(closeButton).padRight(3);
closeButton.addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
hide();
}
});
Table graphTable = new Table();
Table graphLabels = new Table();
for (int i = 32; i >= 0; i /= 2) {
graphLabels.add(label(Integer.toString(i), skin)).expandY().center().row();
if (i == 0) break;
}
graphTable.add(graphLabels).expandY().fillY();
graphTable.add(graph = new Graph()).expand().fill();
profilerLabels = new Table();
profilerLabels.add().expandX().fillX();
profilerLabels.add(label("max", skin, Align.right)).minWidth(MIN_LABEL_WIDTH);
profilerLabels.add(label("lmax", skin, Align.right)).minWidth(MIN_LABEL_WIDTH);
profilerLabels.add(label("avg", skin, Align.right)).minWidth(MIN_LABEL_WIDTH);
for (SystemProfiler profiler : SystemProfiler.get()) {
rows.add(new ProfilerRow(profiler, skin));
}
profilersTable = new Table();
// basic once so we can get all profilers and can pack nicely
act(0);
ScrollPane pane = new ScrollPane(profilersTable);
pane.setScrollingDisabled(true, false);
add(graphTable).expand().fill();
add(pane).fillX().pad(0, 10, 10, 10).top()
.prefWidth(MIN_LABEL_WIDTH * 7).minWidth(0);
pack();
}
private static Label label(String text, Skin skin) {
return label(text, skin, Align.left);
}
private static Label label(String text, Skin skin, int align) {
Label label = new Label(text, skin, STYLE_SMALL);
label.setAlignment(align);
return label;
}
public void updateAndRender(float delta, ShapeRenderer renderer) {
update(delta);
renderGraph(renderer);
}
float refreshTimer = REFRESH_RATE;
Comparator<ProfilerRow> byAvg = new Comparator<ProfilerRow>() {
@Override
public int compare(ProfilerRow o1, ProfilerRow o2) {
return (int) (o2.getAverage() - o1.getAverage());
}
};
/**
* Call to update, rate limited by {@link SystemProfilerGUI#REFRESH_RATE}
*
* This is not in {@link Window#act(float)} to avoid polluting results of actual system with stage
* if one exists
*
* @param delta duration of last frame
*/
public void update(float delta) {
refreshTimer += delta;
if (refreshTimer < REFRESH_RATE) return;
refreshTimer -= REFRESH_RATE;
if (rows.size != SystemProfiler.size()) {
rebuildRows();
}
Sort.instance().sort(rows, byAvg);
profilersTable.clear();
profilersTable.add(profilerLabels).expandX().fillX().right();
profilersTable.row();
for (ProfilerRow row : rows) {
row.update();
profilersTable.add(row).expandX().fillX().left();
profilersTable.row();
}
}
private void rebuildRows() {
int target = SystemProfiler.size();
if (target > rows.size) {
for (int i = rows.size; i < target; i++) {
rows.add(new ProfilerRow(skin));
}
} else if (target < rows.size) {
rows.removeRange(rows.size - target + 1, rows.size - 1);
}
for (int i = 0; i < target; i++) {
SystemProfiler profiler = SystemProfiler.get(i);
rows.get(i).init(profiler);
}
}
private Vector2 temp = new Vector2();
/**
* Render graph for profilers, should be called after {@link Stage#draw()} so it is on top of the
* gui
*
* @param renderer {@link ShapeRenderer} to use, must be ready and set to Line type
*/
public void renderGraph(ShapeRenderer renderer) {
graph.localToStageCoordinates(temp.setZero());
drawGraph(renderer, temp.x, temp.y, graph.getWidth(), graph.getHeight(), getColor().a);
}
/**
* Render graph for profilers in a given bounds
*
* @param renderer {@link ShapeRenderer} to use, must be ready and set to Line type
*/
public static void drawGraph(ShapeRenderer renderer, float x, float y, float width, float height, float alpha) {
Gdx.gl.glEnable(GL20.GL_BLEND);
// we do this so the logical 0 and top are in the middle of the labels
drawGraphAxis(renderer, x, y, width, height, alpha);
float sep = height / 7;
y += sep / 2;
height -= sep;
graphProfileTimes(renderer, x, y, width, height, alpha);
}
private static void drawGraphAxis(ShapeRenderer renderer, float x, float y, float width, float height, float alpha) {
float sep = height / 7;
y += sep / 2;
renderer.setColor(GRAPH_V_LINE.r, GRAPH_V_LINE.g, GRAPH_V_LINE.b, alpha);
renderer.line(x, y, x, y + height - sep);
renderer.line(x + width, y, x + width, y + height - sep);
renderer.setColor(GRAPH_H_LINE.r, GRAPH_H_LINE.g, GRAPH_H_LINE.b, alpha);
for (int i = 0; i < 7; i++) {
renderer.line(x, y + i * sep, x + width, y + i * sep);
}
}
private static final float NANO_MULTI = 1 / 1000000f;
static Comparator<SystemProfiler> byLocalMax = new Comparator<SystemProfiler>() {
@Override
public int compare(SystemProfiler o1, SystemProfiler o2) {
return (int) (o2.getLocalMax() - o1.getLocalMax());
}
};
private static void graphProfileTimes(ShapeRenderer renderer, float x, float y, float width, float height, float alpha) {
Sort.instance().sort(SystemProfiler.get(), byLocalMax);
int drawn = 0;
for (SystemProfiler profiler : SystemProfiler.get()) {
if (!profiler.getDrawGraph())
continue;
if (drawn++ > DRAW_MAX_COUNT)
break;
renderer.setColor(profiler.getColor());
renderer.getColor().a = alpha;
// distance between 2 point
float sampleLen = width / profiler.times.length;
int current = profiler.getCurrentSampleIndex();
int skip = current;
long[] times = profiler.getSampleData();
float currentPoint = getPoint(times[current] * NANO_MULTI);
for (int i = times.length - 1; i >= 1; i--) {
int prev = current == 0 ? times.length - 1 : current - 1;
float prevPoint = getPoint(times[prev] * NANO_MULTI);
// we want do skip line between actaul first and last points, as that may result in ugly line at the edge
if (current != skip && currentPoint > 0)
renderer.line(x + (i - 1) * sampleLen, y + prevPoint * height / 6, x + i * sampleLen, y + currentPoint * height / 6);
current = prev;
currentPoint = prevPoint;
}
}
}
private static float getPoint(float sampleValue) {
return sampleValue < 1 ? sampleValue : (MathUtils.log2(sampleValue) + 1);
}
/**
* Single row for profiler list
*/
private static class ProfilerRow extends Table {
SystemProfiler profiler;
Label name, max, localMax, avg;
CheckBox draw;
float lastMax, lastLocalMax, lastAvg;
ChangeListener listener;
public ProfilerRow(Skin skin) {
this(null, skin);
}
public ProfilerRow(SystemProfiler profiler, Skin skin) {
super();
draw = new CheckBox("", skin);
name = new Label("", skin, STYLE_SMALL);
name.setEllipsis(true);
max = label("", skin, Align.right);
localMax = label("", skin, Align.right);
avg = label("", skin, Align.right);
add(draw);
add(name).expandX().fillX();
;
add(max).minWidth(MIN_LABEL_WIDTH);
add(localMax).minWidth(MIN_LABEL_WIDTH);
add(avg).minWidth(MIN_LABEL_WIDTH);
if (profiler != null) init(profiler);
}
public void init(final SystemProfiler profiler) {
this.profiler = profiler;
if (listener != null) draw.removeListener(listener);
draw.setChecked(profiler.getDrawGraph());
draw.addListener(listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
profiler.setDrawGraph(!profiler.getDrawGraph());
if (profiler.getDrawGraph()) {
setChildColor(profiler.getColor());
} else {
setChildColor(Color.LIGHT_GRAY);
}
}
});
name.setText(profiler.getName());
setChildColor(profiler.getColor());
lastMax = lastLocalMax = lastAvg = -1;
invalidateHierarchy();
layout();
}
private void setChildColor(Color color) {
name.setColor(color);
max.setColor(color);
localMax.setColor(color);
avg.setColor(color);
}
public void update() {
// we don't want to update if the change wont affect the representation
if (!MathUtils.isEqual(lastMax, profiler.getMax(), PRECISION)) {
lastMax = profiler.getMax();
max.setText(timingToString(lastMax));
}
if (!MathUtils.isEqual(lastLocalMax, profiler.getLocalMax(), PRECISION)) {
lastLocalMax = profiler.getLocalMax();
localMax.setText(timingToString(lastLocalMax));
}
if (!MathUtils.isEqual(lastAvg, profiler.getMovingAvg(), PRECISION)) {
lastAvg = profiler.getMovingAvg();
avg.setText(timingToString(lastAvg));
}
}
private String timingToString(float var) {
int decimals = (int) (var * 100) % 100;
return Integer.toString((int) (var)) + (decimals < 10 ? ".0" : ".") + Integer.toString(decimals);
}
public float getAverage() {
return profiler.getAverage();
}
public float getLocalMax() {
return profiler.getLocalMax();
}
public SystemProfiler getProfiler() {
return profiler;
}
public float getMax() {
return profiler.getMax();
}
}
/**
* Simple placeholder for actual graph
*/
private class Graph extends Table {
public Graph() {}
@Override
public float getMinWidth() {
return GRAPH_MIN_WIDTH;
}
@Override
public float getMinHeight() {
return GRAPH_MIN_HEIGHT;
}
}
/**
* Show the profiler window
*
* @param stage stage to add to
*/
public void show(Stage stage) {
stage.addActor(this);
setColor(1, 1, 1, 0);
addAction(Actions.fadeIn(FADE_TIME, Interpolation.fade));
}
/**
* Hide the window and remove from the stage
*/
public void hide() {
addAction(Actions.sequence(Actions.fadeOut(FADE_TIME, Interpolation.fade), Actions.removeActor()));
}
}

View File

@ -6,6 +6,8 @@ import com.artemis.WorldConfiguration;
import com.artemis.WorldConfigurationBuilder;
import com.artemis.managers.TagManager;
import com.artemis.utils.IntBag;
import net.mostlyoriginal.api.event.common.EventSystem;
import com.badlogic.gdx.Application;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
@ -29,6 +31,7 @@ import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import com.riiablo.Client;
import com.riiablo.Cvars;
import com.riiablo.Keys;
@ -106,6 +109,7 @@ import com.riiablo.map.Box2DPhysics;
import com.riiablo.map.Map;
import com.riiablo.map.MapManager;
import com.riiablo.map.RenderSystem;
import com.riiablo.profiler.ProfilerPlugin;
import com.riiablo.save.CharData;
import com.riiablo.screen.panel.CharacterPanel;
import com.riiablo.screen.panel.ControlPanel;
@ -123,9 +127,6 @@ import com.riiablo.screen.panel.StashPanel;
import com.riiablo.screen.panel.WaygatePanel;
import com.riiablo.widget.TextArea;
import net.mostlyoriginal.api.event.common.EventSystem;
import net.mostlyoriginal.plugin.ProfilerPlugin;
public class GameScreen extends ScreenAdapter implements GameLoadingScreen.Loadable {
private static final String TAG = "GameScreen";
private static final boolean DEBUG = true;