Skip to content

Commit

Permalink
feat: esm lru cache
Browse files Browse the repository at this point in the history
  • Loading branch information
shiroyk committed Dec 29, 2023
1 parent b73945c commit 670e05d
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 11 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ require (
)

replace (
github.com/dop251/goja => v0.0.0-20231212144616-08f562ee86d0
github.com/dop251/goja => github.com/mstoykov/goja v0.0.0-20231212144616-08f562ee86d0
github.com/shiroyk/cloudcat/plugin => ./plugin
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mstoykov/goja v0.0.0-20231115172654-7aaf816c3720 h1:jrBzw98yL3+cwfcqlyyKzquVTUNEHRFcxFwNpd7Bd9U=
github.com/mstoykov/goja v0.0.0-20231115172654-7aaf816c3720/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/mstoykov/goja v0.0.0-20231212144616-08f562ee86d0 h1:AcJZgDvroNJdSX/Ip5hN0P5xhatMwmJBbLHqn3jqjME=
github.com/mstoykov/goja v0.0.0-20231212144616-08f562ee86d0/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/ohler55/ojg v1.20.3 h1:Z+fnElsA/GbI5oiT726qJaG4Ca9q5l7UO68Qd0PtkD4=
github.com/ohler55/ojg v1.20.3/go.mod h1:uHcD1ErbErC27Zhb5Df2jUjbseLLcmOCo6oxSr3jZxo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
20 changes: 14 additions & 6 deletions parsers/js/esm.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,23 @@ import (
"github.com/dop251/goja"
"github.com/shiroyk/cloudcat"
"github.com/shiroyk/cloudcat/js"
"github.com/shiroyk/cloudcat/parsers/js/lru"
"github.com/shiroyk/cloudcat/plugin"
)

// ESMParser the js parser with es module
type ESMParser struct {
mu *sync.Mutex
cache map[uint64]goja.CyclicModuleRecord
cache *lru.Cache[uint64, goja.CyclicModuleRecord]
hash *maphash.Hash
load func() js.ModuleLoader
}

// NewESMParser returns a new ESMParser
func NewESMParser() *ESMParser {
func NewESMParser(maxCache int) *ESMParser {
return &ESMParser{
new(sync.Mutex),
make(map[uint64]goja.CyclicModuleRecord),
lru.New[uint64, goja.CyclicModuleRecord](maxCache),
new(maphash.Hash),
cloudcat.MustResolveLazy[js.ModuleLoader](),
}
Expand Down Expand Up @@ -65,7 +66,14 @@ func (p *ESMParser) GetElements(ctx *plugin.Context, content any, arg string) ([
func (p *ESMParser) ClearCache() {
p.mu.Lock()
defer p.mu.Unlock()
clear(p.cache)
p.cache.Clear()
}

// LenCache size the module cache
func (p *ESMParser) LenCache() int {
p.mu.Lock()
defer p.mu.Unlock()
return p.cache.Len()
}

func (p *ESMParser) run(ctx *plugin.Context, content any, script string) (any, error) {
Expand All @@ -77,14 +85,14 @@ func (p *ESMParser) run(ctx *plugin.Context, content any, script string) (any, e
hash := p.hash.Sum64()
p.hash.Reset()

mod, ok := p.cache[hash]
mod, ok := p.cache.Get(hash)
if !ok {
var err error
mod, err = goja.ParseModule("", script, p.load().ResolveModule)
if err != nil {
return nil, err
}
p.cache[hash] = mod
p.cache.Add(hash, mod)
}

result, err := js.RunModule(ctx, mod)
Expand Down
11 changes: 7 additions & 4 deletions parsers/js/esm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import (
"github.com/stretchr/testify/assert"
)

var esmParser = NewESMParser()
var esmParser = NewESMParser(1)

func TestESMCache(t *testing.T) {
_, err := esmParser.GetString(ctx, ``, `export default 1;`)
assert.NoError(t, err)
assert.Equal(t, 1, len(esmParser.cache))
assert.Equal(t, 1, esmParser.cache.Len())
_, err = esmParser.GetString(ctx, ``, `export default 1;`)
assert.NoError(t, err)
assert.Equal(t, 1, len(esmParser.cache))
assert.Equal(t, 1, esmParser.cache.Len())
_, err = esmParser.GetString(ctx, ``, `export default 2;`)
assert.NoError(t, err)
assert.Equal(t, 1, esmParser.cache.Len())
esmParser.ClearCache()
assert.Equal(t, 0, len(esmParser.cache))
assert.Equal(t, 0, esmParser.cache.Len())
}

func TestESMGetString(t *testing.T) {
Expand Down
130 changes: 130 additions & 0 deletions parsers/js/lru/lru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
Copyright 2013 Google Inc.
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 lru implements an LRU cache.
package lru

import "container/list"

// Cache is an LRU cache. It is not safe for concurrent access.
type Cache[K comparable, V any] struct {
// MaxEntries is the maximum number of cache entries before
// an item is evicted. Zero means no limit.
MaxEntries int

// OnEvicted optionally specifies a callback function to be
// executed when an entry is purged from the cache.
OnEvicted func(key K, value V)

ll *list.List
cache map[K]*list.Element
}

type entry[K comparable, V any] struct {
key K
value V
}

// New creates a new Cache.
// If maxEntries is zero, the cache has no limit and it's assumed
// that eviction is done by the caller.
func New[K comparable, V any](maxEntries int) *Cache[K, V] {
return &Cache[K, V]{
MaxEntries: maxEntries,
ll: list.New(),
cache: make(map[K]*list.Element),
}
}

// Add adds a value to the cache.
func (c *Cache[K, V]) Add(key K, value V) {
if c.cache == nil {
c.cache = make(map[K]*list.Element)
c.ll = list.New()
}
if ee, ok := c.cache[key]; ok {
c.ll.MoveToFront(ee)
ee.Value.(*entry[K, V]).value = value
return
}
ele := c.ll.PushFront(&entry[K, V]{key, value})
c.cache[key] = ele
if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries {
c.RemoveOldest()
}
}

// Get looks up a key's value from the cache.
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
if c.cache == nil {
return
}
if ele, hit := c.cache[key]; hit {
c.ll.MoveToFront(ele)
return ele.Value.(*entry[K, V]).value, true
}
return
}

// Remove removes the provided key from the cache.
func (c *Cache[K, V]) Remove(key K) {
if c.cache == nil {
return
}
if ele, hit := c.cache[key]; hit {
c.removeElement(ele)
}
}

// RemoveOldest removes the oldest item from the cache.
func (c *Cache[K, V]) RemoveOldest() {
if c.cache == nil {
return
}
ele := c.ll.Back()
if ele != nil {
c.removeElement(ele)
}
}

func (c *Cache[K, V]) removeElement(e *list.Element) {
c.ll.Remove(e)
kv := e.Value.(*entry[K, V])
delete(c.cache, kv.key)
if c.OnEvicted != nil {
c.OnEvicted(kv.key, kv.value)
}
}

// Len returns the number of items in the cache.
func (c *Cache[K, V]) Len() int {
if c.cache == nil {
return 0
}
return c.ll.Len()
}

// Clear purges all stored items from the cache.
func (c *Cache[K, V]) Clear() {
if c.OnEvicted != nil {
for _, e := range c.cache {
kv := e.Value.(*entry[K, V])
c.OnEvicted(kv.key, kv.value)
}
}
c.ll = nil
c.cache = nil
}
97 changes: 97 additions & 0 deletions parsers/js/lru/lru_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
Copyright 2013 Google Inc.
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 lru

import (
"fmt"
"testing"
)

type simpleStruct struct {
int
string
}

type complexStruct struct {
int
simpleStruct
}

var getTests = []struct {
name string
keyToAdd interface{}
keyToGet interface{}
expectedOk bool
}{
{"string_hit", "myKey", "myKey", true},
{"string_miss", "myKey", "nonsense", false},
{"simple_struct_hit", simpleStruct{1, "two"}, simpleStruct{1, "two"}, true},
{"simple_struct_miss", simpleStruct{1, "two"}, simpleStruct{0, "noway"}, false},
{"complex_struct_hit", complexStruct{1, simpleStruct{2, "three"}},
complexStruct{1, simpleStruct{2, "three"}}, true},
}

func TestGet(t *testing.T) {
for _, tt := range getTests {
lru := New(0)
lru.Add(tt.keyToAdd, 1234)
val, ok := lru.Get(tt.keyToGet)
if ok != tt.expectedOk {
t.Fatalf("%s: cache hit = %v; want %v", tt.name, ok, !ok)
} else if ok && val != 1234 {
t.Fatalf("%s expected get to return 1234 but got %v", tt.name, val)
}
}
}

func TestRemove(t *testing.T) {
lru := New(0)
lru.Add("myKey", 1234)
if val, ok := lru.Get("myKey"); !ok {
t.Fatal("TestRemove returned no match")
} else if val != 1234 {
t.Fatalf("TestRemove failed. Expected %d, got %v", 1234, val)
}

lru.Remove("myKey")
if _, ok := lru.Get("myKey"); ok {
t.Fatal("TestRemove returned a removed entry")
}
}

func TestEvict(t *testing.T) {
evictedKeys := make([]Key, 0)
onEvictedFun := func(key Key, value interface{}) {
evictedKeys = append(evictedKeys, key)
}

lru := New(20)
lru.OnEvicted = onEvictedFun
for i := 0; i < 22; i++ {
lru.Add(fmt.Sprintf("myKey%d", i), 1234)
}

if len(evictedKeys) != 2 {
t.Fatalf("got %d evicted keys; want 2", len(evictedKeys))
}
if evictedKeys[0] != Key("myKey0") {
t.Fatalf("got %v in first evicted key; want %s", evictedKeys[0], "myKey0")
}
if evictedKeys[1] != Key("myKey1") {
t.Fatalf("got %v in second evicted key; want %s", evictedKeys[1], "myKey1")
}
}

0 comments on commit 670e05d

Please sign in to comment.