Skip to content

Commit 612bbb3

Browse files
pyroscope.java: allow custom asprof distributions instead of embeded one. (#4452)
* pyroscope.java: decouple archive extraction from the profiler, remove Profiler(use Distribution). * playground * fix args * cleanup * todo * review fix * review fixes * pyroscope.java: add integration tests (#4454) * pyroscope.java: add integration tests fix package name Revert "pyroscope.java: Fix java log level parameter (#4440)" This reverts commit 4909877. move the helper to pyroscope package * second integration test * revert compose tests * revert unneeded changes * fix buildtag * fix buildtag * improve start time for pyroscope container * skip integration test if it's not pyoroscope job * update makefile * pyroscope.java: Fix java log level parameter (#4440) * pyroscope.java: Fix java log level parameter The version bundled of the async profiler has no loglevel parameter: ``` ts=2025-09-16T08:16:50.898924708Z level=error component_path=/profiling.feature component_id=pyroscope.java.java_pods pid=1184752 err="failed to start: asprof failed to run: asprof failed to run /tmp/alloy-asprof-ae0261b1093f2bc4df44a87300fef98dcdebccb5/bin/asprof: exit status 1 Unrecognized option: --loglevel\n" ``` * Quiet is not a valid argument for the async-profiler cli It can only be used for attaching using agent * remove comments * fix --------- Co-authored-by: Christian Simon <[email protected]> --------- Co-authored-by: Christian Simon <[email protected]>
1 parent c1111d4 commit 612bbb3

File tree

17 files changed

+525
-199
lines changed

17 files changed

+525
-199
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ internal/web/ui/build/
77
packaging/windows/LICENSE
88
packaging/windows/agent-windows-amd64.exe
99
cmd/grafana-agent/Dockerfile
10+
alloy

.github/workflows/test_pyroscope_pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ jobs:
2626
go-version-file: go.mod
2727
cache: false
2828

29-
- run: make GO_TAGS="nodocker" test-pyroscope
29+
- run: sudo make test-pyroscope

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ node_modules
3333
# file of the pair will detect a dirty work tree and detect the wrong tag name.
3434
.tag-only
3535
.image-tag
36+
alloy

internal/component/pyroscope/java/args.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ type Arguments struct {
1414

1515
TmpDir string `alloy:"tmp_dir,attr,optional"`
1616
ProfilingConfig ProfilingConfig `alloy:"profiling_config,block,optional"`
17+
18+
// undocumented
19+
Dist string `alloy:"dist,attr,optional"`
1720
}
1821

1922
type ProfilingConfig struct {
@@ -29,7 +32,7 @@ type ProfilingConfig struct {
2932
}
3033

3134
func (rc *Arguments) UnmarshalAlloy(f func(interface{}) error) error {
32-
*rc = defaultArguments()
35+
*rc = DefaultArguments()
3336
type config Arguments
3437
return f((*config)(rc))
3538
}
@@ -43,7 +46,7 @@ func (arg *Arguments) Validate() error {
4346
}
4447
}
4548

46-
func defaultArguments() Arguments {
49+
func DefaultArguments() Arguments {
4750
return Arguments{
4851
TmpDir: "/tmp",
4952
ProfilingConfig: ProfilingConfig{

internal/component/pyroscope/java/asprof/asprof.go

Lines changed: 40 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -20,49 +20,47 @@ var fsMutex sync.Mutex
2020

2121
type Distribution struct {
2222
extractedDir string
23-
version int
2423
}
2524

26-
func (d *Distribution) LauncherPath() string {
27-
return filepath.Join(d.extractedDir, "bin/asprof")
25+
func NewExtractedDistribution(extractedDir string) (Distribution, error) {
26+
d := Distribution{extractedDir: extractedDir}
27+
if _, err := os.Stat(d.LauncherPath()); err != nil {
28+
return d, fmt.Errorf("asprof launcher not found: %w", err)
29+
}
30+
if _, err := os.Stat(d.LibPath()); err != nil {
31+
return d, fmt.Errorf("asprof lib not found: %w", err)
32+
}
33+
return d, nil
2834
}
2935

30-
type Profiler struct {
31-
tmpDir string
32-
extractOnce sync.Once
33-
dist *Distribution
34-
extractError error
35-
tmpDirMarker any
36-
archiveHash string
37-
archive Archive
36+
func (d Distribution) LauncherPath() string {
37+
return filepath.Join(d.extractedDir, "bin/asprof")
3838
}
3939

4040
type Archive struct {
41-
data []byte
42-
version int
43-
format int
41+
data []byte
42+
format int
43+
}
44+
45+
func (a *Archive) sha1() string {
46+
sum := sha1.Sum(a.data)
47+
return hex.EncodeToString(sum[:])
48+
}
49+
50+
func (a *Archive) DistName() string {
51+
return fmt.Sprintf("alloy-asprof-%s", a.sha1())
4452
}
4553

4654
const (
4755
ArchiveFormatTarGz = iota
4856
ArchiveFormatZip
4957
)
5058

51-
func NewProfiler(tmpDir string, archive Archive) *Profiler {
52-
res := &Profiler{tmpDir: tmpDir, dist: new(Distribution), tmpDirMarker: "alloy-asprof"}
53-
sum := sha1.Sum(archive.data)
54-
hexSum := hex.EncodeToString(sum[:])
55-
res.archiveHash = hexSum
56-
res.dist.version = archive.version
57-
res.archive = archive
58-
return res
59-
}
60-
61-
func (p *Profiler) Execute(dist *Distribution, argv []string) (string, string, error) {
59+
func (d Distribution) Execute(argv []string) (string, string, error) {
6260
stdout := bytes.NewBuffer(nil)
6361
stderr := bytes.NewBuffer(nil)
6462

65-
exe := dist.LauncherPath()
63+
exe := d.LauncherPath()
6664
cmd := exec.Command(exe, argv...)
6765

6866
cmd.Stdout = stdout
@@ -78,61 +76,46 @@ func (p *Profiler) Execute(dist *Distribution, argv []string) (string, string, e
7876
return stdout.String(), stderr.String(), nil
7977
}
8078

81-
func (p *Profiler) Distribution() *Distribution {
82-
return p.dist
83-
}
84-
85-
func (p *Profiler) ExtractDistributions() error {
86-
p.extractOnce.Do(func() {
87-
p.extractError = p.extractDistributions()
88-
})
89-
return p.extractError
90-
}
91-
92-
func (p *Profiler) extractDistributions() error {
79+
func ExtractDistribution(a Archive, tmpDir, distName string) (Distribution, error) {
80+
d := Distribution{}
9381
fsMutex.Lock()
9482
defer fsMutex.Unlock()
95-
distName := p.getDistName()
9683

97-
var launcher, dist []byte
98-
err := readArchive(p.archive.data, p.archive.format, func(name string, fi fs.FileInfo, data []byte) error {
84+
var launcher, lib []byte
85+
err := readArchive(a.data, a.format, func(name string, fi fs.FileInfo, data []byte) error {
9986
if strings.Contains(name, "asprof") {
10087
launcher = data
10188
}
10289
if strings.Contains(name, "libasyncProfiler") {
103-
dist = data
90+
lib = data
10491
}
10592
return nil
10693
})
10794
if err != nil {
108-
return err
95+
return d, err
10996
}
110-
if launcher == nil || dist == nil {
111-
return fmt.Errorf("failed to find libasyncProfiler in archive %s", distName)
97+
if launcher == nil || lib == nil {
98+
return d, fmt.Errorf("failed to find libasyncProfiler in archive %s", distName)
11299
}
113100

114101
fileMap := map[string][]byte{}
115-
fileMap[filepath.Join(distName, p.dist.LauncherPath())] = launcher
116-
fileMap[filepath.Join(distName, p.dist.LibPath())] = dist
117-
tmpDirFile, err := os.Open(p.tmpDir)
102+
fileMap[filepath.Join(distName, d.LauncherPath())] = launcher
103+
fileMap[filepath.Join(distName, d.LibPath())] = lib
104+
tmpDirFile, err := os.Open(tmpDir)
118105
if err != nil {
119-
return fmt.Errorf("failed to open tmp dir %s: %w", p.tmpDir, err)
106+
return d, fmt.Errorf("failed to open tmp dir %s: %w", tmpDir, err)
120107
}
121108
defer tmpDirFile.Close()
122109

123110
if err = checkTempDirPermissions(tmpDirFile); err != nil {
124-
return err
111+
return d, err
125112
}
126113

127114
for path, data := range fileMap {
128115
if err = writeFile(tmpDirFile, path, data, true); err != nil {
129-
return err
116+
return d, err
130117
}
131118
}
132-
p.dist.extractedDir = filepath.Join(p.tmpDir, distName)
133-
return nil
134-
}
135-
136-
func (p *Profiler) getDistName() string {
137-
return fmt.Sprintf("%s-%s", p.tmpDirMarker, p.archiveHash)
119+
d.extractedDir = filepath.Join(tmpDir, distName)
120+
return d, nil
138121
}

internal/component/pyroscope/java/asprof/asprof_darwin.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@ var embeddedArchiveData []byte
1313
// bin/asprof
1414
// lib/libasyncProfiler.dylib
1515

16-
var embeddedArchiveVersion = 300
16+
var EmbeddedArchive = Archive{data: embeddedArchiveData, format: ArchiveFormatZip}
1717

18-
var EmbeddedArchive = Archive{data: embeddedArchiveData, version: embeddedArchiveVersion, format: ArchiveFormatZip}
19-
20-
func (d *Distribution) LibPath() string {
18+
func (d Distribution) LibPath() string {
2119
return filepath.Join(d.extractedDir, "lib/libasyncProfiler.dylib")
2220
}
2321

24-
func (p *Profiler) CopyLib(dist *Distribution, pid int) error {
22+
func (d Distribution) CopyLib(pid int) error {
2523
return nil
2624
}
2725

internal/component/pyroscope/java/asprof/asprof_linux.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,20 @@ import (
1010
"strings"
1111
)
1212

13-
var embeddedArchiveVersion = 300
13+
var EmbeddedArchive = Archive{data: embeddedArchiveData, format: ArchiveFormatTarGz}
1414

15-
var EmbeddedArchive = Archive{data: embeddedArchiveData, version: embeddedArchiveVersion, format: ArchiveFormatTarGz}
16-
17-
func (d *Distribution) LibPath() string {
15+
func (d Distribution) LibPath() string {
1816
return filepath.Join(d.extractedDir, "lib/libasyncProfiler.so")
1917
}
2018

21-
func (p *Profiler) CopyLib(dist *Distribution, pid int) error {
19+
func (d Distribution) CopyLib(pid int) error {
2220
fsMutex.Lock()
2321
defer fsMutex.Unlock()
24-
libData, err := os.ReadFile(dist.LibPath())
22+
libData, err := os.ReadFile(d.LibPath())
2523
if err != nil {
2624
return err
2725
}
28-
launcherData, err := os.ReadFile(dist.LauncherPath())
26+
launcherData, err := os.ReadFile(d.LauncherPath())
2927
if err != nil {
3028
return err
3129
}
@@ -35,8 +33,8 @@ func (p *Profiler) CopyLib(dist *Distribution, pid int) error {
3533
return fmt.Errorf("failed to open proc root %s: %w", procRoot, err)
3634
}
3735
defer procRootFile.Close()
38-
dstLibPath := strings.TrimPrefix(dist.LibPath(), "/")
39-
dstLauncherPath := strings.TrimPrefix(dist.LauncherPath(), "/")
36+
dstLibPath := strings.TrimPrefix(d.LibPath(), "/")
37+
dstLauncherPath := strings.TrimPrefix(d.LauncherPath(), "/")
4038
if err = writeFile(procRootFile, dstLibPath, libData, false); err != nil {
4139
return err
4240
}
@@ -48,15 +46,5 @@ func (p *Profiler) CopyLib(dist *Distribution, pid int) error {
4846
}
4947

5048
func ProcessPath(path string, pid int) string {
51-
f := procFile{path, pid}
52-
return f.procRootPath()
53-
}
54-
55-
type procFile struct {
56-
path string
57-
pid int
58-
}
59-
60-
func (f *procFile) procRootPath() string {
61-
return filepath.Join("/proc", strconv.Itoa(f.pid), "root", f.path)
49+
return filepath.Join("/proc", strconv.Itoa(pid), "root", path)
6250
}

internal/component/pyroscope/java/asprof/asprof_test.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/google/uuid"
12-
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
1313
)
1414

1515
// extracting to /tmp
@@ -23,45 +23,45 @@ import (
2323
// write skippable tests with uid=0
2424
func TestStickyDir(t *testing.T) {
2525
dir := "/tmp"
26-
p := NewProfiler(dir, EmbeddedArchive)
27-
p.tmpDirMarker = fmt.Sprintf("alloy-asprof-%s", uuid.NewString())
28-
t.Logf("tmpDirMarker: %s", p.tmpDirMarker)
29-
err := p.ExtractDistributions()
30-
assert.NoError(t, err)
26+
tmpDirMarker := fmt.Sprintf("alloy-asprof-%s", uuid.NewString())
27+
t.Logf("tmpDirMarker: %s", tmpDirMarker)
28+
dist, err := ExtractDistribution(EmbeddedArchive, dir, tmpDirMarker)
29+
require.NoError(t, err)
30+
require.NotNil(t, dist)
3131
}
3232

3333
func TestOwnedDir(t *testing.T) {
3434
dir := t.TempDir()
3535
err := os.Chmod(dir, 0755)
36-
assert.NoError(t, err)
37-
p := NewProfiler(dir, EmbeddedArchive)
38-
err = p.ExtractDistributions()
39-
assert.NoError(t, err)
36+
require.NoError(t, err)
37+
dist, err := ExtractDistribution(EmbeddedArchive, dir, "alloy-asprof")
38+
require.NoError(t, err)
39+
require.NotNil(t, dist)
4040
}
4141

4242
func TestOwnedDirWrongPermission(t *testing.T) {
4343
dir := t.TempDir()
4444
err := os.Chmod(dir, 0777)
45-
assert.NoError(t, err)
46-
p := NewProfiler(dir, EmbeddedArchive)
47-
err = p.ExtractDistributions()
48-
assert.Error(t, err)
45+
require.NoError(t, err)
46+
dist, err := ExtractDistribution(EmbeddedArchive, dir, "alloy-asprof-")
47+
require.Error(t, err)
48+
require.Empty(t, dist.extractedDir)
4949
}
5050

5151
func TestDistSymlink(t *testing.T) {
5252
root := t.TempDir()
5353
err := os.Chmod(root, 0755)
54-
assert.NoError(t, err)
54+
require.NoError(t, err)
5555
manipulated := t.TempDir()
5656
err = os.Chmod(manipulated, 0755)
57-
assert.NoError(t, err)
58-
p := NewProfiler(root, EmbeddedArchive)
59-
distName := p.getDistName()
57+
require.NoError(t, err)
58+
distName := "dist"
6059

6160
err = os.Symlink(manipulated, filepath.Join(root, distName))
62-
assert.NoError(t, err)
61+
require.NoError(t, err)
6362

64-
err = p.ExtractDistributions()
63+
dist, err := ExtractDistribution(EmbeddedArchive, root, distName)
6564
t.Logf("expected %s", err)
66-
assert.Error(t, err)
65+
require.Error(t, err)
66+
require.Empty(t, dist.extractedDir)
6767
}

0 commit comments

Comments
 (0)