diff --git a/pkg/clickhouse/ingester_test.go b/pkg/clickhouse/ingester_test.go index 656eed3a1c4..47d3e2cc64c 100644 --- a/pkg/clickhouse/ingester_test.go +++ b/pkg/clickhouse/ingester_test.go @@ -79,6 +79,24 @@ func TestDecodeLineInfoNoMapping(t *testing.T) { require.Equal(t, "app/importer.py", info.FunctionFilename) } +func TestDecodeLineInfoV2SystemName(t *testing.T) { + stringTable := []string{"", "decode_system", "app/importer.py"} + encoded := profile.EncodePprofLocation( + &pprofpb.Location{ + Address: 0x47, + Line: []*pprofpb.Line{{FunctionId: 1, Line: 7}}, + }, + nil, + []*pprofpb.Function{{SystemName: 1, Filename: 2}}, + stringTable, + ) + + info := decodeLineInfo(encoded) + require.Empty(t, info.FunctionName) + require.Equal(t, "decode_system", info.FunctionSystemName) + require.Equal(t, "app/importer.py", info.FunctionFilename) +} + // Native frames are sent unsymbolized (no lines); nothing should be decoded. func TestDecodeLineInfoUnsymbolizedFrame(t *testing.T) { stringTable := []string{"", "build-id-456", "libc.so.6"} diff --git a/pkg/clickhouse/querier.go b/pkg/clickhouse/querier.go index b375365b9d4..cadefaa8cff 100644 --- a/pkg/clickhouse/querier.go +++ b/pkg/clickhouse/querier.go @@ -835,6 +835,13 @@ type sampleData struct { period int64 } +func (s sampleData) hasStoredFunction(i int) bool { + return (i < len(s.functionNames) && s.functionNames[i] != "") || + (i < len(s.functionSystemNames) && s.functionSystemNames[i] != "") || + (i < len(s.functionFilenames) && s.functionFilenames[i] != "") || + (i < len(s.functionStartLines) && s.functionStartLines[i] != 0) +} + // rowsToArrowRecords converts ClickHouse query results to Arrow records. func (q *Querier) rowsToArrowRecords( ctx context.Context, @@ -884,8 +891,8 @@ func (q *Querier) rowsToArrowRecords( } addr := s.addresses[i] - // Check if this location needs symbolization (no function name but has build ID) - needsSymbolization := (i >= len(s.functionNames) || s.functionNames[i] == "") && buildID != "" && addr != 0 + // Symbolize only when no stored function metadata is available. + needsSymbolization := !s.hasStoredFunction(i) && buildID != "" && addr != 0 if needsSymbolization { if _, ok := locationIndex[buildID]; !ok { @@ -1037,8 +1044,9 @@ func (q *Querier) rowsToArrowRecords( w.FunctionStartLine.AppendNull() } } - } else if idx < len(s.functionNames) && s.functionNames[idx] != "" { - // Use stored function data + } else if s.hasStoredFunction(idx) { + // Use stored function data, including v2 rows where only the + // system name is populated. w.Lines.Append(true) w.Line.Append(true) if idx < len(s.lineNumbers) { diff --git a/pkg/clickhouse/querier_test.go b/pkg/clickhouse/querier_test.go new file mode 100644 index 00000000000..56aa3781cc1 --- /dev/null +++ b/pkg/clickhouse/querier_test.go @@ -0,0 +1,26 @@ +// Copyright 2026 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 clickhouse + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSampleDataHasStoredFunction(t *testing.T) { + require.True(t, (sampleData{functionSystemNames: []string{"local_function"}}).hasStoredFunction(0)) + require.True(t, (sampleData{functionNames: []string{"function"}}).hasStoredFunction(0)) + require.False(t, (sampleData{functionNames: []string{""}, functionSystemNames: []string{""}}).hasStoredFunction(0)) +}