Further performance and memory usage improvements

- Cached custom URI fetches are about ~35% faster
- About ~40% less memory usage for cache - about 600KB on the example above
- Cached real URI's - I feel the speedup difference (more than 100x) is too dramatic to ignore
- Improve `openFileDescriptor` support to better comply with SDL's `mode` parameter
This commit is contained in:
crudelios 2026-06-02 16:33:56 +01:00
parent e0067df491
commit 0342340621
2 changed files with 163 additions and 91 deletions

View file

@ -2109,7 +2109,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
}
public static class SAFDocument {
static private final HashMap<Uri, SAFDocument> cache = new HashMap<>();
static private final HashMap<String, SAFDocument> cache = new HashMap<>();
static private boolean cacheInvalidationRequested = false;
static public void requestCacheInvalidation() {
cacheInvalidationRequested = true;
@ -2117,53 +2117,34 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
private boolean dirty;
private final String id;
private final String mimeType;
public final boolean isDirectory;
public long lastModified;
public long size;
private final Uri uri;
private final Uri tree;
private HashMap <String, SAFDocument> children;
public final Uri uri;
private ArrayList<String> children;
private SAFDocument(Cursor cursor, Uri uri) {
private SAFDocument(Cursor cursor, Uri uri, boolean uri_is_tree) {
this.id = cursor.getString(0);
this.mimeType = cursor.getString(2);
this.lastModified = cursor.getLong(3);
this.size = cursor.getLong(4);
this.isDirectory = this.mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
this.tree = uri;
this.uri = uri;
this.dirty = false;
}
private SAFDocument(Cursor cursor, SAFDocument parent) {
this.id = cursor.getString(0);
this.mimeType = cursor.getString(2);
this.lastModified = cursor.getLong(3);
this.size = cursor.getLong(4);
this.tree = parent.tree;
this.uri = DocumentsContract.buildDocumentUriUsingTree(this.tree, this.id);
this.isDirectory = this.mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
this.isDirectory = cursor.getString(2).equals(DocumentsContract.Document.MIME_TYPE_DIR);
this.uri = uri_is_tree ? DocumentsContract.buildDocumentUriUsingTree(uri, this.id) : uri;
this.dirty = false;
}
private SAFDocument(Uri tree) {
this.id = DocumentsContract.getTreeDocumentId(tree);
this.mimeType = DocumentsContract.Document.MIME_TYPE_DIR;
this.isDirectory = true;
this.tree = tree;
this.uri = DocumentsContract.buildDocumentUriUsingTree(this.tree, this.id);
this.uri = DocumentsContract.buildDocumentUriUsingTree(tree, this.id);
this.dirty = true;
}
private SAFDocument(Uri uri, Uri tree, String mimeType)
{
this.id = DocumentsContract.getDocumentId(uri);
this.mimeType = mimeType;
this.isDirectory = this.mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
this.isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
this.lastModified = System.currentTimeMillis();
this.tree = tree;
this.uri = uri;
this.uri = DocumentsContract.buildDocumentUriUsingTree(tree, this.id);
this.dirty = true;
}
@ -2180,23 +2161,43 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
private static SAFDocument getTree(Uri uri) {
Uri tree = uri.buildUpon().fragment(null).build();
if (cacheInvalidationRequested) {
cache.clear();
cacheInvalidationRequested = false;
SAFDocument document = SAFDocument.cache.get(tree.toString());
if (document == null) {
document = new SAFDocument(tree);
cache.put(tree.toString(), document);
}
SAFDocument treeInfo = cache.get(tree);
if (treeInfo == null) {
treeInfo = new SAFDocument(tree);
cache.put(tree, treeInfo);
}
return treeInfo;
return document;
}
private static SAFDocument fromUri(Uri uri) throws FileNotFoundException {
/* Tree URIs get special handling */
if (!DocumentsContract.isDocumentUri(mSingleton.getApplicationContext(), uri)) {
return new SAFDocument(uri);
String document_id;
try {
document_id = DocumentsContract.getDocumentId(uri);
} catch (IllegalArgumentException e) {
/* Tree URIs get special handling */
return SAFDocument.getTree(uri);
}
String tree_id;
try {
tree_id = DocumentsContract.getTreeDocumentId(uri);
} catch (IllegalArgumentException e) {
tree_id = null;
}
String cache_key;
if (tree_id != null) {
cache_key = uri.getAuthority() + "#" + tree_id + "/" + document_id;
} else {
cache_key = uri.getAuthority() + "#" + document_id;
}
SAFDocument document = SAFDocument.cache.get(cache_key);
if (document != null) {
return document;
}
Cursor cursor = mSingleton.getContentResolver().query(uri, SAFDocument.queryColumns,
@ -2207,20 +2208,41 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
}
cursor.moveToFirst();
SAFDocument document = new SAFDocument(cursor, uri);
document = new SAFDocument(cursor, uri, false);
cursor.close();
SAFDocument.cache.put(cache_key, document);
return document;
}
private static SAFDocument fromPath(Uri uri, String[] pathSegments) throws FileNotFoundException {
SAFDocument document = SAFDocument.getTree(uri);
SAFDocument document = SAFDocument.cache.get(uri.toString());
if (document != null) {
return document;
}
document = SAFDocument.getTree(uri);
String createdPath = document.getTree() + "#";
int prefix = createdPath.length();
for (String segment : pathSegments) {
if (segment.isEmpty()) {
continue;
}
document = document.getChildren().get(segment);
document.getChildren(createdPath.substring(prefix));
if (createdPath.endsWith("#")) {
createdPath += segment;
} else {
createdPath += "/" + segment;
}
document = SAFDocument.cache.get(createdPath);
if (document == null) {
throw new FileNotFoundException("Failed to resolve path segment \"" + segment + "\" in path \"" + String.join("/", pathSegments) + "\"");
}
@ -2234,13 +2256,20 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
}
public static SAFDocument get(Uri uri) throws FileNotFoundException {
/* Original "content://" URI, parse directly */
if (uri.getFragment() == null) {
return SAFDocument.fromUri(uri);
} else {
/* SDL "content://" URI, with "#path" at the end, parse by directory */
return SAFDocument.fromPath(uri, uri.getFragment().split("/"));
if (cacheInvalidationRequested) {
cache.clear();
cacheInvalidationRequested = false;
}
String fragment = uri.getFragment();
/* Original "content://" URI, parse directly */
if (fragment == null) {
return SAFDocument.fromUri(uri);
}
/* SDL "content://" URI, with "#path" at the end, parse by directory */
return SAFDocument.fromPath(uri, fragment.split("/"));
}
public static SAFDocument create(Uri uri) throws IllegalArgumentException, FileNotFoundException {
@ -2261,47 +2290,79 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
path.remove(path.size() - 1);
SAFDocument parent = SAFDocument.fromPath(uri, path.toArray(new String[0]));
Uri tree = parent.getTree();
Uri newFileUri = DocumentsContract.createDocument(mSingleton.getContentResolver(),
parent.uri, SAFDocument.newDocumentMimeType, newFileName);
if (newFileUri == null) {
throw new IllegalArgumentException("Unable to create a new file for writing");
}
SAFDocument newFileInfo = new SAFDocument(newFileUri, parent.tree, SAFDocument.newDocumentMimeType);
parent.children.put(newFileName, newFileInfo);
SAFDocument document = new SAFDocument(newFileUri, tree, SAFDocument.newDocumentMimeType);
parent.children.add(newFileName);
SAFDocument.cache.put(tree + "#" + uri.getFragment(), document);
return newFileInfo;
return document;
}
private HashMap <String, SAFDocument> getChildren() throws FileNotFoundException {
if (!this.isDirectory) {
private ArrayList<String> getChildren(String path) throws FileNotFoundException {
Uri tree = this.getTree();
if (!this.isDirectory || tree == null) {
throw new FileNotFoundException(this.id + " is not a directory for uri: " + this.uri);
}
if (this.children != null) {
return this.children;
}
Uri children = DocumentsContract.buildChildDocumentsUriUsingTree(this.tree, this.id);
Uri children = DocumentsContract.buildChildDocumentsUriUsingTree(tree, this.id);
Cursor cursor = mSingleton.getContentResolver().query(children, queryColumns,
null, null, null);
if (cursor == null) {
throw new FileNotFoundException("The URI " + uri + " is not a valid");
throw new FileNotFoundException("The URI " + uri + " is not valid");
}
this.children = new HashMap<>();
String cache_key_prefix;
if (path == null) {
cache_key_prefix = this.uri.getAuthority() + "#" +
DocumentsContract.getTreeDocumentId(tree) + "/";
} else {
if (path.isEmpty()) {
cache_key_prefix = tree + "#";
} else {
cache_key_prefix = tree + "#" + path + "/";
}
}
this.children = new ArrayList<>();
/* Get the directory contents */
while (cursor.moveToNext()) {
SAFDocument document = new SAFDocument(cursor, this);
this.children.put(cursor.getString(1), document);
SAFDocument document = new SAFDocument(cursor, tree, true);
String name = cursor.getString(1);
this.children.add(name);
/* Cache the result */
SAFDocument.cache.put(cache_key_prefix +
(path == null ? document.id : name), document);
}
cursor.close();
return this.children;
}
private Uri getTree() {
try {
return DocumentsContract.buildTreeDocumentUri(this.uri.getAuthority(),
DocumentsContract.getTreeDocumentId(this.uri));
} catch(Exception e) {
return null;
}
}
public void requestUpdate() {
this.dirty = true;
}
@ -2335,7 +2396,14 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
throw new IllegalStateException("SDLActivity is not initialized");
}
return SAFDocument.get(Uri.parse(uri)).getChildren().keySet().toArray(new String[0]);
if (uri.endsWith("/")) {
uri = uri.substring(0, uri.length() - 1);
} else if (uri.endsWith("%2F")) {
uri = uri.substring(0, uri.length() - 3);
}
Uri parsedUri = Uri.parse(uri);
return SAFDocument.get(parsedUri).getChildren(parsedUri.getFragment()).toArray(new String[0]);
}
/**
@ -2352,26 +2420,27 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
/**
* This method is called by SDL using JNI.
*/
public static int openFileDescriptor(String uri, String mode) throws FileNotFoundException {
public static int openFileDescriptor(String uri, String mode, boolean no_overwrite) throws FileNotFoundException, IllegalAccessException {
if (mSingleton == null) {
throw new IllegalStateException("SDLActivity is not initialized");
}
Uri contentUri = Uri.parse(uri);
if (contentUri.getFragment() != null) {
try {
SAFDocument document = SAFDocument.get(contentUri);
contentUri = document.uri;
if (mode.contains("w")) {
document.requestUpdate();
try {
SAFDocument document = SAFDocument.get(contentUri);
contentUri = document.uri;
if (mode.contains("w")) {
if (no_overwrite) {
throw new IllegalAccessException("The requested file \"" + uri + "\" already exists.");
}
} catch (FileNotFoundException e) {
if (!mode.contains("w")) {
throw e;
}
/* Maybe we want to create a new file and write to it? */
contentUri = SAFDocument.create(contentUri).uri;
document.requestUpdate();
}
} catch (FileNotFoundException e) {
if (!mode.contains("w")) {
throw e;
}
/* Maybe we want to create a new file and write to it? */
contentUri = SAFDocument.create(contentUri).uri;
}
ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(contentUri, mode);

View file

@ -696,7 +696,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl
midShouldMinimizeOnFocusLoss = (*env)->GetStaticMethodID(env, mActivityClass, "shouldMinimizeOnFocusLoss", "()Z");
midShowTextInput = (*env)->GetStaticMethodID(env, mActivityClass, "showTextInput", "(IIIII)Z");
midSupportsRelativeMouse = (*env)->GetStaticMethodID(env, mActivityClass, "supportsRelativeMouse", "()Z");
midOpenFileDescriptor = (*env)->GetStaticMethodID(env, mActivityClass, "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)I");
midOpenFileDescriptor = (*env)->GetStaticMethodID(env, mActivityClass, "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;Z)I");
midShowFileDialog = (*env)->GetStaticMethodID(env, mActivityClass, "showFileDialog", "([Ljava/lang/String;ZILjava/lang/String;I)Z");
midGetDocumentChildrenNames = (*env)->GetStaticMethodID(env, mActivityClass, "getSAFDocumentChildrenNames", "(Ljava/lang/String;)[Ljava/lang/String;");
midGetSAFDocument = (*env)->GetStaticMethodID(env, mActivityClass, "getSAFDocument", "(Ljava/lang/String;)Lorg/libsdl/app/SDLActivity$SAFDocument;");
@ -2705,7 +2705,17 @@ bool Android_JNI_EnumerateContentDirectory(const char *uri, SDL_EnumerateDirecto
return false;
}
char *dirname = GetURIWithNormalizedPath(uri, true);
char *dirname = NULL;
// If we are traversing a genuine tree URI and not an SDL generated one, the "dirname"
// parameter on the enumeration callback must provide the document URI, not the tree URI.
// Because of that, if we're using a tree URI, we need to fetch the document URI from Java.
if (!SDL_strchr(uri, '#')) {
dirname = GetDocumentURIFromTreeURI(env, uri);
} else {
dirname = GetURIWithNormalizedPath(uri, true);
}
if (!dirname) {
LocalReferenceHolder_Cleanup(&refs);
return false;
@ -2720,10 +2730,10 @@ bool Android_JNI_EnumerateContentDirectory(const char *uri, SDL_EnumerateDirecto
}
// Invoke JNI
jobjectArray jFileNamesArray = (*env)->CallStaticObjectMethod(env, mActivityClass,
midGetDocumentChildrenNames, juri);
jobjectArray jFileNamesArray = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetDocumentChildrenNames, juri);
if (Android_JNI_ExceptionOccurred(false)) {
SDL_free(dirname);
LocalReferenceHolder_Cleanup(&refs);
return false;
}
@ -2731,18 +2741,6 @@ bool Android_JNI_EnumerateContentDirectory(const char *uri, SDL_EnumerateDirecto
jsize length = (*env)->GetArrayLength(env, jFileNamesArray);
bool success = true;
// If we are traversing a genuine tree URI and not an SDL generated one, the "dirname"
// parameter on the enumeration callback must provide the document URI, not the tree URI.
// Because of that, if we're using a tree URI, we need to fetch the document URI from Java.
if (!SDL_strchr(dirname, '#')) {
SDL_free(dirname);
dirname = GetDocumentURIFromTreeURI(env, uri);
if (!dirname) {
LocalReferenceHolder_Cleanup(&refs);
return false;
}
}
for (int i = 0; i < length; ++i) {
jstring jFileName = (*env)->GetObjectArrayElement(env, jFileNamesArray, i);
@ -3637,6 +3635,7 @@ int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode)
{
// Get fopen-style modes
int moderead = 0, modewrite = 0, modeappend = 0, modeupdate = 0;
jboolean no_overwrite = JNI_FALSE;
for (const char *cmode = mode; *cmode; cmode++) {
switch (*cmode) {
@ -3652,6 +3651,9 @@ int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode)
case '+':
modeupdate = 1;
break;
case 'x':
no_overwrite = JNI_TRUE;
break;
default:
break;
}
@ -3681,13 +3683,14 @@ int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode)
JNIEnv *env = Android_JNI_GetEnv();
struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION);
if (!LocalReferenceHolder_Init(&refs, env)) {
SDL_free(normalizedUri);
LocalReferenceHolder_Cleanup(&refs);
return -1;
}
jstring jstringUri = (*env)->NewStringUTF(env, normalizedUri);
jstring jstringMode = (*env)->NewStringUTF(env, contentResolverMode);
jint fd = (*env)->CallStaticIntMethod(env, mActivityClass, midOpenFileDescriptor, jstringUri, jstringMode);
jint fd = (*env)->CallStaticIntMethod(env, mActivityClass, midOpenFileDescriptor, jstringUri, jstringMode, no_overwrite);
SDL_free(normalizedUri);