Skip to content

Commit

Permalink
Optimizations, file watcher feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Vilsol committed Nov 9, 2020
1 parent 56ff5ab commit c7d0191
Show file tree
Hide file tree
Showing 14 changed files with 529 additions and 324 deletions.
50 changes: 31 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ Flags:
--colors Force output with colors
--expiry Use cache expiry
--expiry-interval duration Interval between cache GC's (default 10m0s)
--expiry-memory int Max memory usage in MB (default 128)
--expiry-shards int Cache shard count (default 64)
--expiry-time duration Lifetime of a cache entry (default 1h0m0s)
-h, --help help for yeet
--host string Hostname to bind the webserver (default "0.0.0.0")
Expand All @@ -26,6 +24,7 @@ Flags:
--paths strings Paths to serve on the webserver (default [./www])
--port int Port to run the webserver on (default 8080)
--warmup Load all files into memory on startup
--watch Watch filesystem for changes
Use "yeet [command] --help" for more information about a command.
```
Expand All @@ -38,28 +37,41 @@ docker run -v /path/to/data:/www -p 8080:8080 ghcr.io/vilsol/yeet:latest

## Benchmarks (GOMAXPROCS=1)

### Without cache expiry
### Baseline

```
BenchmarkServerGet1ReqPerConn 8684911 1345 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn 11419969 1046 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn 15521960 763 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10KReqPerConn 17596045 690 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet1ReqPerConn10KClients 8395502 1386 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn10KClients 11061466 1053 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn10KClients 15229926 771 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet100ReqPerConn10KClients 16757032 699 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet1ReqPerConn 8512062 1379 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn 11406890 1057 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn 15189015 775 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10KReqPerConn 17068996 698 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet1ReqPerConn10KClients 8310056 1409 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn10KClients 10608926 1058 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn10KClients 15363962 773 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet100ReqPerConn10KClients 16854955 707 ns/op 0 B/op 0 allocs/op
```

### With cache expiry

```
BenchmarkServerGet1ReqPerConnExpiry 2419442 4780 ns/op 6176 B/op 2 allocs/op
BenchmarkServerGet2ReqPerConnExpiry 2912630 4074 ns/op 6176 B/op 2 allocs/op
BenchmarkServerGet10ReqPerConnExpiry 3479211 3468 ns/op 6176 B/op 2 allocs/op
BenchmarkServerGet10KReqPerConnExpiry 4027263 2952 ns/op 6176 B/op 2 allocs/op
BenchmarkServerGet1ReqPerConn10KClientsExpiry 1645986 7244 ns/op 6411 B/op 2 allocs/op
BenchmarkServerGet2ReqPerConn10KClientsExpiry 1818912 7026 ns/op 6177 B/op 2 allocs/op
BenchmarkServerGet10ReqPerConn10KClientsExpiry 1960964 6217 ns/op 6177 B/op 2 allocs/op
BenchmarkServerGet100ReqPerConn10KClientsExpiry 2766102 4114 ns/op 6223 B/op 2 allocs/op
BenchmarkServerGet1ReqPerConnExpiry 8677137 1375 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConnExpiry 11386528 1053 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConnExpiry 15480867 773 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10KReqPerConnExpiry 16949194 707 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet1ReqPerConn10KClientsExpiry 8515335 1388 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn10KClientsExpiry 11266317 1060 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn10KClientsExpiry 15184057 776 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet100ReqPerConn10KClientsExpiry 16339011 714 ns/op 0 B/op 0 allocs/op
```

### With file watching

```
BenchmarkServerGet1ReqPerConnWatch 8282121 1430 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConnWatch 11046026 1080 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConnWatch 15087688 791 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10KReqPerConnWatch 16619936 733 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet1ReqPerConn10KClientsWatch 8312827 1460 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn10KClientsWatch 10938738 1097 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn10KClientsWatch 14515534 811 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet100ReqPerConn10KClientsWatch 16010731 717 ns/op 0 B/op 0 allocs/op
```
16 changes: 16 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cache

import "time"

type CachedInstance struct {
RelativePath string
AbsolutePath string
Data []byte
ContentType string
LoadTime time.Time
}

type Cache interface {
Index() (int64, error)
Get(path []byte) (string, []byte)
}
94 changes: 94 additions & 0 deletions cache/readonly.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package cache

import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"time"
)

type readOnlyInstance struct {
Instance *CachedInstance
Get func(instance *readOnlyInstance) (string, []byte)
}

type readOnlyCache struct {
Cache
data map[string]*readOnlyInstance
}

func NewReadOnlyCache() (*readOnlyCache, error) {
if viper.GetBool("watch") {
return nil, errors.New("read only cache doesn't support file watching")
}

c := &readOnlyCache{
data: make(map[string]*readOnlyInstance),
}

if viper.GetBool("expiry") {
go func(c *readOnlyCache) {
ticker := time.NewTicker(viper.GetDuration("expiry.interval"))
defer ticker.Stop()
for range ticker.C {
cleanBefore := time.Now().Add(viper.GetDuration("expiry.time") * -1)
for key, instance := range c.data {
if instance.Instance.Data != nil && instance.Instance.LoadTime.Before(cleanBefore) {
instance.Get = loadReadOnly
instance.Instance.Data = nil
instance.Instance.ContentType = ""
log.Tracef("Evicted from cache: %s", key)
}
}
}
}(c)
}

return c, nil
}

func (c *readOnlyCache) Index() (int64, error) {
return index(func(absolutePath string, cleanedPath string) int64 {
instance := &readOnlyInstance{
Instance: &CachedInstance{
RelativePath: cleanedPath,
AbsolutePath: absolutePath,
},
Get: loadReadOnly,
}

c.data[cleanedPath] = instance

if viper.GetBool("warmup") {
if viper.GetBool("expiry") {
panic("expiry not supported if warmup is enabled")
}

instance.Get(instance)
return int64(len(instance.Instance.Data))
}

return 0
})
}

func (c *readOnlyCache) Get(path []byte) (string, []byte) {
if instance, ok := c.data[string(path)]; ok {
return instance.Get(instance)
}

return "", nil
}

func loadReadOnly(instance *readOnlyInstance) (string, []byte) {
fileType, data := load(instance.Instance)

instance.Instance.LoadTime = time.Now()
instance.Instance.Data = data
instance.Instance.ContentType = fileType
instance.Get = func(cache *readOnlyInstance) (string, []byte) {
return cache.Instance.ContentType, cache.Instance.Data
}

return fileType, data
}
154 changes: 154 additions & 0 deletions cache/readwrite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package cache

import (
"github.com/cornelk/hashmap"
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"path/filepath"
"strings"
"time"
)

type readWriteInstance struct {
Instance *CachedInstance
Get func(instance *readWriteInstance) (string, []byte)
}

type readWriteCache struct {
Cache
data *hashmap.HashMap
}

func NewReadWriteCache() (*readWriteCache, error) {
data := &hashmap.HashMap{}

c := &readWriteCache{
data: data,
}

if viper.GetBool("expiry") {
go func(c *readWriteCache) {
ticker := time.NewTicker(viper.GetDuration("expiry.interval"))
defer ticker.Stop()
for range ticker.C {
cleanBefore := time.Now().Add(viper.GetDuration("expiry.time") * -1)
for keyVal := range c.data.Iter() {
instance := keyVal.Value.(*readWriteInstance)
if instance.Instance.Data != nil && instance.Instance.LoadTime.Before(cleanBefore) {
instance.Get = loadReadWrite
instance.Instance.Data = nil
instance.Instance.ContentType = ""
log.Tracef("Evicted from cache: %s", keyVal.Key)
}
}
}
}(c)
}

return c, nil
}

func (c *readWriteCache) Index() (int64, error) {
indexFunc := func(absolutePath string, cleanedPath string) int64 {
instance := &readWriteInstance{
Instance: &CachedInstance{
RelativePath: cleanedPath,
AbsolutePath: absolutePath,
},
Get: loadReadWrite,
}

c.data.Set(cleanedPath, instance)

if viper.GetBool("warmup") {
if viper.GetBool("expiry") {
panic("expiry not supported if warmup is enabled")
}

instance.Get(instance)
return int64(len(instance.Instance.Data))
}

return 0
}

totalCount, err := index(indexFunc)

if viper.GetBool("watch") {
watcher, err := fsnotify.NewWatcher()

if err != nil {
return 0, err
}

go func(c *readWriteCache) {
for event := range watcher.Events {
dirPath := ""
for _, path := range viper.GetStringSlice("paths") {
if strings.HasPrefix(event.Name, filepath.Clean(path)) {
dirPath = path
}
}

if dirPath == "" {
log.Warnf("Received update about an unknown path: %s", event.Name)
continue
}

cleanPath := cleanPath(event.Name, dirPath)

switch {
case event.Op&fsnotify.Rename == fsnotify.Rename:
fallthrough
case event.Op&fsnotify.Remove == fsnotify.Remove:
c.data.Del(cleanPath)
log.Tracef("File removed from disk and cache: %s", cleanPath)
case event.Op&fsnotify.Create == fsnotify.Create:
absPath, _ := filepath.Abs(event.Name)
indexFunc(absPath, cleanPath)
log.Tracef("File created on disk and added to cache: %s", cleanPath)
case event.Op&fsnotify.Write == fsnotify.Write:
instance, ok := c.data.GetStringKey(cleanPath)
if ok {
instance.(*readWriteInstance).Get = loadReadWrite
instance.(*readWriteInstance).Instance.Data = nil
instance.(*readWriteInstance).Instance.ContentType = ""
log.Tracef("File updated on disk and evicted from cache: %s", cleanPath)
}
}
}
}(c)

for _, dirPath := range viper.GetStringSlice("paths") {
log.Debugf("Watching path: %s", dirPath)
err = watcher.Add(dirPath)
if err != nil {
return 0, err
}
}
}

return totalCount, err
}

func (c *readWriteCache) Get(path []byte) (string, []byte) {
if instance, ok := c.data.GetStringKey(string(path)); ok {
return instance.(*readWriteInstance).Get(instance.(*readWriteInstance))
}

return "", nil
}

func loadReadWrite(instance *readWriteInstance) (string, []byte) {
fileType, data := load(instance.Instance)

instance.Instance.LoadTime = time.Now()
instance.Instance.Data = data
instance.Instance.ContentType = fileType
instance.Get = func(cache *readWriteInstance) (string, []byte) {
return cache.Instance.ContentType, cache.Instance.Data
}

return fileType, data
}
Loading

0 comments on commit c7d0191

Please sign in to comment.