Skip to content

Commit

Permalink
pkg/scrape: Support parsing executable info out of pprof comments
Browse files Browse the repository at this point in the history
  • Loading branch information
brancz committed Oct 16, 2023
1 parent 543e39a commit 4e41b79
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 7 deletions.
70 changes: 63 additions & 7 deletions pkg/scrape/scrape.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ package scrape
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -493,13 +496,27 @@ mainLoop:
}

byt := buf.Bytes()
p, err := profile.ParseData(byt)
if err != nil {
level.Error(sl.l).Log("msg", "failed to parse profile data", "err", err)
continue
}

executableInfo := []*profilepb.ExecutableInfo{}
for _, comment := range p.Comments {
if strings.HasPrefix(comment, "executableInfo=") {
ei, err := parseExecutableInfo(comment)
if err != nil {
level.Error(sl.l).Log("msg", "failed to parse executableInfo", "err", err)
continue
}

executableInfo = append(executableInfo, ei)
}
}

ks := sl.target.KeepSet()
if len(ks) > 0 {
p, err := profile.ParseData(byt)
if err != nil {
level.Error(sl.l).Log("msg", "failed to parse profile data", "err", err)
continue
}
keepIndexes := []int{}
newTypes := []*profile.ValueType{}
for i, st := range p.SampleType {
Expand Down Expand Up @@ -528,14 +545,15 @@ mainLoop:
b = newB // We want to make sure we return the new buffer to the pool further below.
}

_, err := sl.store.WriteRaw(sl.ctx, &profilepb.WriteRawRequest{
_, err = sl.store.WriteRaw(sl.ctx, &profilepb.WriteRawRequest{
Normalized: sl.normalizedAddresses,
Series: []*profilepb.RawProfileSeries{
{
Labels: protolbls,
Samples: []*profilepb.RawSample{
{
RawProfile: byt,
RawProfile: byt,
ExecutableInfo: executableInfo,
},
},
},
Expand Down Expand Up @@ -582,6 +600,44 @@ mainLoop:
close(sl.stopped)
}

// parseExecutableInfo parses the executableInfo string from the comment. It is in the format of: "executableInfo=elfType;offset;vaddr".
func parseExecutableInfo(comment string) (*profilepb.ExecutableInfo, error) {
eiString := strings.TrimPrefix(comment, "executableInfo=")
eiParts := strings.Split(eiString, ";")
if len(eiParts) == 0 {
return nil, errors.New("executableInfo string is empty")
}

var (
res = &profilepb.ExecutableInfo{}
err error
)

if len(eiParts) >= 1 {
elfType, err := strconv.ParseUint(strings.TrimPrefix(eiParts[0], "0x"), 16, 32)
if err != nil {
return nil, fmt.Errorf("parse elfType: %w", err)
}

res.ElfType = uint32(elfType)
}

if len(eiParts) == 3 {
res.LoadSegment = &profilepb.LoadSegment{}
res.LoadSegment.Offset, err = strconv.ParseUint(strings.TrimPrefix(eiParts[1], "0x"), 16, 64)
if err != nil {
return nil, fmt.Errorf("parse load segment offset: %w", err)
}

res.LoadSegment.Vaddr, err = strconv.ParseUint(strings.TrimPrefix(eiParts[2], "0x"), 16, 64)
if err != nil {
return nil, fmt.Errorf("parse load segment vaddr: %w", err)
}
}

return res, nil
}

// Stop the scraping. May still write data and stale markers after it has
// returned. Cancel the context to stop all writes.
func (sl *scrapeLoop) stop() {
Expand Down
75 changes: 75 additions & 0 deletions pkg/scrape/scrape_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2023 The Parca Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package scrape

import (
"errors"
"testing"

"github.com/stretchr/testify/require"

profilepb "github.com/parca-dev/parca/gen/proto/go/parca/profilestore/v1alpha1"
)

func TestParseExecutableInfo(t *testing.T) {
testCases := []struct {
name string
input string
expected *profilepb.ExecutableInfo
err error
}{
{
name: "valid",
input: "executableInfo=0x1;0x1000;0x1000",
expected: &profilepb.ExecutableInfo{
ElfType: 0x1,
LoadSegment: &profilepb.LoadSegment{
Offset: 0x1000,
Vaddr: 0x1000,
},
},
err: nil,
},
{
name: "only_elf_type",
input: "executableInfo=0x1",
expected: &profilepb.ExecutableInfo{
ElfType: 0x1,
},
},
{
name: "invalid_elf_type",
input: "test",
err: errors.New("parse elfType: strconv.ParseUint: parsing \"test\": invalid syntax"),
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
info, err := parseExecutableInfo(tc.input)
if tc.err != nil && (err == nil || err.Error() != tc.err.Error()) {
t.Fatalf("unexpected error: %v", err)
return
}
if tc.err != nil && err != nil && err.Error() == tc.err.Error() {
return
}
require.Equal(t, tc.expected.ElfType, info.ElfType)
if tc.expected.LoadSegment != nil {
require.Equal(t, tc.expected.LoadSegment.Offset, info.LoadSegment.Offset)
require.Equal(t, tc.expected.LoadSegment.Vaddr, info.LoadSegment.Vaddr)
}
})
}
}

0 comments on commit 4e41b79

Please sign in to comment.