alot more changes
Build and Publish / Metadata (push) Successful in 8s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 36s

This commit is contained in:
mats committed 2026-09-21 18:59:13 +02:00
1 parent 23243ddbdc
commit 0ed2935f04
40 files changed
+2889 -115

No files matched your search

+93 -27
View File
@@ -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: