You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2026/05/14 06:12:27 UTC

(skywalking-banyandb) 17/50: test(banyand/measure): storage-level bench for block_cursor decode (passthrough vs native)

This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch vectorized-query
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git

commit d2b877ef3034596f90816f43ee0c55af0a216879
Author: Hongtao Gao <ha...@gmail.com>
AuthorDate: Sat May 9 04:28:48 2026 +0000

    test(banyand/measure): storage-level bench for block_cursor decode (passthrough vs native)
    
    Build a real tsTable, snapshot it, and time three storage emit paths
    over identical raw stored bytes:
    
      - row:         copyAllTo (legacy row path) → *MeasureResult
      - passthrough: copyAllToBatch with ColumnTypeTagValue/FieldValue
      - native:      copyAllToBatch with native primitives (Int64/String/...)
    
    This is the gate the package-level bench in pkg/query/vectorized/measure
    cannot model — its fixture builds *modelv1.TagValue at the
    model.MeasureResult boundary, so both passthrough and native end up
    reading pre-built wrappers and the storage-side savings are invisible.
    
    Numbers (100 series x 1000 rows, 2 tags + 1 field, count=3 benchtime=2s):
    
      Row         44ms / 1.21M allocs / 60.5MB
      Passthrough 43ms / 1.20M allocs / 57.7MB
      Native      14ms / 0.30M allocs / 20.8MB
    
    Native skips ~3 allocs per cell (the *modelv1.TagValue wrapper +
    oneof + inner struct that mustDecodeTagValue builds for the row and
    passthrough paths). 100K rows x 3 cells x 3 allocs/cell ≈ 904K alloc
    delta — matches the observed 1.207M - 0.303M.
    
    Native and passthrough are production-equivalent end-to-end: storage
    savings of native (3 allocs/cell) cancel the egress reconstruction
    cost (3 allocs/cell at columnValueToTagValue). The package-level
    bench's failure of native types is bench-fixture bias — its fixture
    skips storage decode entirely so native sees only the egress
    reconstruction, not the storage savings that balance it.
---
 banyand/measure/block_batch_benchmark_test.go | 248 ++++++++++++++++++++++++++
 1 file changed, 248 insertions(+)

diff --git a/banyand/measure/block_batch_benchmark_test.go b/banyand/measure/block_batch_benchmark_test.go
new file mode 100644
index 000000000..d4ae37a78
--- /dev/null
+++ b/banyand/measure/block_batch_benchmark_test.go
@@ -0,0 +1,248 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you 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 measure
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/apache/skywalking-banyandb/api/common"
+	"github.com/apache/skywalking-banyandb/banyand/internal/storage"
+	"github.com/apache/skywalking-banyandb/banyand/protector"
+	"github.com/apache/skywalking-banyandb/pkg/convert"
+	"github.com/apache/skywalking-banyandb/pkg/fs"
+	"github.com/apache/skywalking-banyandb/pkg/logger"
+	pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
+	"github.com/apache/skywalking-banyandb/pkg/query/model"
+	"github.com/apache/skywalking-banyandb/pkg/query/vectorized"
+	"github.com/apache/skywalking-banyandb/pkg/test"
+	"github.com/apache/skywalking-banyandb/pkg/timestamp"
+)
+
+// G5b storage-level bench: measures the cost of block_cursor decode at
+// the raw-bytes-to-cell boundary, where native column types can skip
+// the *modelv1.TagValue wrapper construction (3 allocs/cell) that the
+// row path and passthrough column types pay through mustDecodeTagValue.
+//
+// The package-level bench in pkg/query/vectorized/measure cannot model
+// this — its fixture builds *modelv1.TagValue at the model.MeasureResult
+// boundary, so both passthrough and native end up reading pre-built
+// wrappers. Here, we build a real tsTable, snapshot it, and time three
+// storage emit paths over identical raw stored bytes:
+//
+//   - row:         copyAllTo (legacy row path) → *MeasureResult
+//   - passthrough: copyAllToBatch with ColumnTypeTagValue/FieldValue
+//   - native:      copyAllToBatch with native primitives
+//
+// Run via:
+//
+//	go test ./banyand/measure -bench=BenchmarkStorageDecode -benchmem -run=^$ -count=3 -benchtime=2s
+
+const (
+	benchStorageSeries  = 100
+	benchStorageRowsPer = 1000
+)
+
+var benchStorageProj = []model.TagProjection{
+	{Family: "default", Names: []string{"svc", "env_id"}},
+}
+
+var benchStorageFieldProj = []string{"v_int"}
+
+var benchStorageSchemaTagTypes = map[string]pbv1.ValueType{
+	"svc":    pbv1.ValueTypeStr,
+	"env_id": pbv1.ValueTypeInt64,
+}
+
+// benchPassthroughSchema mirrors what BuildBatchSchema currently produces
+// in production: tag/field cells stay as *modelv1.TagValue / FieldValue.
+func benchPassthroughSchema() *vectorized.BatchSchema {
+	return vectorized.NewBatchSchema([]vectorized.ColumnDef{
+		{Role: vectorized.RoleTimestamp, Type: vectorized.ColumnTypeInt64},
+		{Role: vectorized.RoleVersion, Type: vectorized.ColumnTypeInt64},
+		{Role: vectorized.RoleSeriesID, Type: vectorized.ColumnTypeInt64},
+		{Role: vectorized.RoleShardID, Type: vectorized.ColumnTypeInt64},
+		{Role: vectorized.RoleTag, TagFamily: "default", Name: "svc", Type: vectorized.ColumnTypeTagValue},
+		{Role: vectorized.RoleTag, TagFamily: "default", Name: "env_id", Type: vectorized.ColumnTypeTagValue},
+		{Role: vectorized.RoleField, Name: "v_int", Type: vectorized.ColumnTypeFieldValue},
+	})
+}
+
+// benchNativeSchema declares the same columns with native primitive types,
+// so block_cursor's appendDecodedTagBytesAsTyped path is exercised.
+func benchNativeSchema() *vectorized.BatchSchema {
+	return vectorized.NewBatchSchema([]vectorized.ColumnDef{
+		{Role: vectorized.RoleTimestamp, Type: vectorized.ColumnTypeInt64},
+		{Role: vectorized.RoleVersion, Type: vectorized.ColumnTypeInt64},
+		{Role: vectorized.RoleSeriesID, Type: vectorized.ColumnTypeInt64},
+		{Role: vectorized.RoleShardID, Type: vectorized.ColumnTypeInt64},
+		{Role: vectorized.RoleTag, TagFamily: "default", Name: "svc", Type: vectorized.ColumnTypeString},
+		{Role: vectorized.RoleTag, TagFamily: "default", Name: "env_id", Type: vectorized.ColumnTypeInt64},
+		{Role: vectorized.RoleField, Name: "v_int", Type: vectorized.ColumnTypeInt64},
+	})
+}
+
+func buildBenchStorageDataPoints(seriesCount, rowsPerSeries int) *dataPoints {
+	total := seriesCount * rowsPerSeries
+	dps := &dataPoints{
+		seriesIDs:   make([]common.SeriesID, total),
+		timestamps:  make([]int64, total),
+		versions:    make([]int64, total),
+		tagFamilies: make([][]nameValues, total),
+		fields:      make([]nameValues, total),
+	}
+	for s := range seriesCount {
+		for r := range rowsPerSeries {
+			i := s*rowsPerSeries + r
+			dps.seriesIDs[i] = common.SeriesID(s + 1)
+			dps.timestamps[i] = int64(r + 1)
+			dps.versions[i] = 1
+			dps.tagFamilies[i] = []nameValues{
+				{
+					name: "default", values: []*nameValue{
+						{name: "svc", valueType: pbv1.ValueTypeStr, value: []byte("alpha")},
+						{name: "env_id", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(int64(s + 1))},
+					},
+				},
+			}
+			dps.fields[i] = nameValues{
+				name: "skipped", values: []*nameValue{
+					{name: "v_int", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(42)},
+				},
+			}
+		}
+	}
+	return dps
+}
+
+// benchStorageHarness owns the tsTable + snapshot + part list used by all
+// three storage decode benchmarks. Built once per benchmark function via
+// b.Helper / b.Cleanup so the per-iteration cost focuses on cursor decode.
+type benchStorageHarness struct {
+	tst       *tsTable
+	snap      *snapshot
+	parts     []*part
+	sids      []common.SeriesID
+	queryOpts queryOptions
+}
+
+func setupBenchStorageHarness(b *testing.B) *benchStorageHarness {
+	b.Helper()
+	tmpPath, defFn := test.Space(require.New(b))
+	tst, err := newTSTable(
+		fs.NewLocalFileSystem(),
+		tmpPath, common.Position{},
+		logger.GetLogger("bench"),
+		timestamp.TimeRange{},
+		option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+		nil,
+	)
+	require.NoError(b, err)
+	dps := buildBenchStorageDataPoints(benchStorageSeries, benchStorageRowsPer)
+	tst.mustAddDataPoints(dps)
+	time.Sleep(200 * time.Millisecond)
+
+	snap := tst.currentSnapshot()
+	require.NotNil(b, snap)
+
+	shardCache := storage.NewShardCache("bench-group", 0, 0)
+	parts, _ := snap.getParts(nil, shardCache, 0, int64(benchStorageRowsPer)+1)
+	require.NotEmpty(b, parts)
+
+	sids := make([]common.SeriesID, benchStorageSeries)
+	for i := range sids {
+		sids[i] = common.SeriesID(i + 1)
+	}
+
+	queryOpts := queryOptions{
+		schemaTagTypes: benchStorageSchemaTagTypes,
+		minTimestamp:   0,
+		maxTimestamp:   int64(benchStorageRowsPer) + 1,
+	}
+	queryOpts.MeasureQueryOptions.TagProjection = benchStorageProj
+	queryOpts.MeasureQueryOptions.FieldProjection = benchStorageFieldProj
+
+	b.Cleanup(func() {
+		snap.decRef()
+		tst.Close()
+		defFn()
+	})
+	return &benchStorageHarness{tst: tst, snap: snap, parts: parts, sids: sids, queryOpts: queryOpts}
+}
+
+// drainCursors walks every block in the harness's parts and invokes f
+// against each loaded cursor. Mirrors loadCursorsForBatch's per-block
+// step but inline so the bench stays single-goroutine and deterministic.
+func (h *benchStorageHarness) drainCursors(f func(bc *blockCursor)) {
+	ti := &tstIter{}
+	ti.init(h.parts, h.sids, h.queryOpts.minTimestamp, h.queryOpts.maxTimestamp)
+	for ti.nextBlock() {
+		bc := generateBlockCursor()
+		p := ti.piHeap[0]
+		opts := h.queryOpts
+		opts.TagProjection = benchStorageProj
+		opts.FieldProjection = benchStorageFieldProj
+		bc.init(p.p, p.curBlock, opts)
+		tmpBlock := generateBlock()
+		if bc.loadData(tmpBlock) {
+			f(bc)
+		}
+		releaseBlock(tmpBlock)
+		releaseBlockCursor(bc)
+	}
+}
+
+func BenchmarkStorageDecode_Row(b *testing.B) {
+	h := setupBenchStorageHarness(b)
+	b.ReportAllocs()
+	b.ResetTimer()
+	for b.Loop() {
+		h.drainCursors(func(bc *blockCursor) {
+			r := &model.MeasureResult{}
+			bc.copyAllTo(r, nil, benchStorageProj, false)
+		})
+	}
+}
+
+func BenchmarkStorageDecode_Passthrough(b *testing.B) {
+	h := setupBenchStorageHarness(b)
+	schema := benchPassthroughSchema()
+	b.ReportAllocs()
+	b.ResetTimer()
+	for b.Loop() {
+		h.drainCursors(func(bc *blockCursor) {
+			mb := newMeasureBatchForSchema(schema, len(bc.timestamps))
+			bc.copyAllToBatch(mb, schema, nil, false)
+		})
+	}
+}
+
+func BenchmarkStorageDecode_Native(b *testing.B) {
+	h := setupBenchStorageHarness(b)
+	schema := benchNativeSchema()
+	b.ReportAllocs()
+	b.ResetTimer()
+	for b.Loop() {
+		h.drainCursors(func(bc *blockCursor) {
+			mb := newMeasureBatchForSchema(schema, len(bc.timestamps))
+			bc.copyAllToBatch(mb, schema, nil, false)
+		})
+	}
+}