Skip to content

Commit 3e97366

Browse files
committed
Add cache for debug files
1 parent b5104a1 commit 3e97366

File tree

6 files changed

+199
-58
lines changed

6 files changed

+199
-58
lines changed

cmd/symbolization/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func main() {
2121
// Alternatively, use a local debug info file:
2222
//client := &localDebuginfodClient{debugFilePath: "/path/to/your/debug/file"}
2323

24-
s := symbolizer.NewSymbolizer(client)
24+
s := symbolizer.NewSymbolizer(client, nil)
2525
ctx := context.Background()
2626

2727
_, err := client.FetchDebuginfo(buildID)

pkg/experiment/query_backend/backend.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import (
44
"context"
55
"flag"
66
"fmt"
7-
87
"github.com/go-kit/log"
98
"github.com/grafana/dskit/grpcclient"
109
"github.com/grafana/dskit/services"
10+
"github.com/grafana/pyroscope/pkg/objstore/client"
1111
"github.com/opentracing/opentracing-go"
1212
"github.com/prometheus/client_golang/prometheus"
1313
"golang.org/x/sync/errgroup"
@@ -21,13 +21,14 @@ import (
2121
type Config struct {
2222
Address string `yaml:"address"`
2323
GRPCClientConfig grpcclient.Config `yaml:"grpc_client_config" doc:"description=Configures the gRPC client used to communicate between the query-frontends and the query-schedulers."`
24-
DebuginfodURL string `yaml:"debuginfod_url"`
24+
Symbolizer symbolizer.Config `yaml:"symbolizer"`
25+
DebugStorage client.Config `yaml:"debug_storage"`
2526
}
2627

2728
func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
2829
f.StringVar(&cfg.Address, "query-backend.address", "localhost:9095", "")
29-
f.StringVar(&cfg.DebuginfodURL, "query-backend.debuginfod-url", "https://debuginfod.elfutils.org", "URL of the debuginfod server")
3030
cfg.GRPCClientConfig.RegisterFlagsWithPrefix("query-backend.grpc-client-config", f)
31+
cfg.Symbolizer.RegisterFlagsWithPrefix("query-backend.symbolizer", f)
3132
}
3233

3334
func (cfg *Config) Validate() error {
@@ -63,10 +64,12 @@ func New(
6364
blockReader QueryHandler,
6465
) (*QueryBackend, error) {
6566
var sym *symbolizer.Symbolizer
66-
if config.DebuginfodURL != "" {
67-
sym = symbolizer.NewSymbolizer(
68-
symbolizer.NewDebuginfodClient(config.DebuginfodURL),
69-
)
67+
if config.Symbolizer.DebuginfodURL != "" {
68+
var err error
69+
sym, err = symbolizer.NewFromConfig(context.Background(), config.Symbolizer)
70+
if err != nil {
71+
return nil, fmt.Errorf("create symbolizer: %w", err)
72+
}
7073
}
7174

7275
q := QueryBackend{

pkg/experiment/symbolizer/cache.go

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package symbolizer
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"time"
8+
9+
"github.com/grafana/pyroscope/pkg/objstore"
10+
)
11+
12+
// CacheConfig holds configuration for the debug info cache
13+
type CacheConfig struct {
14+
Enabled bool `yaml:"enabled"`
15+
MaxAge time.Duration `yaml:"max_age"`
16+
}
17+
18+
func NewObjstoreCache(bucket objstore.Bucket, maxAge time.Duration) *ObjstoreCache {
19+
return &ObjstoreCache{
20+
bucket: bucket,
21+
maxAge: maxAge,
22+
}
23+
}
24+
25+
// DebugInfoCache handles caching of debug info files
26+
type DebugInfoCache interface {
27+
Get(ctx context.Context, buildID string) (io.ReadCloser, error)
28+
Put(ctx context.Context, buildID string, reader io.Reader) error
29+
}
30+
31+
// ObjstoreCache implements DebugInfoCache using S3 storage
32+
type ObjstoreCache struct {
33+
bucket objstore.Bucket
34+
maxAge time.Duration
35+
}
36+
37+
func (c *ObjstoreCache) Get(ctx context.Context, buildID string) (io.ReadCloser, error) {
38+
// First check if object exists to avoid unnecessary operations
39+
reader, err := c.bucket.Get(ctx, buildID)
40+
if err != nil {
41+
if c.bucket.IsObjNotFoundErr(err) {
42+
return nil, err
43+
}
44+
return nil, fmt.Errorf("get from cache: %w", err)
45+
}
46+
47+
// Get attributes - this should use the same HEAD request that Get used
48+
attrs, err := c.bucket.Attributes(ctx, buildID)
49+
if err != nil {
50+
reader.Close()
51+
return nil, fmt.Errorf("get cache attributes: %w", err)
52+
}
53+
54+
// Check if expired
55+
if time.Since(attrs.LastModified) > c.maxAge {
56+
reader.Close()
57+
// Async deletion to not block the request
58+
go func() {
59+
delCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
60+
defer cancel()
61+
_ = c.bucket.Delete(delCtx, buildID)
62+
}()
63+
return nil, fmt.Errorf("cached object expired")
64+
}
65+
66+
return reader, nil
67+
}
68+
69+
func (c *ObjstoreCache) Put(ctx context.Context, buildID string, reader io.Reader) error {
70+
if err := c.bucket.Upload(ctx, buildID, reader); err != nil {
71+
return fmt.Errorf("upload to cache: %w", err)
72+
}
73+
return nil
74+
}
75+
76+
// NullCache implements DebugInfoCache but performs no caching
77+
type NullCache struct{}
78+
79+
func NewNullCache() DebugInfoCache {
80+
return &NullCache{}
81+
}
82+
83+
func (n *NullCache) Get(ctx context.Context, buildID string) (io.ReadCloser, error) {
84+
// Always return cache miss
85+
return nil, fmt.Errorf("cache miss")
86+
}
87+
88+
func (n *NullCache) Put(ctx context.Context, buildID string, reader io.Reader) error {
89+
// Do nothing
90+
return nil
91+
}

pkg/experiment/symbolizer/symbolizer.go

+82-45
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import (
44
"context"
55
"debug/dwarf"
66
"debug/elf"
7+
"flag"
78
"fmt"
9+
"io"
10+
"os"
11+
"time"
12+
13+
objstoreclient "github.com/grafana/pyroscope/pkg/objstore/client"
814
)
915

1016
// DwarfResolver implements the liner interface
@@ -37,44 +43,106 @@ func (d *DwarfResolver) Close() error {
3743
return d.file.Close()
3844
}
3945

46+
type Config struct {
47+
DebuginfodURL string `yaml:"debuginfod_url"`
48+
Cache CacheConfig `yaml:"cache"`
49+
Storage objstoreclient.Config `yaml:"storage"`
50+
}
51+
4052
type Symbolizer struct {
4153
client DebuginfodClient
54+
cache DebugInfoCache
4255
}
4356

44-
func NewSymbolizer(client DebuginfodClient) *Symbolizer {
57+
func NewSymbolizer(client DebuginfodClient, cache DebugInfoCache) *Symbolizer {
58+
if cache == nil {
59+
cache = NewNullCache()
60+
}
4561
return &Symbolizer{
4662
client: client,
63+
cache: cache,
64+
}
65+
}
66+
67+
func NewFromConfig(ctx context.Context, cfg Config) (*Symbolizer, error) {
68+
client := NewDebuginfodClient(cfg.DebuginfodURL)
69+
70+
// Default to no caching
71+
var cache = NewNullCache()
72+
73+
if cfg.Cache.Enabled {
74+
if cfg.Storage.Backend == "" {
75+
return nil, fmt.Errorf("storage configuration required when cache is enabled")
76+
}
77+
bucket, err := objstoreclient.NewBucket(ctx, cfg.Storage, "debuginfo")
78+
if err != nil {
79+
return nil, fmt.Errorf("create debug info storage: %w", err)
80+
}
81+
cache = NewObjstoreCache(bucket, cfg.Cache.MaxAge)
4782
}
83+
84+
return &Symbolizer{
85+
client: client,
86+
cache: cache,
87+
}, nil
4888
}
4989

5090
func (s *Symbolizer) Symbolize(ctx context.Context, req Request) error {
51-
// Fetch debug info file
52-
debugFilePath, err := s.client.FetchDebuginfo(req.BuildID)
91+
debugReader, err := s.cache.Get(ctx, req.BuildID)
92+
if err == nil {
93+
defer debugReader.Close()
94+
return s.symbolizeFromReader(ctx, debugReader, req)
95+
}
96+
97+
// Cache miss - fetch from debuginfod
98+
filepath, err := s.client.FetchDebuginfo(req.BuildID)
5399
if err != nil {
54100
return fmt.Errorf("fetch debuginfo: %w", err)
55101
}
56102

57-
// Open ELF file
58-
f, err := elf.Open(debugFilePath)
103+
// Open for symbolization
104+
f, err := os.Open(filepath)
59105
if err != nil {
60-
return fmt.Errorf("open ELF file: %w", err)
106+
return fmt.Errorf("open debug file: %w", err)
61107
}
62108
defer f.Close()
63109

110+
// Cache it for future use
111+
if _, err := f.Seek(0, 0); err != nil {
112+
return fmt.Errorf("seek file: %w", err)
113+
}
114+
if err := s.cache.Put(ctx, req.BuildID, f); err != nil {
115+
// TODO: Log it but don't fail?
116+
}
117+
118+
// Seek back to start for symbolization
119+
if _, err := f.Seek(0, 0); err != nil {
120+
return fmt.Errorf("seek file: %w", err)
121+
}
122+
123+
return s.symbolizeFromReader(ctx, f, req)
124+
}
125+
126+
func (s *Symbolizer) symbolizeFromReader(ctx context.Context, r io.ReadCloser, req Request) error {
127+
elfFile, err := elf.NewFile(io.NewSectionReader(r.(io.ReaderAt), 0, 1<<63-1))
128+
if err != nil {
129+
return fmt.Errorf("create ELF file from reader: %w", err)
130+
}
131+
defer elfFile.Close()
132+
64133
// Get executable info for address normalization
65-
ei, err := ExecutableInfoFromELF(f)
134+
ei, err := ExecutableInfoFromELF(elfFile)
66135
if err != nil {
67136
return fmt.Errorf("executable info from ELF: %w", err)
68137
}
69138

70139
// Create liner
71-
liner, err := NewDwarfResolver(f)
140+
liner, err := NewDwarfResolver(elfFile)
72141
if err != nil {
73142
return fmt.Errorf("create liner: %w", err)
74143
}
75144
//defer liner.Close()
76145

77-
// Process each mapping's locations
78146
for _, mapping := range req.Mappings {
79147
for _, loc := range mapping.Locations {
80148
addr, err := MapRuntimeAddress(loc.Address, ei, Mapping{
@@ -92,48 +160,17 @@ func (s *Symbolizer) Symbolize(ctx context.Context, req Request) error {
92160
continue // Skip errors for individual addresses
93161
}
94162

95-
// Update the location directly
96163
loc.Lines = lines
97164
}
98165
}
99166

100167
return nil
101168
}
102169

103-
func (s *Symbolizer) SymbolizeAll(ctx context.Context, buildID string) error {
104-
// Reuse the existing debuginfo file
105-
debugFilePath, err := s.client.FetchDebuginfo(buildID)
106-
if err != nil {
107-
return fmt.Errorf("fetch debuginfo: %w", err)
108-
}
170+
func (cfg *Config) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
171+
f.StringVar(&cfg.DebuginfodURL, prefix+".debuginfod-url", "https://debuginfod.elfutils.org", "URL of the debuginfod server")
109172

110-
f, err := elf.Open(debugFilePath)
111-
if err != nil {
112-
return fmt.Errorf("open ELF file: %w", err)
113-
}
114-
defer f.Close()
115-
116-
debugData, err := f.DWARF()
117-
if err != nil {
118-
return fmt.Errorf("get DWARF data: %w", err)
119-
}
120-
121-
debugInfo := NewDWARFInfo(debugData)
122-
allSymbols := debugInfo.SymbolizeAllAddresses()
123-
124-
fmt.Println("\nSymbolizing all addresses in DWARF file:")
125-
fmt.Println("----------------------------------------")
126-
127-
for addr, lines := range allSymbols {
128-
fmt.Printf("\nAddress: 0x%x\n", addr)
129-
for _, line := range lines {
130-
fmt.Printf(" Function: %s\n", line.Function.Name)
131-
fmt.Printf(" File: %s\n", line.Function.Filename)
132-
fmt.Printf(" Line: %d\n", line.Line)
133-
fmt.Printf(" StartLine: %d\n", line.Function.StartLine)
134-
fmt.Println("----------------------------------------")
135-
}
136-
}
137-
138-
return nil
173+
cachePrefix := prefix + ".cache"
174+
f.BoolVar(&cfg.Cache.Enabled, cachePrefix+".enabled", false, "Enable debug info caching")
175+
f.DurationVar(&cfg.Cache.MaxAge, cachePrefix+".max-age", 7*24*time.Hour, "Maximum age of cached debug info")
139176
}

pkg/phlaredb/symdb/resolver_tree.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package symdb
22

33
import (
44
"context"
5+
"fmt"
56
"sync"
67

78
pprof "github.com/google/pprof/profile"
@@ -23,8 +24,9 @@ func buildTree(
2324
) (*model.Tree, error) {
2425
// Try debuginfod symbolization first
2526
if symbols != nil && symbols.Symbolizer != nil {
27+
//nolint:staticcheck
2628
if err := symbolizeLocations(ctx, symbols); err != nil {
27-
// TODO: Log error but continue? partial symbolization is better than none
29+
// TODO: Log/process error but continue? partial symbolization is better than none
2830
}
2931
}
3032

@@ -250,6 +252,8 @@ func minValue(nodes []Node, maxNodes int64) int64 {
250252
}
251253

252254
func symbolizeLocations(ctx context.Context, symbols *Symbols) error {
255+
var errs []error
256+
253257
type locToSymbolize struct {
254258
idx int32
255259
loc *schemav1.InMemoryLocation
@@ -259,11 +263,12 @@ func symbolizeLocations(ctx context.Context, symbols *Symbols) error {
259263

260264
// Find all locations needing symbolization
261265
for i, loc := range symbols.Locations {
266+
locCopy := loc
262267
if mapping := &symbols.Mappings[loc.MappingId]; symbols.needsDebuginfodSymbolization(&loc, mapping) {
263268
buildIDStr := symbols.Strings[mapping.BuildId]
264269
locsByBuildId[buildIDStr] = append(locsByBuildId[buildIDStr], locToSymbolize{
265270
idx: int32(i),
266-
loc: &loc,
271+
loc: &locCopy,
267272
mapping: mapping,
268273
})
269274
}
@@ -290,7 +295,7 @@ func symbolizeLocations(ctx context.Context, symbols *Symbols) error {
290295
}
291296

292297
if err := symbols.Symbolizer.Symbolize(ctx, req); err != nil {
293-
// TODO: log/process errors but continue with other build IDs
298+
errs = append(errs, fmt.Errorf("symbolize build ID %s: %w", buildID, err))
294299
continue
295300
}
296301

@@ -328,5 +333,10 @@ func symbolizeLocations(ctx context.Context, symbols *Symbols) error {
328333
}
329334
}
330335
}
336+
337+
if len(errs) > 0 {
338+
return fmt.Errorf("symbolization errors: %v", errs)
339+
}
340+
331341
return nil
332342
}

0 commit comments

Comments
 (0)