diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index d2091c362a3..72b39e24d17 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.NavigableSet; +import java.util.Random; import java.util.Set; import java.util.TreeSet; @@ -36,6 +37,14 @@ public final class SimpleCache implements Cache { private static final String TAG = "SimpleCache"; + /** + * Cache files are distributed between a number of subdirectories. This helps to avoid poor + * performance in cases where the performance of the underlying file system (e.g. FAT32) scales + * badly with the number of files per directory. See + * https://github.com/google/ExoPlayer/issues/4253. + */ + private static final int SUBDIRECTORY_COUNT = 10; + private static final HashSet lockedCacheDirs = new HashSet<>(); private static boolean cacheFolderLockingDisabled; @@ -44,6 +53,7 @@ public final class SimpleCache implements Cache { private final CacheEvictor evictor; private final CachedContentIndex index; private final HashMap> listeners; + private final Random random; private long totalSpace; private boolean released; @@ -128,7 +138,8 @@ public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolea this.cacheDir = cacheDir; this.evictor = evictor; this.index = index; - this.listeners = new HashMap<>(); + listeners = new HashMap<>(); + random = new Random(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); @@ -271,8 +282,13 @@ public synchronized File startFile(String key, long position, long length) throw removeStaleSpans(); } evictor.onStartFile(this, key, position, length); - return SimpleCacheSpan.getCacheFile( - cacheDir, cachedContent.id, position, System.currentTimeMillis()); + // Randomly distribute files into subdirectories with a uniform distribution. + File fileDir = new File(cacheDir, Integer.toString(random.nextInt(SUBDIRECTORY_COUNT))); + if (!fileDir.exists()) { + fileDir.mkdir(); + } + long lastAccessTimestamp = System.currentTimeMillis(); + return SimpleCacheSpan.getCacheFile(fileDir, cachedContent.id, position, lastAccessTimestamp); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index c35e3974f67..decbe80c84f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -26,7 +26,9 @@ /** This class stores span metadata in filename. */ /* package */ final class SimpleCacheSpan extends CacheSpan { - private static final String SUFFIX = ".v3.exo"; + /* package */ static final String COMMON_SUFFIX = ".exo"; + + private static final String SUFFIX = ".v3" + COMMON_SUFFIX; private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile( "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL); private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index bdb9d4f9d9d..6140d0ac82e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -75,7 +75,7 @@ public void testCommittingOneFile() throws Exception { NavigableSet cachedSpans = simpleCache.getCachedSpans(KEY_1); assertThat(cachedSpans.isEmpty()).isTrue(); assertThat(simpleCache.getCacheSpace()).isEqualTo(0); - assertThat(cacheDir.listFiles()).hasLength(0); + assertNoCacheFiles(cacheDir); addCache(simpleCache, KEY_1, 0, 15); @@ -233,7 +233,7 @@ public void testEncryptedIndexWrongKey() throws Exception { // Cache should be cleared assertThat(simpleCache.getKeys()).isEmpty(); - assertThat(cacheDir.listFiles()).hasLength(0); + assertNoCacheFiles(cacheDir); } @Test @@ -252,7 +252,7 @@ public void testEncryptedIndexLostKey() throws Exception { // Cache should be cleared assertThat(simpleCache.getKeys()).isEmpty(); - assertThat(cacheDir.listFiles()).hasLength(0); + assertNoCacheFiles(cacheDir); } @Test @@ -391,6 +391,20 @@ private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOEx } } + private static void assertNoCacheFiles(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (file.isDirectory()) { + assertNoCacheFiles(file); + } else { + assertThat(file.getName().endsWith(SimpleCacheSpan.COMMON_SUFFIX)).isFalse(); + } + } + } + private static byte[] generateData(String key, int position, int length) { byte[] bytes = new byte[length]; new Random((long) (key.hashCode() ^ position)).nextBytes(bytes);