diff --git a/cmd/mount.go b/cmd/mount.go index 023801cb84..9eea8f87ab 100644 --- a/cmd/mount.go +++ b/cmd/mount.go @@ -109,6 +109,7 @@ be interacting with the file system.`) DummyIOCfg: newConfig.DummyIo, IsTypeCacheDeprecated: newConfig.EnableTypeCacheDeprecation, ImplicitDir: newConfig.ImplicitDirs, + EnableOptimizedMetadataCache: newConfig.MetadataCache.ExperimentalEnableOptimizedMetadataCache, EnableEmptyManagedFolders: newConfig.List.EnableEmptyManagedFolders, } bm := gcsx.NewBucketManager(bucketCfg, storageHandle) diff --git a/internal/cache/lru/lru_memory_test.go b/internal/cache/lru/lru_memory_test.go index 4c55c7fb2c..5103178666 100644 --- a/internal/cache/lru/lru_memory_test.go +++ b/internal/cache/lru/lru_memory_test.go @@ -77,24 +77,48 @@ func (d dummyValue) Size() uint64 { func TestMapCacheWorkloads(t *testing.T) { count := 1000000 // 1 Million files capacity := uint64(count * 1000) - workloads := []string{"flat", "nested", "deeply_nested"} - for _, w := range workloads { - t.Logf("=== WORKLOAD: %s (%d files) ===", w, count) + tests := []struct { + name string + workload string + isRadix bool + }{ + {"Flat_Map", "flat", false}, + {"Flat_Radix", "flat", true}, + {"Nested_Map", "nested", false}, + {"Nested_Radix", "nested", true}, + {"DeeplyNested_Map", "deeply_nested", false}, + {"DeeplyNested_Radix", "deeply_nested", true}, + } - // 1. Pure MapLRU - paths := generatePaths(w, count) - baseMem := getMemStats() + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Arrange + paths := generatePaths(tc.workload, count) + baseMem := getMemStats() + var cache lru.Cache + if tc.isRadix { + cache = lru.NewRadixCache(capacity) + } else { + cache = lru.NewCache(capacity) + } - pureCache := lru.NewCache(capacity) - for _, p := range paths { - _, _ = pureCache.Insert(p, dummyValue{}) - } - alloc := getMemStats() - pureMem := alloc - baseMem - runtime.KeepAlive(pureCache) - runtime.KeepAlive(paths) + // Act + for _, p := range paths { + _, _ = cache.Insert(p, dummyValue{}) + } + + // Assert (or Log in this case) + alloc := getMemStats() + pureMem := alloc - baseMem + runtime.KeepAlive(cache) + runtime.KeepAlive(paths) - t.Logf("%-20s Heap Used: %10.2f MB\n", "MapLRU", float64(pureMem)/(1024*1024)) + cacheType := "MapLRU" + if tc.isRadix { + cacheType = "RadixLRU" + } + t.Logf("%-20s Heap Used: %10.2f MB\n", cacheType, float64(pureMem)/(1024*1024)) + }) } } diff --git a/internal/cache/lru/lru_workload_benchmark_test.go b/internal/cache/lru/lru_workload_benchmark_test.go index 5090faae09..9a36afcd41 100644 --- a/internal/cache/lru/lru_workload_benchmark_test.go +++ b/internal/cache/lru/lru_workload_benchmark_test.go @@ -90,19 +90,15 @@ func benchmarkErasePrefix(b *testing.B, cache lru.Cache, prefixMap map[string][] i := 0 for b.Loop() { prefix := prefixes[i%len(prefixes)] - - b.StartTimer() cache.EraseEntriesWithGivenPrefix(prefix) - //reset the timer so that we only measure erasure time - b.StopTimer() - //restore the map - keysToRestore := prefixMap[prefix] - for _, key := range keysToRestore { + // UNTIMED: Pause clock, restore erased keys, and restart timer for next iteration + b.StopTimer() + for _, key := range prefixMap[prefix] { _, _ = cache.Insert(key, data) } - i++ + b.StartTimer() // Timer MUST be running when b.Loop() evaluates next! } } @@ -116,14 +112,23 @@ func runBenchmarks(b *testing.B, name string, depth int) { b.Run(name+"_MapLRU_Insert", func(b *testing.B) { benchmarkInsert(b, lru.NewCache(capacity), keys) }) + b.Run(name+"_RadixLRU_Insert", func(b *testing.B) { + benchmarkInsert(b, lru.NewRadixCache(capacity), keys) + }) b.Run(name+"_MapLRU_Lookup", func(b *testing.B) { benchmarkLookup(b, lru.NewCache(capacity), keys) }) + b.Run(name+"_RadixLRU_Lookup", func(b *testing.B) { + benchmarkLookup(b, lru.NewRadixCache(capacity), keys) + }) b.Run(name+"_MapLRU_ErasePrefix", func(b *testing.B) { benchmarkErasePrefix(b, lru.NewCache(capacity), prefixMap, prefixes) }) + b.Run(name+"_RadixLRU_ErasePrefix", func(b *testing.B) { + benchmarkErasePrefix(b, lru.NewRadixCache(capacity), prefixMap, prefixes) + }) } func BenchmarkLRU_Flat(b *testing.B) { diff --git a/internal/cache/metadata/stat_cache_test.go b/internal/cache/metadata/stat_cache_test.go index 597de2666c..38210b5a80 100644 --- a/internal/cache/metadata/stat_cache_test.go +++ b/internal/cache/metadata/stat_cache_test.go @@ -435,6 +435,10 @@ func (t *MultiBucketStatCacheTest) Test_ExpiresLeastRecentlyUsed() { // Insert another. saffron := &gcs.MinObject{Name: "saffron"} spices.Insert(saffron, expiration) // size = 1424 bytes (cumulative = 5688 bytes) + + saffron2 := &gcs.MinObject{Name: "saffron2"} + spices.Insert(saffron2, expiration) + // This will evict the least recent entry, i.e. orange. // See what's left. @@ -543,7 +547,7 @@ func (t *StatCacheTest) Test_ShouldReturnHitTrueWhenOnlyObjectAlreadyHasEntry() } func (t *StatCacheTest) Test_ShouldEvictEntryOnFullCapacityIncludingFolderSize() { - localCache := lru.NewCache(uint64(2700)) + localCache := lru.NewCache(uint64(2600)) t.statCache = metadata.NewStatCacheBucketView(localCache, "local_bucket") objectEntry1 := &gcs.MinObject{Name: "1"} objectEntry2 := &gcs.MinObject{Name: "2"} diff --git a/internal/cache/metadata/statcache_memory_test.go b/internal/cache/metadata/statcache_memory_test.go index 3810bd77aa..b03845e192 100644 --- a/internal/cache/metadata/statcache_memory_test.go +++ b/internal/cache/metadata/statcache_memory_test.go @@ -73,28 +73,50 @@ func generatePaths(workload string, count int) []string { func TestStatCacheMemoryWorkloads(t *testing.T) { count := 1000000 // 1 Million files capacity := uint64(count * 100000) // high capacity to avoid eviction - workloads := []string{"flat", "nested", "deeply_nested"} expiration := time.Now().Add(time.Hour) - for _, w := range workloads { - t.Logf("=== WORKLOAD: %s (%d files) ===", w, count) - - // 1. StatCache - paths := generatePaths(w, count) - baseMem := getMemStats() + tests := []struct { + name string + workload string + isRadix bool + }{ + {"Flat_Map", "flat", false}, + {"Flat_Radix", "flat", true}, + {"Nested_Map", "nested", false}, + {"Nested_Radix", "nested", true}, + {"DeeplyNested_Map", "deeply_nested", false}, + {"DeeplyNested_Radix", "deeply_nested", true}, + } - sharedCache := lru.NewCache(capacity) - statCache := metadata.NewStatCacheBucketView(sharedCache, "") + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Arrange + paths := generatePaths(tc.workload, count) + baseMem := getMemStats() + var sharedCache lru.Cache + if tc.isRadix { + sharedCache = lru.NewRadixCache(capacity) + } else { + sharedCache = lru.NewCache(capacity) + } + statCache := metadata.NewStatCacheBucketView(sharedCache, "") - for _, p := range paths { - statCache.Insert(&gcs.MinObject{Name: p}, expiration) - } + // Act + for _, p := range paths { + statCache.Insert(&gcs.MinObject{Name: p}, expiration) + } - alloc := getMemStats() - pureMem := alloc - baseMem - runtime.KeepAlive(statCache) - runtime.KeepAlive(paths) + // Assert (or Log in this case) + alloc := getMemStats() + pureMem := alloc - baseMem + runtime.KeepAlive(statCache) + runtime.KeepAlive(paths) - t.Logf("%-20s Heap Used: %10.2f MB\n", "StatCache", float64(pureMem)/(1024*1024)) + cacheType := "Map StatCache" + if tc.isRadix { + cacheType = "Radix StatCache" + } + t.Logf("%-20s Heap Used: %10.2f MB\n", cacheType, float64(pureMem)/(1024*1024)) + }) } } diff --git a/internal/gcsx/bucket_manager.go b/internal/gcsx/bucket_manager.go index 0c65d1629e..bf093d57c6 100644 --- a/internal/gcsx/bucket_manager.go +++ b/internal/gcsx/bucket_manager.go @@ -79,7 +79,8 @@ type BucketConfig struct { ImplicitDir bool - EnableEmptyManagedFolders bool + EnableOptimizedMetadataCache bool + EnableEmptyManagedFolders bool } // BucketManager manages the lifecycle of buckets. @@ -105,7 +106,12 @@ type bucketManager struct { func NewBucketManager(config BucketConfig, storageHandle storage.StorageHandle) BucketManager { var c lru.Cache if config.StatCacheMaxSizeMB > 0 { - c = lru.NewCache(util.MiBsToBytes(config.StatCacheMaxSizeMB)) + cacheSize := util.MiBsToBytes(config.StatCacheMaxSizeMB) + if config.EnableOptimizedMetadataCache { + c = lru.NewRadixCache(cacheSize) + } else { + c = lru.NewCache(cacheSize) + } } bm := &bucketManager{