Skip to content

Commit

Permalink
fs: recreate kernelNodeIds & stableAttrs maps when they shrink
Browse files Browse the repository at this point in the history
Maps do not free all memory when elements get deleted
( golang/go#20135 ).

As a workaround, we recreate our two big maps (kernelNodeIds & stableAttrs)
when they have shrunk dramatically (100 x smaller).

Benchmarkung with loopback (go version go1.16.2 linux/amd64) shows:

Before this change:

Step               VmRSS (kiB)
-----              -----------
Fresh mount          4000
ls -R 500k files   271100
after drop_cache   336448
wait ~ 3 minutes   101588

After:

Step               VmRSS (kiB)
-----              -----------
Fresh mount          4012
ls -R 500k files   271100
after drop_cache    31528

Results for gocryptfs are similar.

Has survived xfstests via gocryptfs.

Fixes: rfjakob/gocryptfs#569
Change-Id: Idcae1ab953270516735839a034d586717647b8db
  • Loading branch information
rfjakob authored and hanwen committed Jun 11, 2021
1 parent f4f2789 commit 24a1dfe
Showing 1 changed file with 51 additions and 1 deletion.
52 changes: 51 additions & 1 deletion fs/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package fs
import (
"context"
"log"
"runtime/debug"
"sync"
"syscall"
"time"
Expand Down Expand Up @@ -81,6 +82,12 @@ type rawBridge struct {
kernelNodeIds map[uint64]*Inode
// nextNodeID is the next free NodeID. Increment after copying the value.
nextNodeId uint64
// nodeCountHigh records the highest number of entries we had in the
// kernelNodeIds map.
// As the size of stableAttrs tracks kernelNodeIds (+- a few entries due to
// concurrent FORGETs, LOOKUPs, and the fixed NodeID 1), this is also a good
// estimate for stableAttrs.
nodeCountHigh int

files []*fileEntry
freeFiles []uint32
Expand Down Expand Up @@ -201,6 +208,9 @@ func (b *rawBridge) addNewChild(parent *Inode, name string, child *Inode, file F
child.changeCounter++

b.kernelNodeIds[child.nodeId] = child
if len(b.kernelNodeIds) > b.nodeCountHigh {
b.nodeCountHigh = len(b.kernelNodeIds)
}
// Any node that might be there is overwritten - it is obsolete now
b.stableAttrs[id] = child
if file != nil {
Expand Down Expand Up @@ -465,7 +475,47 @@ func (b *rawBridge) Create(cancel <-chan struct{}, input *fuse.CreateIn, name st

func (b *rawBridge) Forget(nodeid, nlookup uint64) {
n, _ := b.inode(nodeid, 0)
n.removeRef(nlookup, false)
forgotten, _ := n.removeRef(nlookup, false)

if forgotten {
b.compactMemory()
}
}

// compactMemory tries to free memory that was previously used by forgotten
// nodes.
//
// Maps do not free all memory when elements get deleted
// ( https://github.com/golang/go/issues/20135 ).
// As a workaround, we recreate our two big maps (stableAttrs & kernelNodeIds)
// every time they have shrunk dramatically (100 x smaller).
// In this case, `nodeCountHigh` is reset to the new (smaller) size.
func (b *rawBridge) compactMemory() {
b.mu.Lock()

if b.nodeCountHigh <= len(b.kernelNodeIds)*100 {
b.mu.Unlock()
return
}

tmpStableAttrs := make(map[StableAttr]*Inode, len(b.stableAttrs))
for i, v := range b.stableAttrs {
tmpStableAttrs[i] = v
}
b.stableAttrs = tmpStableAttrs

tmpKernelNodeIds := make(map[uint64]*Inode, len(b.kernelNodeIds))
for i, v := range b.kernelNodeIds {
tmpKernelNodeIds[i] = v
}
b.kernelNodeIds = tmpKernelNodeIds

b.nodeCountHigh = len(b.kernelNodeIds)

b.mu.Unlock()

// Run outside b.mu
debug.FreeOSMemory()
}

func (b *rawBridge) SetDebug(debug bool) {}
Expand Down

0 comments on commit 24a1dfe

Please sign in to comment.