alot more changes
This commit is contained in:
1 parent
23243ddbdc
commit
0ed2935f04
40 files changed
+2889
-115
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package sourcemaps
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -17,10 +18,30 @@ import (
|
||||
|
||||
var frameRE = regexp.MustCompile(`(.*?)(https?://[^ )]+|[^ )]+):(\d+):(\d+)(\)?\s*)$`)
|
||||
|
||||
// defaultCacheBytes bounds the on-disk size of the maps kept parsed in memory.
|
||||
// A parsed map costs several times its file size, so this is a heap budget of
|
||||
// roughly 4-5x. Override with OTEL_SOURCEMAP_CACHE_MB.
|
||||
const defaultCacheBytes = 64 << 20
|
||||
|
||||
// Resolver indexes uploaded maps by file name at startup and parses them lazily
|
||||
// on first use, keeping the most recently used ones within a size budget. A
|
||||
// release ships hundreds of chunks, so parsing them all up front does not fit
|
||||
// in memory.
|
||||
type Resolver struct {
|
||||
dir string
|
||||
mu sync.RWMutex
|
||||
maps map[string]*sm.Consumer
|
||||
dir string
|
||||
|
||||
mu sync.Mutex
|
||||
index map[string]string // key(release, source) -> file name
|
||||
cache map[string]*list.Element
|
||||
lru *list.List // front = most recently used; values are *cacheEntry
|
||||
cacheBytes int64
|
||||
maxBytes int64
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
key string
|
||||
consumer *sm.Consumer
|
||||
size int64
|
||||
}
|
||||
|
||||
func New(dir string) (*Resolver, error) {
|
||||
@@ -30,8 +51,18 @@ func New(dir string) (*Resolver, error) {
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := &Resolver{dir: dir, maps: make(map[string]*sm.Consumer)}
|
||||
r.load()
|
||||
maxBytes := int64(defaultCacheBytes)
|
||||
if mb, err := strconv.Atoi(os.Getenv("OTEL_SOURCEMAP_CACHE_MB")); err == nil && mb > 0 {
|
||||
maxBytes = int64(mb) << 20
|
||||
}
|
||||
r := &Resolver{
|
||||
dir: dir,
|
||||
index: make(map[string]string),
|
||||
cache: make(map[string]*list.Element),
|
||||
lru: list.New(),
|
||||
maxBytes: maxBytes,
|
||||
}
|
||||
r.scan()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@@ -55,8 +86,9 @@ func parseFileName(name string) (release, source string, ok bool) {
|
||||
return string(rel), string(src), true
|
||||
}
|
||||
|
||||
// load restores previously uploaded maps; unreadable or legacy-named files are skipped.
|
||||
func (r *Resolver) load() {
|
||||
// scan indexes previously uploaded maps by file name without reading them;
|
||||
// legacy-named files are skipped.
|
||||
func (r *Resolver) scan() {
|
||||
entries, err := os.ReadDir(r.dir)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -66,15 +98,51 @@ func (r *Resolver) load() {
|
||||
if !ok || e.IsDir() {
|
||||
continue
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(r.dir, e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
consumer, err := sm.Parse(source+".map", b)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
r.maps[key(release, source)] = consumer
|
||||
r.index[key(release, source)] = e.Name()
|
||||
}
|
||||
}
|
||||
|
||||
// get returns the parsed map for k, loading it from disk on a cache miss. It
|
||||
// returns nil when no map is indexed or the file is unreadable.
|
||||
func (r *Resolver) get(k string) *sm.Consumer {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if el, ok := r.cache[k]; ok {
|
||||
r.lru.MoveToFront(el)
|
||||
return el.Value.(*cacheEntry).consumer
|
||||
}
|
||||
name, ok := r.index[k]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(r.dir, name))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, source, _ := parseFileName(name)
|
||||
consumer, err := sm.Parse(source+".map", b)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
r.put(k, consumer, int64(len(b)))
|
||||
return consumer
|
||||
}
|
||||
|
||||
// put caches a parsed map and evicts least recently used ones over budget,
|
||||
// always keeping the newest entry. Callers hold r.mu.
|
||||
func (r *Resolver) put(k string, consumer *sm.Consumer, size int64) {
|
||||
if el, ok := r.cache[k]; ok {
|
||||
r.cacheBytes -= el.Value.(*cacheEntry).size
|
||||
r.lru.Remove(el)
|
||||
}
|
||||
r.cache[k] = r.lru.PushFront(&cacheEntry{key: k, consumer: consumer, size: size})
|
||||
r.cacheBytes += size
|
||||
for r.cacheBytes > r.maxBytes && r.lru.Len() > 1 {
|
||||
last := r.lru.Back()
|
||||
e := last.Value.(*cacheEntry)
|
||||
r.lru.Remove(last)
|
||||
delete(r.cache, e.key)
|
||||
r.cacheBytes -= e.size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +154,10 @@ type Entry struct {
|
||||
|
||||
// List returns every loaded map, sorted by release then source.
|
||||
func (r *Resolver) List() []Entry {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
out := make([]Entry, 0, len(r.maps))
|
||||
for k := range r.maps {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := make([]Entry, 0, len(r.index))
|
||||
for k := range r.index {
|
||||
release, source, _ := strings.Cut(k, "\x00")
|
||||
out = append(out, Entry{Release: release, Source: source})
|
||||
}
|
||||
@@ -128,8 +196,10 @@ func (r *Resolver) Upload(release, source string, body io.Reader) error {
|
||||
if err := os.WriteFile(name, b, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
k := key(release, source)
|
||||
r.mu.Lock()
|
||||
r.maps[key(release, source)] = consumer
|
||||
r.index[k] = fileName(release, source)
|
||||
r.put(k, consumer, int64(len(b)))
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -149,8 +219,6 @@ func (r *Resolver) ResolveStack(release, stack string) string {
|
||||
if release == "" || stack == "" {
|
||||
return stack
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
lines := strings.Split(stack, "\n")
|
||||
for i, line := range lines {
|
||||
m := frameRE.FindStringSubmatch(line)
|
||||
@@ -159,7 +227,7 @@ func (r *Resolver) ResolveStack(release, stack string) string {
|
||||
}
|
||||
genLine, _ := strconv.Atoi(m[3])
|
||||
genCol, _ := strconv.Atoi(m[4])
|
||||
consumer := r.maps[key(release, m[2])]
|
||||
consumer := r.get(key(release, m[2]))
|
||||
if consumer == nil {
|
||||
continue
|
||||
}
|
||||
@@ -202,8 +270,6 @@ const frameContext = 6
|
||||
// uploaded source maps, including surrounding original source when the map
|
||||
// embeds sourcesContent.
|
||||
func (r *Resolver) ResolveFrames(release, stack string) []Frame {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
var frames []Frame
|
||||
for _, line := range strings.Split(stack, "\n") {
|
||||
m := frameRE.FindStringSubmatch(line)
|
||||
@@ -214,7 +280,7 @@ func (r *Resolver) ResolveFrames(release, stack string) []Frame {
|
||||
col, _ := strconv.Atoi(m[4])
|
||||
fn := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(m[1]), "at "), "("))
|
||||
f := Frame{Raw: strings.TrimSpace(line), Function: fn, File: m[2], Line: ln, Column: col, Status: "mapped"}
|
||||
switch consumer := r.maps[key(release, m[2])]; {
|
||||
switch consumer := r.get(key(release, m[2])); {
|
||||
case release == "":
|
||||
f.Status = "no-release"
|
||||
case consumer == nil:
|
||||
|
||||
@@ -22,3 +22,29 @@ func TestReloadFromDisk(t *testing.T) {
|
||||
t.Fatalf("frames = %+v", frames)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLazyLoadAndEviction(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
r, _ := New(dir)
|
||||
for _, f := range []string{"a", "b", "c"} {
|
||||
if err := r.Upload("rel", "/assets/"+f+".js", strings.NewReader(testMap)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
r2, _ := New(dir)
|
||||
if len(r2.cache) != 0 || len(r2.List()) != 3 {
|
||||
t.Fatalf("startup should index without parsing: cached=%d listed=%d", len(r2.cache), len(r2.List()))
|
||||
}
|
||||
r2.maxBytes = int64(len(testMap)) * 2 // room for two parsed maps
|
||||
for _, f := range []string{"a", "b", "c"} {
|
||||
if fr := r2.ResolveFrames("rel", "at f (https://h/assets/"+f+".js:2:1)"); len(fr) != 1 || fr[0].Status != "mapped" {
|
||||
t.Fatalf("%s not resolved: %+v", f, fr)
|
||||
}
|
||||
}
|
||||
if len(r2.cache) != 2 {
|
||||
t.Fatalf("cache should hold 2 entries, has %d", len(r2.cache))
|
||||
}
|
||||
if r2.get(key("rel", "/assets/a.js")) == nil { // evicted, reloads from disk
|
||||
t.Fatal("evicted map did not reload")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user