Skip to content

Commit 7c2ab09

Browse files
committed
Add multi-layered caching with LRU for symbols, Ristretto for debuginfo, and optimized DWARF parsing
1 parent 9f7b257 commit 7c2ab09

8 files changed

Lines changed: 478 additions & 95 deletions

File tree

.mockery.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,6 @@ packages:
5757
interfaces:
5858
Exporter:
5959
Ruler:
60+
github.com/grafana/pyroscope/pkg/experiment/symbolizer:
61+
interfaces:
62+
DebuginfodClient:

cmd/symbolization/main.go

Lines changed: 1 addition & 1 deletion
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.NewProfileSymbolizer(client, nil, symbolizer.NewMetrics(nil))
24+
s := symbolizer.NewProfileSymbolizer(client, nil, symbolizer.NewMetrics(nil), 0)
2525
ctx := context.Background()
2626

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

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/cespare/xxhash/v2 v2.3.0
1212
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381
1313
github.com/dennwc/varint v1.0.0
14+
github.com/dgraph-io/ristretto v0.2.0
1415
github.com/dgryski/go-groupvarint v0.0.0-20230630160417-2bfb7969fb3c
1516
github.com/dolthub/swiss v0.2.1
1617
github.com/drone/envsubst v1.0.3

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
197197
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
198198
github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
199199
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
200+
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
201+
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
202+
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
203+
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
200204
github.com/dgryski/go-groupvarint v0.0.0-20230630160417-2bfb7969fb3c h1:cHaw4wmusVzAZLEPWOCCGCfu6UvFXx9UboCHQCnjvxY=
201205
github.com/dgryski/go-groupvarint v0.0.0-20230630160417-2bfb7969fb3c/go.mod h1:MlkUQveSLEDbIgq2r1e++tSf0zfzU9mQpa9Qkczl+9Y=
202206
github.com/digitalocean/godo v1.109.0 h1:4W97RJLJSUQ3veRZDNbp1Ol3Rbn6Lmt9bKGvfqYI5SU=

pkg/experiment/symbolizer/debuginfod_client.go

Lines changed: 106 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
package symbolizer
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
78
"io"
89
"net/http"
910
"net/url"
1011
"os"
11-
"path/filepath"
1212
"regexp"
1313
"strings"
14+
"sync"
1415
"time"
16+
17+
"github.com/dgraph-io/ristretto"
1518
)
1619

1720
type DebuginfodClient interface {
18-
FetchDebuginfo(ctx context.Context, buildID string) (string, error)
21+
FetchDebuginfo(ctx context.Context, buildID string) (io.ReadCloser, error)
1922
}
2023

2124
type debuginfodClientConfig struct {
@@ -25,10 +28,23 @@ type debuginfodClientConfig struct {
2528
// userAgent string
2629
httpClient *http.Client
2730
}
28-
2931
type debuginfodClient struct {
3032
cfg debuginfodClientConfig
3133
metrics *Metrics
34+
35+
// In-memory cache of build IDs to file paths
36+
cache *ristretto.Cache
37+
38+
// Track in-flight requests to prevent duplicate fetches
39+
inFlightRequests map[string]*inFlightRequest
40+
inFlightRequestsMutex sync.Mutex
41+
}
42+
43+
// inFlightRequest represents an ongoing fetch operation
44+
type inFlightRequest struct {
45+
done chan struct{}
46+
data []byte
47+
err error
3248
}
3349

3450
func NewDebuginfodClient(baseURL string, metrics *Metrics) DebuginfodClient {
@@ -38,6 +54,16 @@ func NewDebuginfodClient(baseURL string, metrics *Metrics) DebuginfodClient {
3854
TLSHandshakeTimeout: 10 * time.Second,
3955
}
4056

57+
cache, err := ristretto.NewCache(&ristretto.Config{
58+
NumCounters: 1e7, // number of keys to track frequency of (10M)
59+
MaxCost: 2 << 30, // maximum cost of cache (2GB)
60+
BufferItems: 64, // number of keys per Get buffer
61+
})
62+
63+
if err != nil {
64+
cache = nil
65+
}
66+
4167
return &debuginfodClient{
4268
cfg: debuginfodClientConfig{
4369
baseURL: baseURL,
@@ -55,12 +81,14 @@ func NewDebuginfodClient(baseURL string, metrics *Metrics) DebuginfodClient {
5581
},
5682
},
5783
},
58-
metrics: metrics,
84+
metrics: metrics,
85+
cache: cache,
86+
inFlightRequests: make(map[string]*inFlightRequest),
5987
}
6088
}
6189

