Share nodes with equal IDs between checkouts.

Change-Id: I30284cd7a58f31fa8f2aa566a53fcec4d99bda25
diff --git a/fs/gitilesfs.go b/fs/gitilesfs.go
index f9d5d1e..443f1f7 100644
--- a/fs/gitilesfs.go
+++ b/fs/gitilesfs.go
@@ -35,6 +35,8 @@
 type gitilesRoot struct {
 	nodefs.Node
 
+	nodeCache *nodeCache
+
 	cache   *cache.Cache
 	service *gitiles.RepoService
 	tree    *gitiles.Tree
@@ -244,13 +246,14 @@
 // NewGitilesRoot returns the root node for a file system.
 func NewGitilesRoot(c *cache.Cache, tree *gitiles.Tree, service *gitiles.RepoService, options GitilesOptions) nodefs.Node {
 	r := &gitilesRoot{
-		Node:     nodefs.NewDefaultNode(),
-		service:  service,
-		cache:    c,
-		shaMap:   map[git.Oid]string{},
-		tree:     tree,
-		opts:     options,
-		lazyRepo: cache.NewLazyRepo(options.CloneURL, c),
+		Node:      nodefs.NewDefaultNode(),
+		service:   service,
+		nodeCache: newNodeCache(),
+		cache:     c,
+		shaMap:    map[git.Oid]string{},
+		tree:      tree,
+		opts:      options,
+		lazyRepo:  cache.NewLazyRepo(options.CloneURL, c),
 	}
 
 	return r
@@ -306,27 +309,36 @@
 			}
 		}
 
