Skip to content

feat(spanner): add experimental raw decode path for string columns#14631

Open
rahul2393 wants to merge 7 commits into
mainfrom
add-experimental-decode
Open

feat(spanner): add experimental raw decode path for string columns#14631
rahul2393 wants to merge 7 commits into
mainfrom
add-experimental-decode

Conversation

@rahul2393
Copy link
Copy Markdown
Contributor

@rahul2393 rahul2393 commented May 22, 2026

Summary

Adds an opt-in experimental streaming decode path for allocation-sensitive Spanner queries.

The experimental path:

  • uses vtprotobuf unsafe unmarshalling for streaming PartialResultSet responses
  • retains gRPC buffers for the lifetime of the currently returned row
  • reuses row backing storage while iterating fixed-shape streaming results
  • preserves existing streaming query behavior for chunking, retry/resume, stats, transaction metadata, precommit tokens, and cache updates

This path has a stricter row lifetime contract: returned rows are valid until the next Next() call or Stop(). Callers that need to retain values must copy them before advancing the iterator.

Benchmark

Benchmarks use public client APIs with an in-process fake Spanner server, comparing default decode vs QueryOptions{ExperimentalRawDecode: true}.

Command:

go test -run '^$' -bench 'Benchmark(ReadLargeResultSetExactShapePublicQuery|CustomerShapePublicQuery)$' -benchmem -count=5 > /tmp/spanner-final-old.txt
SPANNER_RAW_BENCH=1 go test -run '^$' -bench 'Benchmark(ReadLargeResultSetExactShapePublicQuery|CustomerShapePublicQuery)$' -benchmem -count=5 > /tmp/spanner-final-new.txt
benchstat /tmp/spanner-final-old.txt /tmp/spanner-final-new.txt

Results on Apple M2 Max:

goos: darwin
goarch: arm64
pkg: cloud.google.com/go/spanner
cpu: Apple M2 Max
                                           │ /tmp/spanner-final-old.txt │     /tmp/spanner-final-new.txt      │
                                           │           sec/op           │   sec/op     vs base                │
CustomerShapePublicQuery-12                                 2.432m ± 1%   2.338m ± 2%   -3.89% (p=0.000 n=10)
ReadLargeResultSetExactShapePublicQuery-12                  1.921m ± 5%   1.680m ± 2%  -12.51% (p=0.000 n=10)
geomean                                                     2.161m        1.982m        -8.30%

                                           │ /tmp/spanner-final-old.txt │      /tmp/spanner-final-new.txt      │
                                           │            B/op            │     B/op      vs base                │
CustomerShapePublicQuery-12                               1208.9Ki ± 1%   998.5Ki ± 1%  -17.41% (p=0.000 n=10)
ReadLargeResultSetExactShapePublicQuery-12                 1.195Mi ± 0%   1.153Mi ± 0%   -3.45% (p=0.000 n=10)
geomean                                                    1.188Mi        1.060Mi       -10.70%

                                           │ /tmp/spanner-final-old.txt │     /tmp/spanner-final-new.txt     │
                                           │         allocs/op          │  allocs/op   vs base               │
CustomerShapePublicQuery-12                                 9.243k ± 0%   8.632k ± 0%  -6.62% (p=0.000 n=10)
ReadLargeResultSetExactShapePublicQuery-12                  24.66k ± 0%   22.55k ± 0%  -8.54% (p=0.000 n=10)
geomean                                                     15.10k        13.95k       -7.58%

Benchmark shapes

BenchmarkCustomerShapePublicQuery:

  • 100 rows per query
  • 3 STRING columns
  • one 1600-byte metadata string column
  • default path decodes string then copies into caller buffer
  • experimental path uses the same public query surface and row decode flow

BenchmarkReadLargeResultSetExactShapePublicQuery:

  • 100 rows per query
  • 12 columns matching the large result-set decode shape:
    • BOOL
    • BYTES
    • DATE
    • FLOAT32
    • FLOAT64
    • INTERVAL
    • JSON
    • INT64
    • NUMERIC
    • STRING
    • TIMESTAMP
    • UUID

Notes

This is intentionally experimental and should not be enabled by default.

Key limitations:

  • row/value lifetime differs from the default API contract
  • unsafe protobuf unmarshalling requires correct buffer lifetime management
  • callers retaining row values must copy before calling Next() again
  • generated vtprotobuf files add codegen maintenance surface

@rahul2393 rahul2393 requested review from a team as code owners May 22, 2026 13:40
@product-auto-label product-auto-label Bot added the api: spanner Issues related to the Spanner API. label May 22, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an experimental raw decode path for Spanner streaming SQL queries, allowing string columns to be retrieved as raw bytes via a new Row.ColumnBytes method to optimize performance. Key feedback includes a critical memory leak risk associated with using a global sync.Map to track row data, a failure to handle NULL values in the protobuf decoder, and compatibility issues where the raw path breaks standard Row methods like IsNull. It is also recommended to assign a unique name to the custom gRPC codec for better debuggability.

Comment thread spanner/raw_decode.go Outdated
Comment thread spanner/raw_decode.go Outdated
Comment thread spanner/row.go Outdated
Comment thread spanner/raw_decode.go Outdated
@rahul2393
Copy link
Copy Markdown
Contributor Author

Running single thread larger read prober shows around 30% improvement in P50 latency

Screenshot 2026-05-22 at 11 33 06 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the Spanner API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant