From aea5e4af31c7863be2c65a89c9ee3ea88269df01 Mon Sep 17 00:00:00 2001 From: pspeed42 Date: Fri, 12 Sep 2014 02:09:36 -0400 Subject: [PATCH] Added a basic AppProfiler implementation and corresponding app state. If the app state is attached then it's 'enabled' state can be toggled with the F6 key. It displays a continnuously updating bar graph of the application wide frame timings for 'update' and 'render'... where 'update' is all of the parts that aren't 'rendering', ie: running enqueued tasks, updating states, updating controls, etc.. --- .../main/java/com/jme3/app/BasicProfiler.java | 197 ++++++++++++++ .../java/com/jme3/app/BasicProfilerState.java | 240 ++++++++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 jme3-core/src/main/java/com/jme3/app/BasicProfiler.java create mode 100644 jme3-core/src/main/java/com/jme3/app/BasicProfilerState.java diff --git a/jme3-core/src/main/java/com/jme3/app/BasicProfiler.java b/jme3-core/src/main/java/com/jme3/app/BasicProfiler.java new file mode 100644 index 000000000..e36356040 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/app/BasicProfiler.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2014 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.app; + +import com.jme3.profile.AppProfiler; +import com.jme3.profile.AppStep; +import com.jme3.profile.VpStep; +import com.jme3.renderer.ViewPort; +import com.jme3.renderer.queue.RenderQueue.Bucket; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.util.BufferUtils; +import java.nio.FloatBuffer; + + +/** + * An AppProfiler implementation that collects two + * per-frame application-wide timings for update versus + * render and uses it to create a bar chart style Mesh. + * The number of frames displayed and the update interval + * can be specified. The chart Mesh is in 'milliseconds' + * and can be scaled up or down as required. + * + *

Each column of the chart represents a single frames + * timing. Yellow represents the time it takes to + * perform all non-rendering activities (running enqueued + * tasks, stateManager.update, control.update(), etc) while + * the cyan portion represents the rendering time.

+ * + *

When the end of the chart is reached, the current + * frame cycles back around to the beginning.

+ * + * @author Paul Speed + */ +public class BasicProfiler implements AppProfiler { + + private int size; + private int frameIndex = 0; + private long[] frames; + private long startTime; + private long renderTime; + private long previousFrame; + private long updateInterval = 1000000L; // once a millisecond + private long lastUpdate = 0; + + private Mesh mesh; + + public BasicProfiler() { + this(1280); + } + + public BasicProfiler( int size ) { + setFrameCount(size); + } + + /** + * Sets the number of frames to display and track. By default + * this is 1280. + */ + public final void setFrameCount( int size ) { + if( this.size == size ) { + return; + } + + this.size = size; + this.frames = new long[size*2]; + + createMesh(); + + if( frameIndex >= size ) { + frameIndex = 0; + } + } + + public int getFrameCount() { + return size; + } + + /** + * Sets the number of nanoseconds to wait before updating the + * mesh. By default this is once a millisecond, ie: 1000000 nanoseconds. + */ + public void setUpdateInterval( long nanos ) { + this.updateInterval = nanos; + } + + public long getUpdateInterval() { + return updateInterval; + } + + /** + * Returns the mesh that contains the bar chart of tracked frame + * timings. + */ + public Mesh getMesh() { + return mesh; + } + + protected final void createMesh() { + if( mesh == null ) { + mesh = new Mesh(); + mesh.setMode(Mesh.Mode.Lines); + } + + mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(size * 4 * 3)); + + FloatBuffer cb = BufferUtils.createFloatBuffer(size * 4 * 4); + for( int i = 0; i < size; i++ ) { + // For each index we add 4 colors, one for each line + // endpoint for two layers. + cb.put(0.5f).put(0.5f).put(0).put(1); + cb.put(1).put(1).put(0).put(1); + cb.put(0).put(0.5f).put(0.5f).put(1); + cb.put(0).put(1).put(1).put(1); + } + mesh.setBuffer(Type.Color, 4, cb); + } + + protected void updateMesh() { + FloatBuffer pb = (FloatBuffer)mesh.getBuffer(Type.Position).getData(); + pb.rewind(); + float scale = 1 / 1000000f; // scaled to ms as pixels + for( int i = 0; i < size; i++ ) { + float t1 = frames[i * 2] * scale; + float t2 = frames[i * 2 + 1] * scale; + + pb.put(i).put(0).put(0); + pb.put(i).put(t1).put(0); + pb.put(i).put(t1).put(0); + pb.put(i).put(t2).put(0); + } + mesh.setBuffer(Type.Position, 3, pb); + } + + @Override + public void appStep( AppStep step ) { + + switch(step) { + case BeginFrame: + startTime = System.nanoTime(); + break; + case RenderFrame: + renderTime = System.nanoTime(); + frames[frameIndex * 2] = renderTime - startTime; + break; + case EndFrame: + long time = System.nanoTime(); + frames[frameIndex * 2 + 1] = time - renderTime; + previousFrame = startTime; + frameIndex++; + if( frameIndex >= size ) { + frameIndex = 0; + } + if( startTime - lastUpdate > updateInterval ) { + updateMesh(); + lastUpdate = startTime; + } + break; + } + } + + @Override + public void vpStep( VpStep step, ViewPort vp, Bucket bucket ) { + } +} + + diff --git a/jme3-core/src/main/java/com/jme3/app/BasicProfilerState.java b/jme3-core/src/main/java/com/jme3/app/BasicProfilerState.java new file mode 100644 index 000000000..bfabbee9b --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/app/BasicProfilerState.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2014 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.app; + +import com.jme3.app.state.BaseAppState; +import com.jme3.input.InputManager; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.material.Material; +import com.jme3.material.RenderState.BlendMode; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.VertexBuffer.Type; + + +/** + * Provides a basic profiling visualization that shows + * per-frame application-wide timings for update and + * rendering. + * + * @author Paul Speed + */ +public class BasicProfilerState extends BaseAppState { + + public static final String INPUT_MAPPING_PROFILER_TOGGLE = "BasicProfilerState_Toggle"; + + private BasicProfiler profiler; + private Geometry graph; + private Geometry background; + private float scale = 2; + + private ProfilerKeyListener keyListener = new ProfilerKeyListener(); + + public BasicProfilerState() { + this(false); + } + + public BasicProfilerState( boolean enabled ) { + setEnabled(enabled); + this.profiler = new BasicProfiler(); + } + + public void toggleProfiler() { + setEnabled(!isEnabled()); + } + + public BasicProfiler getProfiler() { + return profiler; + } + + /** + * Sets the vertical scale of the visualization where + * each unit is a millisecond. Defaults to 2, ie: a + * single millisecond stretches two pixels high. + */ + public void setGraphScale( float scale ) { + if( this.scale == scale ) { + return; + } + this.scale = scale; + if( graph != null ) { + graph.setLocalScale(1, scale, 1); + } + } + + public float getGraphScale() { + return scale; + } + + /** + * Sets the number frames displayed and tracked. + */ + public void setFrameCount( int count ) { + if( profiler.getFrameCount() == count ) { + return; + } + profiler.setFrameCount(count); + refreshBackground(); + } + + public int getFrameCount() { + return profiler.getFrameCount(); + } + + protected void refreshBackground() { + Mesh mesh = background.getMesh(); + + int size = profiler.getFrameCount(); + float frameTime = 1000f / 60; + mesh.setBuffer(Type.Position, 3, new float[] { + + // first quad + 0, 0, 0, + size, 0, 0, + size, frameTime, 0, + 0, frameTime, 0, + + // second quad + 0, frameTime, 0, + size, frameTime, 0, + size, frameTime * 2, 0, + 0, frameTime * 2, 0, + + // A lower dark border just to frame the + // 'update' stats against bright backgrounds + 0, -2, 0, + size, -2, 0, + size, 0, 0, + 0, 0, 0 + }); + + mesh.setBuffer(Type.Color, 4, new float[] { + // first quad, within normal frame limits + 0, 1, 0, 0.25f, + 0, 1, 0, 0.25f, + 0, 0.25f, 0, 0.25f, + 0, 0.25f, 0, 0.25f, + + // Second quad, dropped frames + 0.25f, 0, 0, 0.25f, + 0.25f, 0, 0, 0.25f, + 1, 0, 0, 0.25f, + 1, 0, 0, 0.25f, + + 0, 0, 0, 0.5f, + 0, 0, 0, 0.5f, + 0, 0, 0, 0.5f, + 0, 0, 0, 0.5f + }); + + mesh.setBuffer(Type.Index, 3, new short[] { + 0, 1, 2, + 0, 2, 3, + 4, 5, 6, + 4, 6, 7, + 8, 9, 10, + 8, 10, 11 + }); + } + + @Override + protected void initialize( Application app ) { + + graph = new Geometry("profiler", profiler.getMesh()); + + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setBoolean("VertexColor", true); + graph.setMaterial(mat); + graph.setLocalTranslation(0, 300, 0); + graph.setLocalScale(1, scale, 1); + + Mesh mesh = new Mesh(); + background = new Geometry("profiler.background", mesh); + mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setBoolean("VertexColor", true); + mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); + background.setMaterial(mat); + background.setLocalTranslation(0, 300, -1); + background.setLocalScale(1, scale, 1); + + refreshBackground(); + + InputManager inputManager = app.getInputManager(); + if( inputManager != null ) { + inputManager.addMapping(INPUT_MAPPING_PROFILER_TOGGLE, new KeyTrigger(KeyInput.KEY_F6)); + inputManager.addListener(keyListener, INPUT_MAPPING_PROFILER_TOGGLE); + } + } + + @Override + protected void cleanup( Application app ) { + InputManager inputManager = app.getInputManager(); + if( inputManager.hasMapping(INPUT_MAPPING_PROFILER_TOGGLE) ) { + inputManager.deleteMapping(INPUT_MAPPING_PROFILER_TOGGLE); + } + inputManager.removeListener(keyListener); + } + + @Override + protected void enable() { + + // Set the number of visible frames to the current width of the screen + setFrameCount(getApplication().getCamera().getWidth()); + + getApplication().setAppProfiler(profiler); + Node gui = ((SimpleApplication)getApplication()).getGuiNode(); + gui.attachChild(graph); + gui.attachChild(background); + } + + @Override + protected void disable() { + getApplication().setAppProfiler(null); + graph.removeFromParent(); + background.removeFromParent(); + } + + private class ProfilerKeyListener implements ActionListener { + + public void onAction(String name, boolean value, float tpf) { + if (!value) { + return; + } + toggleProfiler(); + } + } +} +