feat: initial release
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-03 10:00:00 +02:00
commit af4ee19703
617 changed files with 191195 additions and 0 deletions
+353
View File
@@ -0,0 +1,353 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package io
// Benchmarks for the read and write paths that carry the per-value
// work: a table decode, a CSV parse, the HDF5 and NetCDF writers and
// readers. Every input is deterministic and written once, before the
// measured loop.
import (
"encoding/binary"
"math"
"os"
"path/filepath"
"strconv"
"testing"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// Sizes: several thousand rows for the tables, and a few hundred
// thousand values for the array formats, which is large enough for the
// per-cell work to dominate the fixed cost of each entry point.
const (
benchRows = 4000
benchCols = 24
benchSide = 512
)
// benchFloats builds a deterministic float64 array.
func benchFloats(shape ...int) *core.Array {
a := core.New(core.Float, shape...)
raw := a.RawFloats()
for i := range raw {
raw[i] = math.Sin(float64(i)*0.03125)*1000 + float64(i%97)*0.5
}
return a
}
// benchInts builds a deterministic int array.
func benchInts(shape ...int) *core.Array {
a := core.New(core.Int, shape...)
raw := a.RawInts()
for i := range raw {
raw[i] = int64(i)*7 - 3
}
return a
}
// benchBinaryTableFile writes a BINTABLE holding one column of every
// numeric form the reader decodes plus a character column, and returns
// its path. The package's own writer emits a subset of those forms
// (B, I and J arrive from other writers), so the table is laid out
// here.
func benchBinaryTableFile(b testing.TB, rows int) string {
b.Helper()
forms := []string{"K", "D", "E", "J", "I", "B", "L", "8A"}
widths := []int{8, 8, 4, 4, 2, 1, 1, 8}
rowBytes := 0
for _, w := range widths {
rowBytes += w
}
cards := []string{
fitsStringCardRaw("XTENSION", "BINTABLE"),
fitsIntCard("BITPIX", 8),
fitsIntCard("NAXIS", 2),
fitsIntCard("NAXIS1", rowBytes),
fitsIntCard("NAXIS2", rows),
fitsIntCard("PCOUNT", 0),
fitsIntCard("GCOUNT", 1),
fitsIntCard("TFIELDS", len(forms)),
}
for i, form := range forms {
n := strconv.Itoa(i + 1)
cards = append(cards,
fitsStringCardRaw("TTYPE"+n, "COL"+n),
fitsStringCardRaw("TFORM"+n, form))
}
cards = append(cards, fitsEndCard())
out := fitsAppendCards(nil, []string{
fitsBoolCard("SIMPLE", true),
fitsIntCard("BITPIX", 8),
fitsIntCard("NAXIS", 0),
fitsBoolCard("EXTEND", true),
fitsEndCard(),
})
out = fitsAppendCards(out, cards)
body := make([]byte, rows*rowBytes)
star := []byte("star ")
for r := range rows {
p := r * rowBytes
binary.BigEndian.PutUint64(body[p:], uint64(1000+r))
p += 8
binary.BigEndian.PutUint64(body[p:], math.Float64bits(float64(r)*0.25-1))
p += 8
binary.BigEndian.PutUint32(body[p:], math.Float32bits(float32(r)*0.5))
p += 4
binary.BigEndian.PutUint32(body[p:], uint32(int32(-r)))
p += 4
binary.BigEndian.PutUint16(body[p:], uint16(int16(r)))
p += 2
body[p] = byte(r)
p++
if r%2 == 0 {
body[p] = 'T'
} else {
body[p] = 'F'
}
p++
copy(body[p:], star)
}
out = append(out, body...)
out = fitsAppendZeroPad(out)
path := filepath.Join(b.TempDir(), "binary.fits")
if err := os.WriteFile(path, out, 0o644); err != nil {
b.Fatal(err)
}
return path
}
// benchASCIITableFile writes an ASCII table with a character, an
// integer and a float column, and returns its path.
func benchASCIITableFile(b testing.TB, rows int) string {
b.Helper()
text := make([]string, rows)
ints := core.New(core.Int, rows)
floats := core.New(core.Float, rows)
for i := range rows {
text[i] = "star-" + strconv.Itoa(i)
ints.RawInts()[i] = int64(1000 + i)
floats.RawFloats()[i] = float64(i)*0.125 - 42
}
cols := []FITSTableColumn{
{Name: "STAR", Form: "12A", Text: text},
{Name: "ID", Form: "I10", Data: ints},
{Name: "MAG", Form: "D20.12", Data: floats},
}
path := filepath.Join(b.TempDir(), "ascii.fits")
if err := SaveFITSTable(path, true, cols, nil); err != nil {
b.Fatal(err)
}
return path
}
// benchCSVFile writes a float64 matrix as CSV and returns its path.
func benchCSVFile(b testing.TB, rows, cols int) string {
b.Helper()
path := filepath.Join(b.TempDir(), "bench.csv")
if err := SaveCSV(path, benchFloats(rows, cols)); err != nil {
b.Fatal(err)
}
return path
}
// benchHDF5File writes a classic HDF5 file of one float64 and one
// int64 dataset, optionally through the deflate and shuffle filters,
// and returns its path.
func benchHDF5File(b testing.TB, filtered bool) string {
b.Helper()
sets := []HDF5Dataset{
{Path: "/field", Values: benchFloats(benchSide, benchSide)},
{Path: "/ids", Values: benchInts(benchSide, benchSide)},
}
attrs := map[string]map[string]string{"/": {"origin": "benchmark"}}
opts := HDF5WriteOptions{}
if filtered {
opts = HDF5WriteOptions{Gzip: 6, Shuffle: true}
}
path := filepath.Join(b.TempDir(), "bench.h5")
if err := SaveHDF5(path, sets, attrs, opts); err != nil {
b.Fatal(err)
}
return path
}
// benchNetCDFFile writes a classic NetCDF file of one float64 variable
// in two dimensions and returns its path.
func benchNetCDFFile(b testing.TB) string {
b.Helper()
dims := []NetCDFDim{{Name: "row", Length: benchSide}, {Name: "col", Length: benchSide}}
vars := []NetCDFVar{{Name: "field", Dims: []string{"row", "col"}, Values: benchFloats(benchSide, benchSide)}}
path := filepath.Join(b.TempDir(), "bench.nc")
if err := SaveNetCDF(path, dims, vars, map[string]string{"title": "benchmark"}); err != nil {
b.Fatal(err)
}
return path
}
func BenchmarkLoadFITSTableBinary(b *testing.B) {
path := benchBinaryTableFile(b, benchRows)
b.ReportAllocs()
for b.Loop() {
table, err := LoadFITSTable(path)
if err != nil {
b.Fatal(err)
}
if table.Rows != benchRows || len(table.Columns) != 8 || table.Text[7] == nil {
b.Fatalf("table came back as %s with %d columns", table.Kind, len(table.Columns))
}
}
}
func BenchmarkLoadFITSTableASCII(b *testing.B) {
path := benchASCIITableFile(b, benchRows)
b.ReportAllocs()
for b.Loop() {
table, err := LoadFITSTable(path)
if err != nil {
b.Fatal(err)
}
if table.Rows != benchRows || len(table.Columns) != 3 {
b.Fatalf("table came back with %d rows and %d columns", table.Rows, len(table.Columns))
}
}
}
func BenchmarkLoadCSV(b *testing.B) {
path := benchCSVFile(b, benchRows, benchCols)
b.ReportAllocs()
for b.Loop() {
a, err := LoadCSV(path, false)
if err != nil {
b.Fatal(err)
}
if a.Shape()[0] != benchRows || a.Shape()[1] != benchCols {
b.Fatalf("array came back with shape %v", a.Shape())
}
}
}
func BenchmarkSaveHDF5(b *testing.B) {
sets := []HDF5Dataset{
{Path: "/field", Values: benchFloats(benchSide, benchSide)},
{Path: "/ids", Values: benchInts(benchSide, benchSide)},
}
attrs := map[string]map[string]string{"/": {"origin": "benchmark"}}
path := filepath.Join(b.TempDir(), "bench.h5")
b.ReportAllocs()
b.SetBytes(int64(2 * benchSide * benchSide * 8))
for b.Loop() {
if err := SaveHDF5(path, sets, attrs); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSaveHDF5Large writes one 8 MiB float64 field, the size
// where moving the payload around shows above the per-value work.
func BenchmarkSaveHDF5Large(b *testing.B) {
sets := []HDF5Dataset{{Path: "/field", Values: benchFloats(1024, 1024)}}
path := filepath.Join(b.TempDir(), "bench.h5")
b.ReportAllocs()
b.SetBytes(8 << 20)
for b.Loop() {
if err := SaveHDF5(path, sets, nil); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSaveHDF5Filtered writes the two benchmark datasets through
// the shuffle and deflate filters, the chunked path.
func BenchmarkSaveHDF5Filtered(b *testing.B) {
sets := []HDF5Dataset{
{Path: "/field", Values: benchFloats(benchSide, benchSide)},
{Path: "/ids", Values: benchInts(benchSide, benchSide)},
}
path := filepath.Join(b.TempDir(), "bench.h5")
b.ReportAllocs()
b.SetBytes(int64(2 * benchSide * benchSide * 8))
for b.Loop() {
if err := SaveHDF5(path, sets, nil, HDF5WriteOptions{Gzip: 6, Shuffle: true}); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSaveHDF5Text writes one string dataset, the fixed-width
// text path whose elements are padded to the longest of the file.
func BenchmarkSaveHDF5Text(b *testing.B) {
const side = 512
text := make([]string, side*side)
width := 0
for i := range text {
text[i] = "row" + strconv.Itoa(i)
width = max(width, len(text[i]))
}
sets := []HDF5TextDataset{{Path: "/labels", Shape: []int{side, side}, Text: text}}
path := filepath.Join(b.TempDir(), "bench.h5")
b.ReportAllocs()
b.SetBytes(int64(side * side * width))
for b.Loop() {
if err := SaveHDF5Text(path, sets); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkLoadHDF5(b *testing.B) {
path := benchHDF5File(b, false)
b.ReportAllocs()
for b.Loop() {
sets, err := LoadHDF5(path)
if err != nil {
b.Fatal(err)
}
if len(sets) != 2 {
b.Fatalf("file came back with %d datasets", len(sets))
}
}
}
func BenchmarkLoadHDF5Filtered(b *testing.B) {
path := benchHDF5File(b, true)
b.ReportAllocs()
for b.Loop() {
sets, err := LoadHDF5(path)
if err != nil {
b.Fatal(err)
}
if len(sets) != 2 {
b.Fatalf("file came back with %d datasets", len(sets))
}
}
}
func BenchmarkSaveNetCDF(b *testing.B) {
dims := []NetCDFDim{{Name: "row", Length: benchSide}, {Name: "col", Length: benchSide}}
vars := []NetCDFVar{{Name: "field", Dims: []string{"row", "col"}, Values: benchFloats(benchSide, benchSide)}}
attrs := map[string]string{"title": "benchmark"}
path := filepath.Join(b.TempDir(), "bench.nc")
b.ReportAllocs()
for b.Loop() {
if err := SaveNetCDF(path, dims, vars, attrs); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkLoadNetCDF(b *testing.B) {
path := benchNetCDFFile(b)
b.ReportAllocs()
for b.Loop() {
_, vars, _, err := LoadNetCDF(path)
if err != nil {
b.Fatal(err)
}
if len(vars) != 1 {
b.Fatalf("file came back with %d variables", len(vars))
}
}
}