Skip to content

OutOfMemory issue in SimpleCache.loadDirectory for download usecase #3002

@tarunraghavpw227

Description

@tarunraghavpw227

Version

Media3 main branch

More version details

No response

Devices that reproduce the issue

Samsung running Android 13 (RAM free: 870.45 MB Disk free: 12.37 GB)
Oppo running Android 14 (RAM free: 1.96 GB Disk free: 13.85 GB)

Devices that do not reproduce the issue

No response

Reproducible in the demo app?

Not tested

Reproduction steps

Not able to reproduce as this is reported via crashlytics.

Expected result

Should not get OOM

Actual result

Getting OOM exception while initialising simplecache for download manager

Media

Issue

Need help to fix this or any suggestion which can help here to fix SimpleCache outofmemory issue.

I also tried the LeastRecentlyUsedCacheEvictor but by that way offline videos were not getting played when maxBytes gets exceeded.

I am not able to reproduce this issue in my device, got this issue from crashlytics.

Attaching the stack trace for the crash below:

Fatal Exception: java.lang.OutOfMemoryError: Failed to allocate a 120 byte allocation with 1468512 free bytes and 1434KB until OOM, target footprint 536870912, growth limit 536870912; giving up on allocation because <1% of heap free after GC.
 at java.io.UnixFileSystem.resolve(UnixFileSystem.java:146)
 at java.io.File.<init>(File.java:273)
 at java.io.File.listFiles(File.java:1235)
 at com.google.android.exoplayer2.upstream.cache.SimpleCache.loadDirectory(SimpleCache.java:633)
 at com.google.android.exoplayer2.upstream.cache.SimpleCache.initialize(SimpleCache.java:586)
 at com.google.android.exoplayer2.upstream.cache.SimpleCache.access$000(SimpleCache.java:49)
 at com.google.android.exoplayer2.upstream.cache.SimpleCache$1.run(SimpleCache.java:268)

Attaching the code for creating simple cache object:

val cookieManager = CookieManager()
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER)
CookieHandler.setDefault(cookieManager)
val standaloneDatabaseProvider = StandaloneDatabaseProvider(context)
val downloadContentDirectory = File(context.filesDir.absolutePath + "/" + APP_NAME + "/Videos")
val simpleCache = SimpleCache(
    downloadContentDirectory, NoOpCacheEvictor(), standaloneDatabaseProvider
)
val upstreamFactory =
    DefaultDataSource.Factory(context, dataSourceFactory)
cacheDataStoreFactory = CacheDataSource.Factory()
    .setCache(simpleCache)
    .setUpstreamDataSourceFactory(upstreamFactory)
    .setCacheWriteDataSinkFactory(null)
    .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
downloadManager = DownloadManager(
    context,
    standaloneDatabaseProvider,
    simpleCache,
    ResolvingDataSource.Factory(dataSourceFactory, resolver),
    Executors.newFixedThreadPool(12) 
)

I also added non-fatal logging on our side to understand the scale at which users are hitting this crash.
From production logs:
Users who crash have ~150 downloaded videos on average
Average video size ~180 MB
Total cached data is typically 25–30 GB

While investigating further, I looked into SimpleCache initialization and noticed a potential performance/memory concern in the recursive directory scan that eagerly loads all cache spans into memory:

private void loadDirectory(
        File directory,
        boolean isRoot,
        @Nullable File[] files,
        @Nullable Map<String, CacheFileMetadata> fileMetadata
) {
    if (files == null || files.length == 0) {
        if (!isRoot) {
            directory.delete();
        }
        return;
    }

    for (File file : files) {
        String fileName = file.getName();

        if (isRoot && fileName.indexOf('.') == -1) {
            loadDirectory(
                    file,
                    /* isRoot= */ false,
                    file.listFiles(),
                    fileMetadata
            );
            continue;
        }

        if (isRoot
                && (CachedContentIndex.isIndexFile(fileName)
                || fileName.endsWith(UID_FILE_SUFFIX))) {
            continue;
        }

        long length = C.LENGTH_UNSET;
        long lastTouchTimestamp = C.TIME_UNSET;

        @Nullable CacheFileMetadata metadata =
                fileMetadata != null ? fileMetadata.remove(fileName) : null;

        if (metadata != null) {
            length = metadata.length;
            lastTouchTimestamp = metadata.lastTouchTimestamp;
        }

        @Nullable SimpleCacheSpan span =
                SimpleCacheSpan.createCacheEntry(
                        file,
                        length,
                        lastTouchTimestamp,
                        contentIndex
                );

        if (span != null) {
            addSpan(span);
        } else {
            file.delete();
        }
    }
}

From what I understand, this:

Recursively scans the entire cache directory at startup
Creates all SimpleCacheSpans eagerly
Populates contentIndex fully before playback
With large offline libraries, this seems like it could cause memory pressure or long blocking work during app startup.

Questions

Is eager loading of all cache spans during SimpleCache initialization mandatory, or is there a supported way to:

Lazily load cache entries, or
Defer span creation until content is actually requested?

Is it required to maintain a full cache index for all downloaded videos upfront?

Would it be valid/supported to create or prepare cache only when a user attempts to play a video, instead of indexing the entire cache at startup?
Are there recommended best practices for handling large offline libraries (100+ videos) with Media3 to avoid startup crashes or OOMs?
e.g., cache size limits, multiple caches, or alternative cache strategies.

Bug Report

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions