Browse Source

Cache last commit to accelerate the repository directory page visit (#10069)

* Cache last commit to accelerate the repository directory page visit

* Default use default cache configuration

* add tests for last commit cache

* Simplify last commit cache

* Revert Enabled back

* Change the last commit cache default ttl to 8760h

* Fix test
tags/v1.12.0-dev
Lunny Xiao GitHub 5 years ago
parent
commit
10ccbbf57e
9 changed files with 260 additions and 22 deletions
  1. +7
    -0
      docs/content/doc/advanced/config-cheat-sheet.en-us.md
  2. +7
    -0
      docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
  3. +63
    -2
      integrations/repo_test.go
  4. +17
    -9
      modules/cache/cache.go
  5. +64
    -0
      modules/cache/last_commit.go
  6. +4
    -2
      modules/git/cache.go
  7. +44
    -1
      modules/git/commit_info.go
  8. +47
    -7
      modules/setting/cache.go
  9. +7
    -1
      routers/repo/view.go

+ 7
- 0
docs/content/doc/advanced/config-cheat-sheet.en-us.md View File

@@ -383,6 +383,7 @@ relation to port exhaustion.


## Cache (`cache`) ## Cache (`cache`)


- `ENABLED`: **true**: Enable the cache.
- `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, or `memcache`. - `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, or `memcache`.
- `INTERVAL`: **60**: Garbage Collection interval (sec), for memory cache only. - `INTERVAL`: **60**: Garbage Collection interval (sec), for memory cache only.
- `HOST`: **\<empty\>**: Connection string for `redis` and `memcache`. - `HOST`: **\<empty\>**: Connection string for `redis` and `memcache`.
@@ -390,6 +391,12 @@ relation to port exhaustion.
- Memcache: `127.0.0.1:9090;127.0.0.1:9091` - Memcache: `127.0.0.1:9090;127.0.0.1:9091`
- `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to 0 disables caching. - `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to 0 disables caching.


## Cache - LastCommitCache settings (`cache.last_commit`)

- `ENABLED`: **true**: Enable the cache.
- `ITEM_TTL`: **8760h**: Time to keep items in cache if not used, Setting it to 0 disables caching.
- `COMMITS_COUNT`: **1000**: Only enable the cache when repository's commits count great than.

## Session (`session`) ## Session (`session`)


- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, mysql, couchbase, memcache, nodb, postgres\]. - `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, mysql, couchbase, memcache, nodb, postgres\].


+ 7
- 0
docs/content/doc/advanced/config-cheat-sheet.zh-cn.md View File

@@ -148,6 +148,7 @@ menu:


## Cache (`cache`) ## Cache (`cache`)


- `ENABLED`: **true**: 是否启用。
- `ADAPTER`: **memory**: 缓存引擎,可以为 `memory`, `redis` 或 `memcache`。 - `ADAPTER`: **memory**: 缓存引擎,可以为 `memory`, `redis` 或 `memcache`。
- `INTERVAL`: **60**: 只对内存缓存有效,GC间隔,单位秒。 - `INTERVAL`: **60**: 只对内存缓存有效,GC间隔,单位秒。
- `HOST`: **\<empty\>**: 针对redis和memcache有效,主机地址和端口。 - `HOST`: **\<empty\>**: 针对redis和memcache有效,主机地址和端口。
@@ -155,6 +156,12 @@ menu:
- Memache: `127.0.0.1:9090;127.0.0.1:9091` - Memache: `127.0.0.1:9090;127.0.0.1:9091`
- `ITEM_TTL`: **16h**: 缓存项目失效时间,设置为 0 则禁用缓存。 - `ITEM_TTL`: **16h**: 缓存项目失效时间,设置为 0 则禁用缓存。


## Cache - LastCommitCache settings (`cache.last_commit`)

- `ENABLED`: **true**: 是否启用。
- `ITEM_TTL`: **8760h**: 缓存项目失效时间,设置为 0 则禁用缓存。
- `COMMITS_COUNT`: **1000**: 仅当仓库的提交数大于时才启用缓存。

## Session (`session`) ## Session (`session`)


- `PROVIDER`: Session 内容存储方式,可选 `memory`, `file`, `redis` 或 `mysql`。 - `PROVIDER`: Session 内容存储方式,可选 `memory`, `file`, `redis` 或 `mysql`。


+ 63
- 2
integrations/repo_test.go View File

@@ -7,8 +7,10 @@ package integrations
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path"
"strings" "strings"
"testing" "testing"
"time"


"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"


@@ -29,12 +31,71 @@ func TestViewRepo(t *testing.T) {
session.MakeRequest(t, req, http.StatusNotFound) session.MakeRequest(t, req, http.StatusNotFound)
} }


func TestViewRepo2(t *testing.T) {
func testViewRepo(t *testing.T) {
defer prepareTestEnv(t)() defer prepareTestEnv(t)()


req := NewRequest(t, "GET", "/user3/repo3") req := NewRequest(t, "GET", "/user3/repo3")
session := loginUser(t, "user2") session := loginUser(t, "user2")
session.MakeRequest(t, req, http.StatusOK)
resp := session.MakeRequest(t, req, http.StatusOK)

htmlDoc := NewHTMLParser(t, resp.Body)
files := htmlDoc.doc.Find("#repo-files-table > TBODY > TR")

type file struct {
fileName string
commitID string
commitMsg string
commitTime string
}

var items []file

files.Each(func(i int, s *goquery.Selection) {
tds := s.Find("td")
var f file
tds.Each(func(i int, s *goquery.Selection) {
if i == 0 {
f.fileName = strings.TrimSpace(s.Text())
} else if i == 1 {
a := s.Find("a")
f.commitMsg = strings.TrimSpace(a.Text())
l, _ := a.Attr("href")
f.commitID = path.Base(l)
}
})

f.commitTime, _ = s.Find("span.time-since").Attr("title")
items = append(items, f)
})

assert.EqualValues(t, []file{
{
fileName: "doc",
commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6",
commitMsg: "init project",
commitTime: time.Date(2017, time.June, 14, 13, 54, 21, 0, time.UTC).Format(time.RFC1123),
},
{
fileName: "README.md",
commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6",
commitMsg: "init project",
commitTime: time.Date(2017, time.June, 14, 13, 54, 21, 0, time.UTC).Format(time.RFC1123),
},
}, items)
}

func TestViewRepo2(t *testing.T) {
// no last commit cache
testViewRepo(t)

// enable last commit cache for all repositories
oldCommitsCount := setting.CacheService.LastCommit.CommitsCount
setting.CacheService.LastCommit.CommitsCount = 0
// first view will not hit the cache
testViewRepo(t)
// second view will hit the cache
testViewRepo(t)
setting.CacheService.LastCommit.CommitsCount = oldCommitsCount
} }


func TestViewRepo3(t *testing.T) { func TestViewRepo3(t *testing.T) {


+ 17
- 9
modules/cache/cache.go View File

@@ -16,20 +16,28 @@ import (
_ "gitea.com/macaron/cache/redis" _ "gitea.com/macaron/cache/redis"
) )


var conn mc.Cache
var (
conn mc.Cache
)

func newCache(cacheConfig setting.Cache) (mc.Cache, error) {
return mc.NewCacher(cacheConfig.Adapter, mc.Options{
Adapter: cacheConfig.Adapter,
AdapterConfig: cacheConfig.Conn,
Interval: cacheConfig.Interval,
})
}


// NewContext start cache service // NewContext start cache service
func NewContext() error { func NewContext() error {
if setting.CacheService == nil || conn != nil {
return nil
var err error

if conn == nil && setting.CacheService.Enabled {
if conn, err = newCache(setting.CacheService.Cache); err != nil {
return err
}
} }


var err error
conn, err = mc.NewCacher(setting.CacheService.Adapter, mc.Options{
Adapter: setting.CacheService.Adapter,
AdapterConfig: setting.CacheService.Conn,
Interval: setting.CacheService.Interval,
})
return err return err
} }




+ 64
- 0
modules/cache/last_commit.go View File

@@ -0,0 +1,64 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package cache

import (
"fmt"

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"

mc "gitea.com/macaron/cache"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)

// LastCommitCache represents a cache to store last commit
type LastCommitCache struct {
repoPath string
ttl int64
repo *git.Repository
commitCache map[string]*object.Commit
mc.Cache
}

// NewLastCommitCache creates a new last commit cache for repo
func NewLastCommitCache(repoPath string, gitRepo *git.Repository, ttl int64) *LastCommitCache {
return &LastCommitCache{
repoPath: repoPath,
repo: gitRepo,
commitCache: make(map[string]*object.Commit),
ttl: ttl,
Cache: conn,
}
}

// Get get the last commit information by commit id and entry path
func (c LastCommitCache) Get(ref, entryPath string) (*object.Commit, error) {
v := c.Cache.Get(fmt.Sprintf("last_commit:%s:%s:%s", c.repoPath, ref, entryPath))
if vs, ok := v.(string); ok {
log.Trace("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
if commit, ok := c.commitCache[vs]; ok {
log.Trace("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs)
return commit, nil
}
id, err := c.repo.ConvertToSHA1(vs)
if err != nil {
return nil, err
}
commit, err := c.repo.GoGitRepo().CommitObject(id)
if err != nil {
return nil, err
}
c.commitCache[vs] = commit
return commit, nil
}
return nil, nil
}

// Put put the last commit id with commit and entry path
func (c LastCommitCache) Put(ref, entryPath, commitID string) error {
log.Trace("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
return c.Cache.Put(fmt.Sprintf("last_commit:%s:%s:%s", c.repoPath, ref, entryPath), commitID, c.ttl)
}

+ 4
- 2
modules/git/cache.go View File

@@ -4,8 +4,10 @@


package git package git


import "gopkg.in/src-d/go-git.v4/plumbing/object"

// LastCommitCache cache // LastCommitCache cache
type LastCommitCache interface { type LastCommitCache interface {
Get(repoPath, ref, entryPath string) (*Commit, error)
Put(repoPath, ref, entryPath string, commit *Commit) error
Get(ref, entryPath string) (*object.Commit, error)
Put(ref, entryPath, commitID string) error
} }

+ 44
- 1
modules/git/commit_info.go View File

@@ -5,6 +5,8 @@
package git package git


import ( import (
"path"

"github.com/emirpasic/gods/trees/binaryheap" "github.com/emirpasic/gods/trees/binaryheap"
"gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object" "gopkg.in/src-d/go-git.v4/plumbing/object"
@@ -30,7 +32,29 @@ func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCom
return nil, nil, err return nil, nil, err
} }


revs, err := getLastCommitForPaths(c, treePath, entryPaths)
var revs map[string]*object.Commit
if cache != nil {
var unHitPaths []string
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache)
if err != nil {
return nil, nil, err
}
if len(unHitPaths) > 0 {
revs2, err := getLastCommitForPaths(c, treePath, unHitPaths)
if err != nil {
return nil, nil, err
}

for k, v := range revs2 {
if err := cache.Put(commit.ID.String(), path.Join(treePath, k), v.ID().String()); err != nil {
return nil, nil, err
}
revs[k] = v
}
}
} else {
revs, err = getLastCommitForPaths(c, treePath, entryPaths)
}
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -127,6 +151,25 @@ func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[
return hashes, nil return hashes, nil
} }


func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache LastCommitCache) (map[string]*object.Commit, []string, error) {
var unHitEntryPaths []string
var results = make(map[string]*object.Commit)
for _, p := range paths {
lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
if err != nil {
return nil, nil, err
}
if lastCommit != nil {
results[p] = lastCommit
continue
}

unHitEntryPaths = append(unHitEntryPaths, p)
}

return results, unHitEntryPaths, nil
}

func getLastCommitForPaths(c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) { func getLastCommitForPaths(c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) {
// We do a tree traversal with nodes sorted by commit time // We do a tree traversal with nodes sorted by commit time
heap := binaryheap.NewWith(func(a, b interface{}) int { heap := binaryheap.NewWith(func(a, b interface{}) int {


+ 47
- 7
modules/setting/cache.go View File

@@ -13,31 +13,71 @@ import (


// Cache represents cache settings // Cache represents cache settings
type Cache struct { type Cache struct {
Enabled bool
Adapter string Adapter string
Interval int Interval int
Conn string Conn string
TTL time.Duration
TTL time.Duration `ini:"ITEM_TTL"`
} }


var ( var (
// CacheService the global cache // CacheService the global cache
CacheService *Cache
CacheService = struct {
Cache

LastCommit struct {
Enabled bool
TTL time.Duration `ini:"ITEM_TTL"`
CommitsCount int64
} `ini:"cache.last_commit"`
}{
Cache: Cache{
Enabled: true,
Adapter: "memory",
Interval: 60,
TTL: 16 * time.Hour,
},
LastCommit: struct {
Enabled bool
TTL time.Duration `ini:"ITEM_TTL"`
CommitsCount int64
}{
Enabled: true,
TTL: 8760 * time.Hour,
CommitsCount: 1000,
},
}
) )


func newCacheService() { func newCacheService() {
sec := Cfg.Section("cache") sec := Cfg.Section("cache")
CacheService = &Cache{
Adapter: sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache"}),
if err := sec.MapTo(&CacheService); err != nil {
log.Fatal("Failed to map Cache settings: %v", err)
} }

CacheService.Adapter = sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache"})
switch CacheService.Adapter { switch CacheService.Adapter {
case "memory": case "memory":
CacheService.Interval = sec.Key("INTERVAL").MustInt(60)
case "redis", "memcache": case "redis", "memcache":
CacheService.Conn = strings.Trim(sec.Key("HOST").String(), "\" ") CacheService.Conn = strings.Trim(sec.Key("HOST").String(), "\" ")
case "": // disable cache
CacheService.Enabled = false
default: default:
log.Fatal("Unknown cache adapter: %s", CacheService.Adapter) log.Fatal("Unknown cache adapter: %s", CacheService.Adapter)
} }
CacheService.TTL = sec.Key("ITEM_TTL").MustDuration(16 * time.Hour)


log.Info("Cache Service Enabled")
if CacheService.Enabled {
log.Info("Cache Service Enabled")
}

sec = Cfg.Section("cache.last_commit")
if !CacheService.Enabled {
CacheService.LastCommit.Enabled = false
}

CacheService.LastCommit.CommitsCount = sec.Key("COMMITS_COUNT").MustInt64(1000)

if CacheService.LastCommit.Enabled {
log.Info("Last Commit Cache Service Enabled")
}
} }

+ 7
- 1
routers/repo/view.go View File

@@ -17,6 +17,7 @@ import (


"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@@ -49,8 +50,13 @@ func renderDirectory(ctx *context.Context, treeLink string) {
} }
entries.CustomSort(base.NaturalSortLess) entries.CustomSort(base.NaturalSortLess)


var c git.LastCommitCache
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
c = cache.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, int64(setting.CacheService.LastCommit.TTL.Seconds()))
}

var latestCommit *git.Commit var latestCommit *git.Commit
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(ctx.Repo.Commit, ctx.Repo.TreePath, nil)
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(ctx.Repo.Commit, ctx.Repo.TreePath, c)
if err != nil { if err != nil {
ctx.ServerError("GetCommitsInfo", err) ctx.ServerError("GetCommitsInfo", err)
return return


Loading…
Cancel
Save