Skip to content

Commit efa688d

Browse files
committed
Restructuring the cache file/directory layout for better performance.
1 parent 50846d6 commit efa688d

File tree

1 file changed

+29
-15
lines changed

1 file changed

+29
-15
lines changed

go/pkg/diskcache/diskcache.go

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ const maxConcurrentRequests = 1000
9191
// DiskCache is a local disk LRU CAS and Action Cache cache.
9292
type DiskCache struct {
9393
root string // path to the root directory of the disk cache.
94+
prefixes sync.Map // set of existing 2-character digest prefixes.
9495
maxCapacityBytes uint64 // if disk size exceeds this, old items will be evicted as needed.
9596
mu sync.Mutex // protects the queue.
9697
store sync.Map // map of keys to qitems.
@@ -125,15 +126,18 @@ func New(ctx context.Context, root string, maxCapacityBytes uint64) *DiskCache {
125126
if d.IsDir() {
126127
return nil
127128
}
128-
fname := d.Name()
129-
k, err := res.getKeyFromFileName(fname)
129+
// We use Git's directory/file naming structure as inspiration:
130+
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects#:~:text=The%20subdirectory%20is%20named%20with%20the%20first%202%20characters%20of%20the%20SHA%2D1%2C%20and%20the%20filename%20is%20the%20remaining%2038%20characters.
131+
subdir := filepath.Base(filepath.Dir(path))
132+
res.prefixes.Store(subdir, true)
133+
k, err := res.getKeyFromFileName(subdir + d.Name())
130134
if err != nil {
131-
log.Errorf("Error parsing cached file name %s: %v", fname, err)
135+
log.Errorf("Error parsing cached file name %s: %v", path, err)
132136
return nil
133137
}
134-
atime, err := GetLastAccessTime(filepath.Join(root, fname))
138+
atime, err := GetLastAccessTime(path)
135139
if err != nil {
136-
log.Errorf("Error getting last accessed time of %s: %v", fname, err)
140+
log.Errorf("Error getting last accessed time of %s: %v", path, err)
137141
return nil
138142
}
139143
it := &qitem{
@@ -142,7 +146,7 @@ func New(ctx context.Context, root string, maxCapacityBytes uint64) *DiskCache {
142146
}
143147
size, err := res.getItemSize(k)
144148
if err != nil {
145-
log.Errorf("Error getting file size of %s: %v", fname, err)
149+
log.Errorf("Error getting file size of %s: %v", path, err)
146150
return nil
147151
}
148152
res.store.Store(k, it)
@@ -175,25 +179,25 @@ func (d *DiskCache) TotalSizeBytes() uint64 {
175179
return uint64(atomic.LoadInt64(&d.sizeBytes))
176180
}
177181

178-
// This function is defined in https://pkg.go.dev/strings#CutPrefix
182+
// This function is defined in https://pkg.go.dev/strings#CutSuffix
179183
// It is copy/pasted here as a hack, because I failed to upgrade the *Reclient* repo to the latest Go 1.20.7.
180-
func CutPrefix(s, prefix string) (after string, found bool) {
181-
if !strings.HasPrefix(s, prefix) {
184+
func CutSuffix(s, suffix string) (before string, found bool) {
185+
if !strings.HasSuffix(s, suffix) {
182186
return s, false
183187
}
184-
return s[len(prefix):], true
188+
return s[:len(s)-len(suffix)], true
185189
}
186190

187191
func (d *DiskCache) getKeyFromFileName(fname string) (key, error) {
188192
pair := strings.Split(fname, ".")
189193
if len(pair) != 2 {
190-
return key{}, fmt.Errorf("Expected file name in the form [ac_]hash/size, got %s", fname)
194+
return key{}, fmt.Errorf("expected file name in the form [ac_]hash/size, got %s", fname)
191195
}
192196
size, err := strconv.ParseInt(pair[1], 10, 64)
193197
if err != nil {
194198
return key{}, fmt.Errorf("invalid size in digest %s: %s", fname, err)
195199
}
196-
hash, isAc := CutPrefix(pair[0], "ac_")
200+
hash, isAc := CutSuffix(pair[0], "ac_")
197201
dg, err := digest.New(hash, size)
198202
if err != nil {
199203
return key{}, fmt.Errorf("invalid digest from file name %s: %v", fname, err)
@@ -202,11 +206,18 @@ func (d *DiskCache) getKeyFromFileName(fname string) (key, error) {
202206
}
203207

204208
func (d *DiskCache) getPath(k key) string {
205-
prefix := ""
209+
suffix := ""
206210
if !k.isCas {
207-
prefix = "ac_"
211+
suffix = "_ac"
212+
}
213+
return filepath.Join(d.root, k.digest.Hash[:2], fmt.Sprintf("%s%s.%d", k.digest.Hash[2:], suffix, k.digest.Size))
214+
}
215+
216+
func (d *DiskCache) maybeCreatePrefixDir(k key) {
217+
prefix := k.digest.Hash[:2]
218+
if _, loaded := d.prefixes.LoadOrStore(prefix, true); !loaded {
219+
os.MkdirAll(filepath.Join(d.root, prefix), os.ModePerm)
208220
}
209-
return filepath.Join(d.root, fmt.Sprintf("%s%s.%d", prefix, k.digest.Hash, k.digest.Size))
210221
}
211222

212223
func (d *DiskCache) StoreCas(dg digest.Digest, path string) error {
@@ -226,6 +237,7 @@ func (d *DiskCache) StoreCas(dg digest.Digest, path string) error {
226237
d.mu.Lock()
227238
heap.Push(d.queue, it)
228239
d.mu.Unlock()
240+
d.maybeCreatePrefixDir(it.key)
229241
if err := copyFile(path, d.getPath(it.key), dg.Size); err != nil {
230242
return err
231243
}
@@ -259,6 +271,7 @@ func (d *DiskCache) StoreActionCache(dg digest.Digest, ar *repb.ActionResult) er
259271
d.mu.Lock()
260272
heap.Push(d.queue, it)
261273
d.mu.Unlock()
274+
d.maybeCreatePrefixDir(it.key)
262275
if err := os.WriteFile(d.getPath(it.key), bytes, 0644); err != nil {
263276
return err
264277
}
@@ -292,6 +305,7 @@ func (d *DiskCache) gc() {
292305
}
293306
atomic.AddInt64(&d.sizeBytes, -size)
294307
it.mu.Lock()
308+
// We only delete the files, and not the prefix directories, because the prefixes are not worth worrying about.
295309
if err := os.Remove(d.getPath(it.key)); err != nil {
296310
log.Errorf("Error removing file: %v", err)
297311
}

0 commit comments

Comments
 (0)