-		n := &gitilesNode{
-			Node:  nodefs.NewDefaultNode(),
-			id:    *id,
-			mode:  uint32(e.Mode),
-			clone: clone,
-			root:  r,
-			// Ninja uses mtime == 0 as "doesn't exist"
-			// flag, (see ninja/files/src/graph.h:66), so
-			// use a nonzero timestamp here.
-			mtime: time.Unix(1, 0),
-		}
-		if e.Size != nil {
-			n.size = int64(*e.Size)
+		xbit := e.Mode&0111 != 0
+		n := r.nodeCache.get(id, xbit)
+		if n == nil {
+			n = &gitilesNode{
+				Node:  nodefs.NewDefaultNode(),
+				id:    *id,
+				mode:  uint32(e.Mode),
+				clone: clone,
+				root:  r,
+				// Ninja uses mtime == 0 as "doesn't exist"
+				// flag, (see ninja/files/src/graph.h:66), so
+				// use a nonzero timestamp here.
+				mtime: time.Unix(1, 0),
+			}
+			if e.Size != nil {
+				n.size = int64(*e.Size)
+			}
+
+			if e.Target != nil {
+				n.linkTarget = []byte(*e.Target)
+				n.size = int64(len(n.linkTarget))
+			}
+
+			r.shaMap[*id] = p
+			parent.NewChild(base, false, n)
+			r.nodeCache.add(n)
+		} else {
+			parent.AddChild(base, n.Inode())
 		}
 
-		if e.Target != nil {
-			n.linkTarget = []byte(*e.Target)
-			n.size = int64(len(n.linkTarget))
-		}
-		r.shaMap[*id] = p
-		parent.NewChild(base, false, n)
 	}
 
 	r.Inode().NewChild(".gitid",
diff --git a/fs/gitilesfs_test.go b/fs/gitilesfs_test.go
index 49dc122..915420e 100644
--- a/fs/gitilesfs_test.go
+++ b/fs/gitilesfs_test.go
@@ -40,8 +40,10 @@
 
 func init() {
 	enc := map[string]string{
-		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORS?format=TEXT": `IyBUaGlzIGlzIHRoZSBvZmZpY2lhbCBsaXN0IG9mIGdsb2cgYXV0aG9ycyBmb3IgY29weXJpZ2h0IHB1cnBvc2VzLgojIFRoaXMgZmlsZSBpcyBkaXN0aW5jdCBmcm9tIHRoZSBDT05UUklCVVRPUlMgZmlsZXMuCiMgU2VlIHRoZSBsYXR0ZXIgZm9yIGFuIGV4cGxhbmF0aW9uLgojCiMgTmFtZXMgc2hvdWxkIGJlIGFkZGVkIHRvIHRoaXMgZmlsZSBhczoKIwlOYW1lIG9yIE9yZ2FuaXphdGlvbiA8ZW1haWwgYWRkcmVzcz4KIyBUaGUgZW1haWwgYWRkcmVzcyBpcyBub3QgcmVxdWlyZWQgZm9yIG9yZ2FuaXphdGlvbnMuCiMKIyBQbGVhc2Uga2VlcCB0aGUgbGlzdCBzb3J0ZWQuCgpLb3VoZWkgU3V0b3UgPGtvdUBjb3ptaXhuZy5vcmc+Ckdvb2dsZSBJbmMuCg==`,
-		"/platform/build/kati/+/ce34badf691d36e8048b63f89d1a86ee5fa4325c/testcase/addprefix.mk":   "dGVzdDoKCWVjaG8gJChhZGRwcmVmaXggc3JjLyxmb28gYmFyKQo=",
+		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORS?format=TEXT":  `IyBUaGlzIGlzIHRoZSBvZmZpY2lhbCBsaXN0IG9mIGdsb2cgYXV0aG9ycyBmb3IgY29weXJpZ2h0IHB1cnBvc2VzLgojIFRoaXMgZmlsZSBpcyBkaXN0aW5jdCBmcm9tIHRoZSBDT05UUklCVVRPUlMgZmlsZXMuCiMgU2VlIHRoZSBsYXR0ZXIgZm9yIGFuIGV4cGxhbmF0aW9uLgojCiMgTmFtZXMgc2hvdWxkIGJlIGFkZGVkIHRvIHRoaXMgZmlsZSBhczoKIwlOYW1lIG9yIE9yZ2FuaXphdGlvbiA8ZW1haWwgYWRkcmVzcz4KIyBUaGUgZW1haWwgYWRkcmVzcyBpcyBub3QgcmVxdWlyZWQgZm9yIG9yZ2FuaXphdGlvbnMuCiMKIyBQbGVhc2Uga2VlcCB0aGUgbGlzdCBzb3J0ZWQuCgpLb3VoZWkgU3V0b3UgPGtvdUBjb3ptaXhuZy5vcmc+Ckdvb2dsZSBJbmMuCg==`,
+		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORSx?format=TEXT": `IyBUaGlzIGlzIHRoZSBvZmZpY2lhbCBsaXN0IG9mIGdsb2cgYXV0aG9ycyBmb3IgY29weXJpZ2h0IHB1cnBvc2VzLgojIFRoaXMgZmlsZSBpcyBkaXN0aW5jdCBmcm9tIHRoZSBDT05UUklCVVRPUlMgZmlsZXMuCiMgU2VlIHRoZSBsYXR0ZXIgZm9yIGFuIGV4cGxhbmF0aW9uLgojCiMgTmFtZXMgc2hvdWxkIGJlIGFkZGVkIHRvIHRoaXMgZmlsZSBhczoKIwlOYW1lIG9yIE9yZ2FuaXphdGlvbiA8ZW1haWwgYWRkcmVzcz4KIyBUaGUgZW1haWwgYWRkcmVzcyBpcyBub3QgcmVxdWlyZWQgZm9yIG9yZ2FuaXphdGlvbnMuCiMKIyBQbGVhc2Uga2VlcCB0aGUgbGlzdCBzb3J0ZWQuCgpLb3VoZWkgU3V0b3UgPGtvdUBjb3ptaXhuZy5vcmc+Ckdvb2dsZSBJbmMuCg==`,
+		"/platform/build/kati/+show/ce34badf691d36e8048b63f89d1a86ee5fa4325c/AUTHORS2?format=TEXT": `IyBUaGlzIGlzIHRoZSBvZmZpY2lhbCBsaXN0IG9mIGdsb2cgYXV0aG9ycyBmb3IgY29weXJpZ2h0IHB1cnBvc2VzLgojIFRoaXMgZmlsZSBpcyBkaXN0aW5jdCBmcm9tIHRoZSBDT05UUklCVVRPUlMgZmlsZXMuCiMgU2VlIHRoZSBsYXR0ZXIgZm9yIGFuIGV4cGxhbmF0aW9uLgojCiMgTmFtZXMgc2hvdWxkIGJlIGFkZGVkIHRvIHRoaXMgZmlsZSBhczoKIwlOYW1lIG9yIE9yZ2FuaXphdGlvbiA8ZW1haWwgYWRkcmVzcz4KIyBUaGUgZW1haWwgYWRkcmVzcyBpcyBub3QgcmVxdWlyZWQgZm9yIG9yZ2FuaXphdGlvbnMuCiMKIyBQbGVhc2Uga2VlcCB0aGUgbGlzdCBzb3J0ZWQuCgpLb3VoZWkgU3V0b3UgPGtvdUBjb3ptaXhuZy5vcmc+Ckdvb2dsZSBJbmMuCg==`,
+		"/platform/build/kati/+/ce34badf691d36e8048b63f89d1a86ee5fa4325c/testcase/addprefix.mk":    "dGVzdDoKCWVjaG8gJChhZGRwcmVmaXggc3JjLyxmb28gYmFyKQo=",
 	}
 	for k, v := range enc {
 		c := make([]byte, base64.StdEncoding.DecodedLen(len(v)))
@@ -106,6 +108,20 @@
     {
       "mode": 33188,
       "type": "blob",
+      "id": "787d767f94fd634ed29cd69ec9f93bab2b25f5d4",
+      "name": "AUTHORS2",
+      "size": 373
+    },
+    {
+      "mode": 33261,
+      "type": "blob",
+      "id": "787d767f94fd634ed29cd69ec9f93bab2b25f5d4",
+      "name": "AUTHORSx",
+      "size": 373
+    },
+    {
+      "mode": 33188,
+      "type": "blob",
       "id": "91c29720b08211898308eb2b6bde8bd3208c6dcd",
       "name": "Android.bp",
       "size": 1935
@@ -163,6 +179,45 @@
 	return listener, err
 }
 
+func TestGitilesFSSharedNodes(t *testing.T) {
+	fix, err := newTestFixture()
+	if err != nil {
+		t.Fatal("newTestFixture", err)
+	}
+	defer fix.cleanup()
+
+	repoService := fix.service.NewRepoService("platform/build/kati")
+	treeResp, err := repoService.GetTree("ce34badf691d36e8048b63f89d1a86ee5fa4325c", "", true)
+	if err != nil {
+		t.Fatal("Tree:", err)
+	}
+
+	options := GitilesOptions{}
+
+	fs := NewGitilesRoot(fix.cache, treeResp, repoService, options)
+	if err := fix.mount(fs); err != nil {
+		t.Fatal("mount", err)
+	}
+
+	ch1 := fs.Inode().GetChild("AUTHORS")
+	if ch1 == nil {
+		t.Fatalf("node for AUTHORS not found")
+	}
+
+	ch2 := fs.Inode().GetChild("AUTHORS2")
+	if ch2 == nil {
+		t.Fatalf("node for AUTHORS2 not found")
+	}
+
+	if ch1 != ch2 {
+		t.Error("equal blobs did not share inodes.")
+	}
+	ch3 := fs.Inode().GetChild("AUTHORSx")
+	if ch1 == ch3 {
+		t.Error("blob with different modes shared inode.")
+	}
+}
+
 func TestGitilesFSTreeID(t *testing.T) {
 	fix, err := newTestFixture()
 	if err != nil {
diff --git a/fs/manifestfs.go b/fs/manifestfs.go
index 7eae4b0..ec5d973 100644
--- a/fs/manifestfs.go
+++ b/fs/manifestfs.go
@@ -33,7 +33,8 @@
 
 	service *gitiles.Service
 
-	cache *cache.Cache
+	cache     *cache.Cache
+	nodeCache *nodeCache
 
 	// trees is Path => Tree map.
 	trees map[string]*gitiles.Tree
@@ -58,6 +59,7 @@
 	}
 	root := &manifestFSRoot{
 		Node:        nodefs.NewDefaultNode(),
+		nodeCache:   newNodeCache(),
 		cache:       cache,
 		service:     service,
 		options:     opts,
@@ -147,6 +149,7 @@
 			}
 
 			subRoot := NewGitilesRoot(fs.cache, fs.trees[p], repoService, opts)
+			subRoot.(*gitilesRoot).nodeCache = fs.nodeCache
 			parent.NewChild(base, true, subRoot)
 			if err := subRoot.(*gitilesRoot).onMount(fsConn); err != nil {
 				return fmt.Errorf("onMount(%s): %v", p, err)
diff --git a/fs/multifs.go b/fs/multifs.go
index 97babe9..444b49a 100644
--- a/fs/multifs.go
+++ b/fs/multifs.go
@@ -28,10 +28,11 @@
 
 type multiManifestFSRoot struct {
 	nodefs.Node
-	cache   *cache.Cache
-	fsConn  *nodefs.FileSystemConnector
-	options MultiFSOptions
-	gitiles *gitiles.Service
+	nodeCache *nodeCache
+	cache     *cache.Cache
+	fsConn    *nodefs.FileSystemConnector
+	options   MultiFSOptions
+	gitiles   *gitiles.Service
 }
 
 func (r *multiManifestFSRoot) OnMount(fsConn *nodefs.FileSystemConnector) {
@@ -46,10 +47,11 @@
 
 func NewMultiFS(service *gitiles.Service, c *cache.Cache, options MultiFSOptions) *multiManifestFSRoot {
 	r := &multiManifestFSRoot{
-		Node:    nodefs.NewDefaultNode(),
-		cache:   c,
-		options: options,
-		gitiles: service,
+		Node:      nodefs.NewDefaultNode(),
+		nodeCache: newNodeCache(),
+		cache:     c,
+		options:   options,
+		gitiles:   service,
 	}
 	return r
 }
@@ -122,6 +124,7 @@
 		log.Printf("NewManifestFS(%s): %v", string(content), err)
 		return nil, fuse.EIO
 	}
+	fs.(*manifestFSRoot).nodeCache = c.root.nodeCache
 
 	ch := c.root.Inode().NewChild(name, true, fs)
 	if ch == nil {
@@ -146,7 +149,3 @@
 
 	return config, fuse.OK
 }
-
-// TODO(hanwen): implement configNode.Unlink
-
-// TODO(hanwen): make sure content nodes are shared between workspaces.
diff --git a/fs/nodecache.go b/fs/nodecache.go
new file mode 100644
index 0000000..2b94e85
--- /dev/null
+++ b/fs/nodecache.go
@@ -0,0 +1,60 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fs
+
+import (
+	"sync"
+
+	"github.com/libgit2/git2go"
+)
+
+type nodeCacheKey struct {
+	ID   git.Oid
+	xbit bool
+}
+
+// The nodeCache keeps a map of ID to FS node. It is safe for
+// concurrent use from multiple goroutines. The cache allows us to
+// reuse out the same node for multiple files, effectively
+// hard-linking the file. This is done for two reasons: first, each
+// blob takes up kernel FS cache memory only once, even if it may be
+// used in multiple checkouts. Second, moving data from the FUSE
+// process into the kernel is relatively expensive. Thus, we can
+// amortize the cost of the read over multiple checkouts.
+type nodeCache struct {
+	mu      sync.RWMutex
+	nodeMap map[nodeCacheKey]*gitilesNode
+}
+
+func newNodeCache() *nodeCache {
+	return &nodeCache{
+		nodeMap: make(map[nodeCacheKey]*gitilesNode),
+	}
+}
+
+func (c *nodeCache) get(id *git.Oid, xbit bool) *gitilesNode {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	return c.nodeMap[nodeCacheKey{*id, xbit}]
+}
+
+func (c *nodeCache) add(n *gitilesNode) {
+	xbit := n.mode&0111 != 0
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.nodeMap[nodeCacheKey{n.id, xbit}] = n
+}