* Fix many memory leaks (mimetype check, performance panel, render notification)

* Try to make libGDX's Pools thread safe
This commit is contained in:
fgnm
2024-04-30 17:14:47 +02:00
parent 07bc5256dc
commit ad9b06d036
16 changed files with 124 additions and 181 deletions
Binary file not shown.
@@ -9,9 +9,6 @@ import com.kotcrab.vis.ui.widget.VisLabel;
import com.kotcrab.vis.ui.widget.VisTable;
import games.rednblack.h2d.common.UIDraggablePanel;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
public class PerformancePanel extends UIDraggablePanel {
private final VisTable mainTable;
@@ -99,9 +96,9 @@ public class PerformancePanel extends UIDraggablePanel {
entitiesCount.setText(entitySubscription.getEntities().size());
fpsLbl.setText(Gdx.graphics.getFramesPerSecond());
MemoryUsage memoryUsage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
long usedMemory = memoryUsage.getUsed() / (1024 * 1024);
long allocatedMemory = memoryUsage.getCommitted() / (1024 * 1024);
long used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
long usedMemory = used / (1024 * 1024);
long allocatedMemory = Runtime.getRuntime().totalMemory() / (1024 * 1024);
memoryLabel.getText().clear();
memoryLabel.getText().append(usedMemory);
memoryLabel.getText().append(" of ");
@@ -1,144 +0,0 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.badlogic.gdx.utils;
/** A pool of objects that can be reused to avoid allocation.
* @see Pools
* @author Nathan Sweet */
abstract public class Pool<T> {
/** The maximum number of objects that will be pooled. */
public final int max;
/** The highest number of free objects. Can be reset any time. */
public int peak;
private final Array<T> freeObjects;
/** Creates a pool with an initial capacity of 16 and no maximum. */
public Pool () {
this(16, Integer.MAX_VALUE);
}
/** Creates a pool with the specified initial capacity and no maximum. */
public Pool (int initialCapacity) {
this(initialCapacity, Integer.MAX_VALUE);
}
/** @param initialCapacity The initial size of the array supporting the pool. No objects are created/pre-allocated. Use
* {@link #fill(int)} after instantiation if needed.
* @param max The maximum number of free objects to store in this pool. */
public Pool (int initialCapacity, int max) {
freeObjects = new Array(false, initialCapacity);
this.max = max;
}
abstract protected T newObject ();
/** Returns an object from this pool. The object may be new (from {@link #newObject()}) or reused (previously
* {@link #free(Object) freed}). */
public T obtain () {
synchronized (freeObjects) {
return freeObjects.size == 0 ? newObject() : freeObjects.pop();
}
}
/** Puts the specified object in the pool, making it eligible to be returned by {@link #obtain()}. If the pool already contains
* {@link #max} free objects, the specified object is {@link #discard(Object) discarded}, it is not reset and not added to the
* pool.
* <p>
* The pool does not check if an object is already freed, so the same object must not be freed multiple times. */
public void free (T object) {
synchronized (freeObjects) {
if (object == null) throw new IllegalArgumentException("object cannot be null.");
if (freeObjects.size < max) {
freeObjects.add(object);
peak = Math.max(peak, freeObjects.size);
reset(object);
} else
discard(object);
}
}
/** Adds the specified number of new free objects to the pool. Usually called early on as a pre-allocation mechanism but can be
* used at any time.
*
* @param size the number of objects to be added */
public void fill (int size) {
synchronized (freeObjects) {
for (int i = 0; i < size; i++)
if (freeObjects.size < max) freeObjects.add(newObject());
peak = Math.max(peak, freeObjects.size);
}
}
/** Called when an object is freed to clear the state of the object for possible later reuse. The default implementation calls
* {@link Poolable#reset()} if the object is {@link Poolable}. */
protected void reset (T object) {
if (object instanceof Poolable) ((Poolable)object).reset();
}
/** Called when an object is discarded. This is the case when an object is freed, but the maximum capacity of the pool is
* reached, and when the pool is {@link #clear() cleared} */
protected void discard (T object) {
reset(object);
}
/** Puts the specified objects in the pool. Null objects within the array are silently ignored.
* <p>
* The pool does not check if an object is already freed, so the same object must not be freed multiple times.
* @see #free(Object) */
public void freeAll (Array<T> objects) {
synchronized (freeObjects) {
if (objects == null) throw new IllegalArgumentException("objects cannot be null.");
Array<T> freeObjects = this.freeObjects;
int max = this.max;
for (int i = 0, n = objects.size; i < n; i++) {
T object = objects.get(i);
if (object == null) continue;
if (freeObjects.size < max) {
freeObjects.add(object);
reset(object);
} else {
discard(object);
}
}
peak = Math.max(peak, freeObjects.size);
}
}
/** Removes and discards all free objects from this pool. */
public void clear () {
synchronized (freeObjects) {
Array<T> freeObjects = this.freeObjects;
for (int i = 0, n = freeObjects.size; i < n; i++)
discard(freeObjects.get(i));
freeObjects.clear();
}
}
/** The number of objects available to be obtained. */
public int getFree () {
synchronized (freeObjects) {
return freeObjects.size;
}
}
/** Objects implementing this interface will have {@link #reset()} called when passed to {@link Pool#free(Object)}. */
static public interface Poolable {
/** Resets the object for reuse. Object references should be nulled and fields may be set to default values. */
public void reset ();
}
}
@@ -0,0 +1,74 @@
package com.badlogic.gdx.utils;
public class Pools {
static private final ThreadLocal<ObjectMap<Class, Pool>> typePools = new ThreadLocal<ObjectMap<Class, Pool>>() {
protected ObjectMap<Class, Pool> initialValue () {
return new ObjectMap<Class, Pool>();
};
};
/** Returns a new or existing pool for the specified type, stored in a Class to {@link Pool} map. Note the max size is ignored
* if this is not the first time this pool has been requested. */
static public <T> Pool<T> get (Class<T> type, int max) {
Pool pool = typePools.get().get(type);
if (pool == null) {
pool = new ReflectionPool(type, 4, max);
typePools.get().put(type, pool);
}
return pool;
}
/** Returns a new or existing pool for the specified type, stored in a Class to {@link Pool} map. The max size of the pool used
* is 100. */
static public <T> Pool<T> get (Class<T> type) {
return get(type, 100);
}
/** Sets an existing pool for the specified type, stored in a Class to {@link Pool} map. */
static public <T> void set (Class<T> type, Pool<T> pool) {
typePools.get().put(type, pool);
}
/** Obtains an object from the {@link #get(Class) pool}. */
static public <T> T obtain (Class<T> type) {
return get(type).obtain();
}
/** Frees an object from the {@link #get(Class) pool}. */
static public void free (Object object) {
if (object == null) throw new IllegalArgumentException("object cannot be null.");
Pool pool = typePools.get().get(object.getClass());
if (pool == null) return; // Ignore freeing an object that was never retained.
pool.free(object);
}
/** Frees the specified objects from the {@link #get(Class) pool}. Null objects within the array are silently ignored. Objects
* don't need to be from the same pool. */
static public void freeAll (Array objects) {
freeAll(objects, false);
}
/** Frees the specified objects from the {@link #get(Class) pool}. Null objects within the array are silently ignored.
* @param samePool If true, objects don't need to be from the same pool but the pool must be looked up for each object. */
static public void freeAll (Array objects, boolean samePool) {
if (objects == null) throw new IllegalArgumentException("objects cannot be null.");
Pool pool = null;
for (int i = 0, n = objects.size; i < n; i++) {
Object object = objects.get(i);
if (object == null) continue;
if (pool == null) {
pool = typePools.get().get(object.getClass());
if (pool == null) continue; // Ignore freeing an object that was never retained.
}
pool.free(object);
if (!samePool) pool = null;
}
}
public static String name() {
return "ThreadSafe Pools";
}
private Pools () {
}
}
@@ -91,9 +91,11 @@ public class HyperLap2D implements IProxy, ApplicationListener, Lwjgl3WindowList
facade.sendNotification(MsgAPI.RESUME);
}
float[] delta = new float[1];
@Override
public void render() {
facade.sendNotification(MsgAPI.RENDER, Math.min(Gdx.graphics.getDeltaTime(), 0.1f));
delta[0] = Math.min(Gdx.graphics.getDeltaTime(), 0.1f);
facade.sendNotification(MsgAPI.RENDER, delta);
}
@Override
@@ -2,6 +2,7 @@ package games.rednblack.editor.controller;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.utils.Pools;
import games.rednblack.editor.utils.AppConfig;
import games.rednblack.h2d.common.HyperLog;
import games.rednblack.puremvc.commands.SimpleCommand;
@@ -21,5 +22,7 @@ public class BootstrapInfoCommand extends SimpleCommand {
HyperLog.info("Shaders version " + gl20.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION));
HyperLog.info("JVM Version: " + System.getProperty("java.version") + " (" + System.getProperty("java.vendor") + ")");
HyperLog.info(Pools.name());
}
}
@@ -91,18 +91,18 @@ public class ResourceManager extends Proxy implements IResourceRetriever {
packer.setTransparentColor(Color.WHITE);
packer.getTransparentColor().a = 0;
FreeTypeFontGenerator dejaVuSansGenerator = new FreeTypeFontGenerator(Gdx.files.internal("freetypefonts/DejaVuSans.ttf")) {
FreeTypeFontGenerator dejaVuSansGenerator = new FreeTypeFontGenerator(Gdx.files.internal("freetypefonts/DejaVuSans.ttf")) /*{
@Override
protected BitmapFont newBitmapFont(BitmapFont.BitmapFontData data, Array<TextureRegion> pageRegions, boolean integer) {
return new ThreadSafeBitmapFont(data, pageRegions, integer);
}
};
FreeTypeFontGenerator monoGenerator = new FreeTypeFontGenerator(Gdx.files.internal("freetypefonts/FiraCode-Regular.ttf")){
}*/;
FreeTypeFontGenerator monoGenerator = new FreeTypeFontGenerator(Gdx.files.internal("freetypefonts/FiraCode-Regular.ttf"))/*{
@Override
protected BitmapFont newBitmapFont(BitmapFont.BitmapFontData data, Array<TextureRegion> pageRegions, boolean integer) {
return new ThreadSafeBitmapFont(data, pageRegions, integer);
}
};
}*/;
FreeTypeFontGenerator.FreeTypeFontParameter parameter = new FreeTypeFontGenerator.FreeTypeFontParameter();
parameter.characters += "⌘⇧⌥\u25CF\u2022";
@@ -3,6 +3,7 @@ package games.rednblack.editor.utils;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IntArray;
import com.kotcrab.vis.ui.widget.file.FileTypeFilter;
import org.apache.commons.io.FileUtils;
@@ -10,7 +11,6 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class AssetsUtils {
@@ -64,38 +64,45 @@ public class AssetsUtils {
return fileTypeFilter;
}
public static boolean isAnimationSequence(String[] names) {
if (names.length < 2) return false;
int[] sequenceArray = new int[names.length];
for (int i = 0; i < names.length; i++) {
String name = names[i];
public static IntArray sequenceArray = new IntArray();
public static boolean isAnimationSequence(Array<String> files) {
if (files.size < 2) return false;
sequenceArray.clear();
sequenceArray.ensureCapacity(files.size);
sequenceArray.size = files.size;
for (int i = 0; i < files.size; i++) {
String name = files.get(i);
// try to remove extension if any
if (name.indexOf(".") > 0) name = name.substring(0, name.indexOf("."));
try {
int intValue = Integer.parseInt(name.replaceAll("(.+)_", ""));
sequenceArray[i] = intValue;
sequenceArray.insert(i, intValue);
} catch (Exception e) {
sequenceArray[i] = -10;
sequenceArray.insert(i, -1);
}
}
Arrays.sort(sequenceArray);
if (sequenceArray[0] == 0 && sequenceArray[sequenceArray.length - 1] == sequenceArray.length - 1) {
sequenceArray.sort();
if (sequenceArray.get(0) == 0 && sequenceArray.get(sequenceArray.size - 1) == sequenceArray.size - 1) {
return true;
}
return sequenceArray[0] == 1 && sequenceArray[sequenceArray.length - 1] == sequenceArray.length;
return sequenceArray.get(0) == 1 && sequenceArray.get(sequenceArray.size - 1) == sequenceArray.size;
}
private final static Array<String> tmpNames = new Array<>();
public static boolean isAtlasAnimationSequence(Array<TextureAtlas.TextureAtlasData.Region> regions) {
if (regions.size < 2) return false;
tmpNames.clear();
//Check old .atlas format
String[] regionNames = new String[regions.size];
for (int i = 0; i < regions.size; i++) {
regionNames[i] = regions.get(i).name;
tmpNames.add(regions.get(i).name);
}
if (isAnimationSequence(regionNames))
if (isAnimationSequence(tmpNames))
return true;
//New .atlas format
@@ -60,13 +60,15 @@ public abstract class Asset implements IAsset {
// save before importing
SceneVO vo = Sandbox.getInstance().sceneVoFromItems();
projectManager.saveCurrentProject(vo);
if (!skipRepack) //Skip saving if internal resource
projectManager.saveCurrentProject(vo);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> importAsset(files, progressHandler, skipRepack));
executor.execute(() -> {
progressHandler.progressChanged(100);
projectManager.saveCurrentProject();
if (!skipRepack) //Skip saving if internal resource
projectManager.saveCurrentProject();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
@@ -134,6 +134,7 @@ public class HyperLap2DLibraryAsset extends Asset {
ResolutionManager resolutionManager = Facade.getInstance().retrieveProxy(ResolutionManager.NAME);
resolutionManager.rePackProjectImagesForAllResolutionsSync();
projectManager.saveCurrentProject();
progressHandler.progressChanged(100);
@@ -26,11 +26,13 @@ import java.util.function.Consumer;
public class ImageAsset extends Asset {
private final Array<String> names = new Array<>();
@Override
public int matchType(Array<FileHandle> files) {
String[] names = new String[files.size];
names.clear();
for (int i = 0; i < files.size; i++) {
names[i] = files.get(i).nameWithoutExtension();
names.add(files.get(i).nameWithoutExtension());
}
return AssetsUtils.isAnimationSequence(names) ? AssetsUtils.TYPE_UNKNOWN : super.matchType(files);
@@ -15,11 +15,13 @@ import java.util.Objects;
public class SpriteAnimationSequenceAsset extends SpriteAnimationAtlasAsset {
private final Array<String> names = new Array<>();
@Override
public int matchType(Array<FileHandle> files) {
String[] names = new String[files.size];
names.clear();
for (int i = 0; i < files.size; i++) {
names[i] = files.get(i).nameWithoutExtension();
names.add(files.get(i).nameWithoutExtension());
}
return AssetsUtils.isAnimationSequence(names) ? getType() : AssetsUtils.TYPE_UNKNOWN;
}
@@ -80,7 +80,8 @@ public class HyperLap2DScreenMediator extends Mediator<HyperLap2DScreen> {
viewComponent.resume();
break;
case MsgAPI.RENDER:
viewComponent.render(notification.getBody());
float[] delta = notification.getBody();
viewComponent.render(delta[0]);
break;
case MsgAPI.RESIZE:
int[] data = notification.getBody();
@@ -1,7 +1,6 @@
package games.rednblack.editor.view.ui.followers;
import com.artemis.ComponentMapper;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.OrthographicCamera;
@@ -1,12 +1,9 @@
package games.rednblack.editor.view.ui.widget.actors.polygon;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.math.Intersector;
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.Touchable;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Pool;
import space.earlygrey.shapedrawer.ShapeDrawer;