Skip to content

Commit

Permalink
Shard SimpleCache files into 10 sub-directories
Browse files Browse the repository at this point in the history
Issue: #4253
PiperOrigin-RevId: 232659869
  • Loading branch information
ojw28 committed Feb 6, 2019
1 parent 2169b94 commit 3845304
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<File> lockedCacheDirs = new HashSet<>();

private static boolean cacheFolderLockingDisabled;
Expand All @@ -44,6 +53,7 @@ public final class SimpleCache implements Cache {
private final CacheEvictor evictor;
private final CachedContentIndex index;
private final HashMap<String, ArrayList<Listener>> listeners;
private final Random random;

private long totalSpace;
private boolean released;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void testCommittingOneFile() throws Exception {
NavigableSet<CacheSpan> 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);

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down

1 comment on commit 3845304

@halaei
Copy link

@halaei halaei commented on 3845304 Aug 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I understand why server-side cache systems have subdirectories! Although the differences are:

  1. They use 2 (or more) levels of sub-directores, instead of 1 level that is used here.
  2. There are usually more sub-directories than 10 at each level, e.g. 256, from '00', to 'ff'.
  3. Sub-directories are generated only if required, and not during the cache initialization.

Example implementations:

  1. Appache (https://httpd.apache.org/docs/trunk/mod/mod_cache_disk.html): see CacheDirLength and CacheDirLevels.
  2. Laravel: (https://github.com/laravel/framework/blob/6dd25f4073cfd0fb48a9a35c1648cced2e4f331c/src/Illuminate/Cache/FileStore.php#L256)

@ojw28 Maybe the same approach could be used here as well?

Please sign in to comment.