So I am developing small game in Android using JME -3.7.0-stable and was testing with memory allocation using anroid studio memory profiler. (Note I am only loading .j3o object created via sdk and blender not custom mesh or programmatically calling Mesh()) Here are my presumptions about native memory allocation by jme and need clarification if presumption are correct.
Lets say I have NPC models of type say A to E
Use Jme simpleupdate loop to create,load and destroy multiple NPC of single type - native memory increases and then remain stable. But even if I detach all children from root Node, and clear all references native memory is not deallocated but multiple instance of same NPC does not increase allocation. May be due to caching and sharing of mesh Data
Use update loop to create,load and destroy multiple NPC just like above but with differnt types i.e A,B,C… types. Each new type will demand fresh allocation demand fresh native memory, but different instances share data so no fresh native allocation.
Now comes problem in Android - when I use jmesurfaceview in an android activity. Game start nicely runs nicely allocated native memory say 300MB. Now if I close android activity via back press or activity finish() call , game is closed but native memory is not released. When activity start again native memory builds up. Java memory is fine.
Is 3rd case appropriate behaviour or is it something in my code ?
(For single activity android game this may not be a problem as on app close process closes and releases all memory).
There are issues regarding native memory deallocation specifically on JmeSurfaceView and generally on the native Android allocator. It might be a little annoying, and demanding a fix at the same time. I will try to work on them on the next release.
EDIT:
If you want to help, you can post the results of the profiler here, I will appreciate it. Thanks for reporting.
Yes that was me for another simple game. At that time I thought it was memory leak of jmesurfaceview. But this time I want to launch fps game require frequent spawning and destroying NPC so was testing memory consumption and found native memory allocation is issue. My bigger problem is memory retention when activity is closed or when navigating between activities the native memory allocated by app.start() is not released. When android app as a whole closes and android system kills process all memory will obviously be release thus problem was not serious for single activity game. And I believe most of sample test for android integration a single activity using mainactivity as jme starting point.So for fps game I am in middle of my evelopment and cannot stop work so there are two things I want to try
1)A work around - save jme app context across multiple activities so fresh memory is not allocated when activity is recreated. Is it possible ?
2) point me to documentation or something about jme buffer allocation process so that I can try to see what I can do to contribute fix issue about memory allocation. Though my JNI knowledge is zero.
EDIT : Before sharing any screen shot for later use I will rather check my own code first for issue and then post it.
The culprit may be that the original Android renderer is using a static game context; that entity might also be one of the causative agents of not invoking some memory deallocators so far…
By the way, make sure you are using AppCompatActivity to trigger the LifecycleOwner API to work, and check for the JmeSurfaceView logs.
Yes but in our previous thread discussion itself it was decided to keep set it to null on destroy. JmeAndroidSystem.setView(null) which is already done. So lack of deallocation of native buffers is different problem.
I check for heap dump. After closing game when return to main activity from gameactivity if heap dump is checked context does not seem to be released
I unfortunately cannot reproduce the issue, this is a run on the current master with KEEP_WHEN_FINISH Destruction Policy and without calling the finish():
package com.example.androidmodule;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.jme3.app.LegacyApplication;
import com.jme3.view.surfaceview.JmeSurfaceView;
import com.jme3.view.surfaceview.OnExceptionThrown;
import com.jme3.view.surfaceview.OnRendererCompleted;
import com.jme3.system.AppSettings;
import myGame.Game;
/**
* Used to create an Android Activity, the main entry for {@link android.view.Choreographer} to render Ui-Components.
*
* @author pavl_g.
*/
public class AndroidLauncher extends AppCompatActivity implements OnRendererCompleted, OnExceptionThrown {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
gl_startGame();
}
@Override
public void onPostCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
super.onPostCreate(savedInstanceState, persistentState);
}
protected void gl_startGame(){
final JmeSurfaceView gl_surfaceView = findViewById(R.id.glView);
gl_surfaceView.setDestructionPolicy(JmeSurfaceView.DestructionPolicy.DESTROY_WHEN_FINISH);
final Game game = new Game();
gl_surfaceView.setLegacyApplication(game);
//set listeners
gl_surfaceView.setOnRendererCompleted(this);
gl_surfaceView.setOnExceptionThrown(this);
gl_surfaceView.startRenderer(0);
//test android views (native android Ui -- managed by Choreographer)
final View button = findViewById(R.id.button);
//invoking the call from the android context aka from the Choreographer thread not gl thread :-))
button.setOnClickListener((view)-> Toast.makeText(getApplicationContext(), "Android View OnClick invoked", Toast.LENGTH_LONG).show());
}
@Override
public void onExceptionThrown(Throwable e) {
System.out.println(e.getMessage());
}
@Override
public void onRenderCompletion(LegacyApplication application, AppSettings appSettings) {
System.out.println("Rendering completed : " + application.getClass().getName());
}
/**
* Fired when the screen has/hasNo touch/mouse focus.
* @param hasFocus specify whether the current screen has focus or not
*/
@Override
public void onWindowFocusChanged(boolean hasFocus) {
/*get the view from the current activity*/
final View decorView = AndroidLauncher.this.getWindow().getDecorView();
/*hide navigation bar, apply fullscreen, hide status bar, immersive sticky to disable the system bars(nav & status) from showing up when user wipes the screen*/
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
public void onBackPressed() {
super.onBackPressed();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
}
}
@Anuj_Topiwala I recommend that you setup a simple testbed to better diagnose the issue…
About the static Game state, well, I don’t know what happens to it, the Android profiler doesn’t give more information, however, I see no leaks from the JmeSurfaceView component so far. This heap capture was recorded after a back press.
However, there are irrelevant memory leaks that take place on the activity components when invoking the finish() in the onBackPressed() lifecycle function:
We might attempt to fix this, despite irrelevant, but we could easily rely on the onStop lifecycle function and eliminate the need for invoking the finish manually to destroy the context…
I, however, have found other issues during testing this time, and will conclude a PR for them.
Well the leaks can be fixed by removeing static reference - JmeAndroidSystem.setView(null)
JmeSystem.setSoftTextDialogInput(null). And also remove lifecycle binding on destroy(). But major issue for me was native memory allocation build up on activity restart in multiple activity based game app.And despite removing leaks native memory is building up so it is problem with allocators. So from what I have read in forum and learnt from jme code, as android latest api requires jdk 17 - jme reflectionallocator which was used by now deprecated AndroidBufferAllocator is broken due to sun.misc.* package usage. Now its replaced by AndroidNativeBufferAllocator which has simple JNI call to native standard calloc/malloc and free methods. I suppose this(AndroidNativeBufferAllocator) allocator is what jme uses-for android, when it loads .j3o models to allocate memory for mesh,index and other buffers. If that is the case native memory is still not released. So where exactly is deallocator called by jme if model is loaded from j3o file not custom mesh and neither buffer creation via Buffutils ? I would too check log of destroyDirectBuffer in AndroidNativeBufferAllocator
When allocating a native buffer via the machine code. The automata flow should be to create a strong reference by the native code (i.e. memory buffer pointer) which is then handled to the JVM as a weak reference (i.e., auto-reclaimable when their memory is cleaned) which is then used to create a PhantomReference object that keeps track of the memory address of the object. When the program flow enters the postmortem stage, the GC is signaled to destroy the native objects by using their phantom references through polling over a reference queue.
There is a work to fix the native issues, but has not been ported yet; because we haven’t proof-tested it yet on Android platforms. Jme-alloc is a dynamic memory allocator API for Java applications, and it has phantom bindings to the GC auto deallocates the native memory via a polling algorithm once no strong references to that native memory are actively present in your code…
Check it out:
If you want to help to deliver this to Android, and eventually fix the native memory issues, let’s leave things apart, and not mix issues together and create another thread to discuss a plan for it… mainly testing and techdemos…
For this thread, let’s deliver a maintenance PR for JmeSurfaceView only.
EDIT:
I acknowledge credits on GitHub for people who help deliver these projects either via PR or a tech demo or a discussion on online forums.
When examining the causative references that lead to these leaks, I have found that the context of the JmeSurfaceView that is passed to the GlSurfaceView as a result of the Android OGLESContext, a concrete implementation of JmeContext, is not being destructed, and hence it leads to bound references to the activity that disables the destruction of the activity’s references; hence a memory leak.
Further investigations and testing have revealed that the way JmeSurfaceView dispatches the SystemListener#destroy() is erroneous, that is it was being dispatched with the Activity#onDestroy() which is as the Android JavaDoc says: it’s a Killable function, mandating another implementation for Java Heap destruction; as memory reclamation shoud never depend on unstable conditions.
You can find more about the solution here:
I will do a maintenance PR soon, and add some optimizations on the JmeSurfaceView#destroy().
EDIT: @Anuj_Topiwala The Activity#onDestroy() is a killable function, that is its resources’ controls are shared with the Android System, and dispatch and interrupt conditions are applied as per the current system behavior of memory and CPU usages; that is why on some devices you get a leak while others not!
EDIT2:
It appears that this is not completed until the context reference to the GlSurfaceView object is released from the AndroidInputHandler on the OGLESContext:
/**
* De-initialize in the OpenGL thread.
*/
protected void deinitInThread() {
if (renderable.get()) {
created.set(false);
if (renderer != null) {
renderer.cleanup();
}
listener.destroy();
androidInput.setView(null); // fixes the issue!
listener = null;
renderer = null;
timer = null;
androidInput = null;
// do android specific cleaning here
logger.fine("Display destroyed.");
renderable.set(false);
}
}
I have removed most of static references already. Well I have copied your custom view code into my own local custom view with minor changes like converting to kotlin, removing handler and using kotlin coroutinescopes, removing references in destroy() of jmesurfaceview and in most cases it does not show any leaks. But as you said these leaks are irrelevant. My major problem was #3 point in the original post about native memory leak on activity restart. For now I am manually calling
if(node is Geometry){
node.mesh.buffers.forEach { buffer->
if(buffer.value.data.isDirect){
//Log.d("BufferDeallocation","Before destruction of buffers is being done Capacity: ${buffer.value.data.capacity()}")
BufferUtils.destroyDirectBuffer(buffer.value.data)
}
}
}
to remove the native memory while quitting game aka activity finish. Though later on I would rather try releasing native memory on game level change but currently its giving null pointer error on native side, probably guessing that jme caches are not cleared and I dont want to clear all caches. Of course an easy alternative would be to use Primitive allocator with allocateDirect that will let GC collect at its own convenience but nevertheless I will try and look at how remove native memory manually.
Regarding your auto deallocating project and phantom reference,reference queues based solution, I would have to read and gather primitive knowledge before I dabble in the stuff.
To make this effective, there are other changes on the OGLESContext.
It’s another issue, which means another work. I will see what I can do to deliver Jme-alloc to the Android Platform. But, notice that these issues are also on LWJGL-2, so testing is multidisciplinary here.
Notice another point:
Using multiple activities; each has its own renderer is an ineffective game development pattern, you should probably use game states through a finite-automata pattern or Entity-component-systems… This pattern of multiple game contexts is also ineffective, and in fact very bad, on desktop systems; as time for destruction is needed between the start and the end of successive contexts, and that’s where the memory culprit can make your application sluggish especially on low-memory devices.
I am using one activity purely for introductory purpose,stores management and stuff while jme context (if thats what you meant by game context) is created only on play button of gameactivity. So essentially context is not shared across activity but survives lifetime of activity.
I agree storing game states are effective for faster loading but then need to be very careful about not having memory leaks or keeping old references, say my cache still refering to old native buff address on restart and app may crash. But I will see what changes I can incorporate in my game. Nevertheless its good learning.
I mean you should have a single activity, and do other stuff internally on game states, so you won’t need to destroy and reallocate application memory. Only the graphics and audio buffers are the ones to be destructed and reallocated if necessary. You can handle the introductory scenes through Android Views; JmeSurfaceView has a utility for this. Or else better to be, a game state.
Creating an activity just to destroy it less than 1 second later is meaningless and will waste a lot of memory. An activity should be a long-living object as far as the lifetime of the Android application.
For native memory leaks, you can give Jme-alloc a try, and report the results, here is a test to get you started (it doesn’t need to be integrated into the Engine, you just need to configure the allocator type before the application starts, and check it against system overwrite operations):
Credits go to @Ali_RS; he has built this example a long while ago and took a lot of time testing the API.
I have found some time to formulate an article on this issue, despite being an easy-to-fix issue. However, discovering the issue and investigating it together with understanding the Android profiler output and correlating it with the actual code is the real culprit. @Anuj_Topiwala Thanks for reporting the issue, you are doing a great work to the community.