6290
// FetchDebuginfo fetches the debuginfo file for a specific build ID.
63-
func (c *debuginfodClient) FetchDebuginfo(ctx context.Context, buildID string) (string, error) {
91+
func (c *debuginfodClient) FetchDebuginfo(ctx context.Context, buildID string) (io.ReadCloser, error) {
6492
start := time.Now()
6593
var lastErr error
6694
c.metrics.debuginfodRequestsTotal.Inc()
@@ -76,23 +104,67 @@ func (c *debuginfodClient) FetchDebuginfo(ctx context.Context, buildID string) (
76104
sanitizedBuildID, err := sanitizeBuildID(buildID)
77105
if err != nil {
78106
c.metrics.debuginfodRequestErrorsTotal.WithLabelValues("invalid_id").Inc()
79-
return "", err
107+
return nil, err
108+
}
109+
110+
// Check in-memory cache first
111+
if c.cache != nil {
112+
if data, found := c.cache.Get(sanitizedBuildID); found {
113+
return io.NopCloser(bytes.NewReader(data.([]byte))), nil
114+
}
80115
}
81116

117+
// Check if there's already a request in flight for this build ID
118+
c.inFlightRequestsMutex.Lock()
119+
req, inFlight := c.inFlightRequests[sanitizedBuildID]
120+
if inFlight {
121+
// There's already a request in flight, wait for it to complete
122+
done := req.done
123+
c.inFlightRequestsMutex.Unlock()
124+
125+
select {
126+
case <-done:
127+
if req.err != nil {
128+
lastErr = req.err
129+
return nil, req.err
130+
}
131+
return io.NopCloser(bytes.NewReader(req.data)), nil
132+
case <-ctx.Done():
133+
lastErr = ctx.Err()
134+
return nil, ctx.Err()
135+
}
136+
}
137+
138+
// Create a new in-flight request
139+
req = &inFlightRequest{
140+
done: make(chan struct{}),
141+
}
142+
c.inFlightRequests[sanitizedBuildID] = req
143+
c.inFlightRequestsMutex.Unlock()
144+
145+
// Ensure we clean up the in-flight request when we're done
146+
defer func() {
147+
c.inFlightRequestsMutex.Lock()
148+
delete(c.inFlightRequests, sanitizedBuildID)
149+
close(req.done)
150+
c.inFlightRequestsMutex.Unlock()
151+
}()
152+
82153
url := fmt.Sprintf("%s/buildid/%s/debuginfo", c.cfg.baseURL, sanitizedBuildID)
154+
var data []byte
83155

84156
// Implement retries with exponential backoff
85157
for attempt := 0; attempt < c.cfg.maxRetries; attempt++ {
86158
if attempt > 0 {
87159
if ctx.Err() != nil {
88-
return "", ctx.Err()
160+
return nil, ctx.Err()
89161
}
90162
time.Sleep(c.cfg.backoffTime * time.Duration(attempt))
91163
}
92164

93-
filePath, err := c.doRequest(ctx, url, sanitizedBuildID)
165+
data, err = c.doRequest(ctx, url)
94166
if err == nil {
95-
return filePath, nil
167+
break
96168
}
97169

98170
lastErr = err
@@ -101,50 +173,52 @@ func (c *debuginfodClient) FetchDebuginfo(ctx context.Context, buildID string) (
101173
}
102174
}
103175

104-
return "", fmt.Errorf("failed to fetch debuginfo after %d attempts: %w", c.cfg.maxRetries, lastErr)
176+
if lastErr != nil {
177+
req.err = fmt.Errorf("failed to fetch debuginfo after %d attempts: %w", c.cfg.maxRetries, lastErr)
178+
return nil, req.err
179+
}
180+
181+
// Store in cache
182+
if c.cache != nil {
183+
// The cost is the size of the data in bytes
184+
c.cache.Set(sanitizedBuildID, data, int64(len(data)))
185+
}
186+
187+
req.data = data
188+
return io.NopCloser(bytes.NewReader(data)), nil
105189
}
106190

107-
func (c *debuginfodClient) doRequest(ctx context.Context, url, buildID string) (string, error) {
191+
func (c *debuginfodClient) doRequest(ctx context.Context, url string) ([]byte, error) {
108192
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
109193
if err != nil {
110-
return "", fmt.Errorf("failed to create request: %w", err)
194+
return nil, fmt.Errorf("failed to create request: %w", err)
111195
}
112196

113197
req.Header.Set("Accept-Encoding", "gzip, deflate")
114198

115199
resp, err := c.cfg.httpClient.Do(req)
116200
if err != nil {
117-
return "", fmt.Errorf("failed to execute request: %w", err)
201+
return nil, fmt.Errorf("failed to execute request: %w", err)
118202
}
119203
defer resp.Body.Close()
120204

121205
if resp.StatusCode != http.StatusOK {
122-
return "", fmt.Errorf("unexpected HTTP status: %d %s", resp.StatusCode, resp.Status)
123-
}
124-
125-
return c.saveDebugInfo(resp.Body, buildID, resp.ContentLength)
126-
}
127-
128-
func (c *debuginfodClient) saveDebugInfo(body io.Reader, buildID string, contentLength int64) (string, error) {
129-
if contentLength > 0 {
130-
c.metrics.debuginfodFileSize.Observe(float64(contentLength))
206+
return nil, fmt.Errorf("unexpected HTTP status: %d %s", resp.StatusCode, resp.Status)
131207
}
132208

133-
tempDir := os.TempDir()
134-
filePath := filepath.Join(tempDir, fmt.Sprintf("%s.elf", buildID))
135-
136-
outFile, err := os.Create(filePath)
209+
// Read the entire response body into memory
210+
data, err := io.ReadAll(resp.Body)
137211
if err != nil {
138-
return "", fmt.Errorf("failed to create temp file: %w", err)
212+
return nil, fmt.Errorf("failed to read response body: %w", err)
139213
}
140-
defer outFile.Close()
141214

142-
if _, err := io.Copy(outFile, body); err != nil {
143-
os.Remove(filePath) // Clean up on error
144-
return "", fmt.Errorf("failed to write debug info: %w", err)
215+
if resp.ContentLength > 0 {
216+
c.metrics.debuginfodFileSize.Observe(float64(resp.ContentLength))
217+
} else {
218+
c.metrics.debuginfodFileSize.Observe(float64(len(data)))
145219
}
146220

147-
return filePath, nil
221+
return data, nil
148222
}
149223

150224
func isRetryableError(err error) bool {

0 commit comments

Comments
 (0)