package net.mostlyoriginal.plugin.profiler; 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.*; 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; import java.util.Comparator; /** * 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; 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())); } }