From 74e6fcaf83855e8619e6d56cd04dd3998f618ef8 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 13 Mar 2026 14:26:17 +0100 Subject: [PATCH 1/5] feat: add static linking for Linux builds using musl Enable static linking for Linux binaries by installing musl-tools and passing appropriate LDFLAGS during build. This ensures portable, self-contained executables for Linux targets. --- .github/workflows/release.yml | 13 ++++++++++++- Makefile | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e419df..ac1a0f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,6 +62,12 @@ jobs: TAG=${GITHUB_REF#refs/tags/Release_} echo "version=$TAG" >> $GITHUB_OUTPUT + - name: Install build tools (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update -q + sudo apt-get install -y musl-tools + - name: Install build tools (macOS) if: runner.os == 'macOS' run: | @@ -74,8 +80,13 @@ jobs: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} VERSION: ${{ steps.get_version.outputs.version }} + CC: ${{ matrix.goos == 'linux' && 'musl-gcc' || '' }} run: | - make obitools + if [ "$GOOS" = "linux" ]; then + make LDFLAGS='-linkmode=external -extldflags=-static' obitools + else + make obitools + fi mkdir -p artifacts # Create a single tar.gz with all binaries for this platform tar -czf artifacts/obitools4_${VERSION}_${{ matrix.output_name }}.tar.gz -C build . diff --git a/Makefile b/Makefile index 1801300..8cd2a0c 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,9 @@ BLUE := \033[0;34m NC := \033[0m GOFLAGS= +LDFLAGS= GOCMD=go -GOBUILD=$(GOCMD) build $(GOFLAGS) +GOBUILD=$(GOCMD) build $(GOFLAGS) $(if $(LDFLAGS),-ldflags='$(LDFLAGS)') GOGENERATE=$(GOCMD) generate GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test From 40769bf8273397e4b716e6558fb72d40b339fb55 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 13 Mar 2026 14:54:14 +0100 Subject: [PATCH 2/5] Add memory-based batching support Implement memory-aware batch sizing with --batch-mem CLI option, enabling adaptive batching based on estimated sequence memory footprint. Key changes: - Added _BatchMem and related getters/setters in pkg/obidefault - Implemented RebatchBySize() in pkg/obiter for memory-constrained batching - Added BioSequence.MemorySize() for conservative memory estimation - Integrated batch-mem option in pkg/obioptions with human-readable size parsing (e.g., 128K, 64M, 1G) - Added obiutils.ParseMemSize/FormatMemSize for unit conversion - Enhanced pool GC in pkg/obiseq/pool.go to trigger explicit GC for large slice discards - Updated sequence_reader.go to apply memory-based rebatching when enabled --- pkg/obidefault/batch.go | 30 ++++++++ pkg/obiiter/batchiterator.go | 56 ++++++++++++++ pkg/obioptions/options.go | 14 ++++ pkg/obiseq/biosequence.go | 22 ++++++ pkg/obiseq/pool.go | 14 ++++ pkg/obitools/obiconvert/sequence_reader.go | 4 + pkg/obiutils/memsize.go | 85 ++++++++++++++++++++++ 7 files changed, 225 insertions(+) create mode 100644 pkg/obiutils/memsize.go diff --git a/pkg/obidefault/batch.go b/pkg/obidefault/batch.go index 5a128fa..d2cc10b 100644 --- a/pkg/obidefault/batch.go +++ b/pkg/obidefault/batch.go @@ -24,3 +24,33 @@ func BatchSize() int { func BatchSizePtr() *int { return &_BatchSize } + +// _BatchMem holds the maximum cumulative memory (in bytes) per batch when +// memory-based batching is requested. A value of 0 disables memory-based +// batching and falls back to count-based batching. +var _BatchMem = 0 +var _BatchMemStr = "" + +// SetBatchMem sets the memory budget per batch in bytes. +func SetBatchMem(n int) { + _BatchMem = n +} + +// BatchMem returns the current memory budget per batch in bytes. +// A value of 0 means memory-based batching is disabled. +func BatchMem() int { + return _BatchMem +} + +func BatchMemPtr() *int { + return &_BatchMem +} + +// BatchMemStr returns the raw --batch-mem string value as provided on the CLI. +func BatchMemStr() string { + return _BatchMemStr +} + +func BatchMemStrPtr() *string { + return &_BatchMemStr +} diff --git a/pkg/obiiter/batchiterator.go b/pkg/obiiter/batchiterator.go index 76b6ab5..9e25d65 100644 --- a/pkg/obiiter/batchiterator.go +++ b/pkg/obiiter/batchiterator.go @@ -444,6 +444,62 @@ func (iterator IBioSequence) Rebatch(size int) IBioSequence { return newIter } +// RebatchBySize reorganises the stream into batches whose cumulative estimated +// memory footprint does not exceed maxBytes. A single sequence larger than +// maxBytes is emitted alone rather than dropped. minSeqs sets a lower bound on +// batch size (in number of sequences) so that very large sequences still form +// reasonably-sized work units; use 1 to disable. +func (iterator IBioSequence) RebatchBySize(maxBytes int, minSeqs int) IBioSequence { + newIter := MakeIBioSequence() + + newIter.Add(1) + + go func() { + newIter.WaitAndClose() + }() + + go func() { + order := 0 + iterator = iterator.SortBatches() + buffer := obiseq.MakeBioSequenceSlice() + bufBytes := 0 + source := "" + + flush := func() { + if len(buffer) > 0 { + newIter.Push(MakeBioSequenceBatch(source, order, buffer)) + order++ + buffer = obiseq.MakeBioSequenceSlice() + bufBytes = 0 + } + } + + for iterator.Next() { + seqs := iterator.Get() + source = seqs.Source() + for _, s := range seqs.Slice() { + sz := s.MemorySize() + // flush before adding if it would overflow, but only if + // we already meet the minimum sequence count + if bufBytes+sz > maxBytes && len(buffer) >= minSeqs { + flush() + } + buffer = append(buffer, s) + bufBytes += sz + } + } + flush() + + newIter.Done() + }() + + if iterator.IsPaired() { + newIter.MarkAsPaired() + } + + return newIter +} + func (iterator IBioSequence) FilterEmpty() IBioSequence { newIter := MakeIBioSequence() diff --git a/pkg/obioptions/options.go b/pkg/obioptions/options.go index 5109ac3..5a9878d 100644 --- a/pkg/obioptions/options.go +++ b/pkg/obioptions/options.go @@ -8,6 +8,7 @@ import ( "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiformats" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" log "github.com/sirupsen/logrus" "github.com/DavidGamba/go-getoptions" @@ -57,6 +58,10 @@ func RegisterGlobalOptions(options *getoptions.GetOpt) { options.GetEnv("OBIBATCHSIZE"), options.Description("Number of sequence per batch for paralelle processing")) + options.StringVar(obidefault.BatchMemStrPtr(), "batch-mem", "", + options.GetEnv("OBIBATCHMEM"), + options.Description("Maximum memory per batch (e.g. 128K, 64M, 1G). Overrides --batch-size when set.")) + options.Bool("solexa", false, options.GetEnv("OBISOLEXA"), options.Description("Decodes quality string according to the Solexa specification.")) @@ -157,6 +162,15 @@ func ProcessParsedOptions(options *getoptions.GetOpt, parseErr error) { if options.Called("solexa") { obidefault.SetReadQualitiesShift(64) } + + if options.Called("batch-mem") { + n, err := obiutils.ParseMemSize(obidefault.BatchMemStr()) + if err != nil { + log.Fatalf("Invalid --batch-mem value %q: %v", obidefault.BatchMemStr(), err) + } + obidefault.SetBatchMem(n) + log.Printf("Memory-based batching enabled: %s per batch", obidefault.BatchMemStr()) + } } func GenerateOptionParser(program string, diff --git a/pkg/obiseq/biosequence.go b/pkg/obiseq/biosequence.go index a362a34..b136bb2 100644 --- a/pkg/obiseq/biosequence.go +++ b/pkg/obiseq/biosequence.go @@ -273,6 +273,28 @@ func (s *BioSequence) Len() int { return len(s.sequence) } +// MemorySize returns an estimate of the memory footprint of the BioSequence +// in bytes. It accounts for the sequence, quality scores, feature data, +// annotations, and fixed struct overhead. The estimate is conservative +// (cap rather than len for byte slices) so it is suitable for memory-based +// batching decisions. +func (s *BioSequence) MemorySize() int { + if s == nil { + return 0 + } + // fixed struct overhead (strings, pointers, mutex pointer) + const overhead = 128 + n := overhead + n += cap(s.sequence) + n += cap(s.qualities) + n += cap(s.feature) + n += len(s.id) + n += len(s.source) + // rough annotation estimate: each key+value pair ~64 bytes on average + n += len(s.annotations) * 64 + return n +} + // HasQualities checks if the BioSequence has sequence qualitiy scores. // // This function does not have any parameters. diff --git a/pkg/obiseq/pool.go b/pkg/obiseq/pool.go index efe0bda..7c5be1f 100644 --- a/pkg/obiseq/pool.go +++ b/pkg/obiseq/pool.go @@ -1,13 +1,20 @@ package obiseq import ( + "runtime" "sync" + "sync/atomic" log "github.com/sirupsen/logrus" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils" ) +const _LargeSliceThreshold = 100 * 1024 // 100 kb — below: leave to GC, above: trigger explicit GC +const _GCBytesBudget = int64(256 * 1024 * 1024) // trigger GC every 256 MB of large discards + +var _largeSliceDiscardedBytes = atomic.Int64{} + var _BioSequenceByteSlicePool = sync.Pool{ New: func() interface{} { bs := make([]byte, 0, 300) @@ -34,6 +41,13 @@ func RecycleSlice(s *[]byte) { } if cap(*s) <= 1024 { _BioSequenceByteSlicePool.Put(s) + } else if cap(*s) >= _LargeSliceThreshold { + n := int64(cap(*s)) + *s = nil + prev := _largeSliceDiscardedBytes.Load() + if _largeSliceDiscardedBytes.Add(n)/_GCBytesBudget > prev/_GCBytesBudget { + runtime.GC() + } } } } diff --git a/pkg/obitools/obiconvert/sequence_reader.go b/pkg/obitools/obiconvert/sequence_reader.go index d05c881..a5e27bc 100644 --- a/pkg/obitools/obiconvert/sequence_reader.go +++ b/pkg/obitools/obiconvert/sequence_reader.go @@ -214,6 +214,10 @@ func CLIReadBioSequences(filenames ...string) (obiiter.IBioSequence, error) { iterator = iterator.Speed("Reading sequences") + if obidefault.BatchMem() > 0 { + iterator = iterator.RebatchBySize(obidefault.BatchMem(), 1) + } + return iterator, nil } diff --git a/pkg/obiutils/memsize.go b/pkg/obiutils/memsize.go new file mode 100644 index 0000000..b2f78d1 --- /dev/null +++ b/pkg/obiutils/memsize.go @@ -0,0 +1,85 @@ +package obiutils + +import ( + "fmt" + "strconv" + "strings" + "unicode" +) + +// ParseMemSize parses a human-readable memory size string and returns the +// equivalent number of bytes. The value is a number optionally followed by a +// unit suffix (case-insensitive): +// +// B or (no suffix) — bytes +// K or KB — kibibytes (1 024) +// M or MB — mebibytes (1 048 576) +// G or GB — gibibytes (1 073 741 824) +// T or TB — tebibytes (1 099 511 627 776) +// +// Examples: "512", "128K", "128k", "64M", "1G", "2GB" +func ParseMemSize(s string) (int, error) { + s = strings.TrimSpace(s) + if s == "" { + return 0, fmt.Errorf("empty memory size string") + } + + // split numeric prefix from unit suffix + i := 0 + for i < len(s) && (unicode.IsDigit(rune(s[i])) || s[i] == '.') { + i++ + } + numStr := s[:i] + unit := strings.ToUpper(strings.TrimSpace(s[i:])) + // strip trailing 'B' from two-letter units (KB→K, MB→M …) + if len(unit) == 2 && unit[1] == 'B' { + unit = unit[:1] + } + + val, err := strconv.ParseFloat(numStr, 64) + if err != nil { + return 0, fmt.Errorf("invalid memory size %q: %w", s, err) + } + + var multiplier float64 + switch unit { + case "", "B": + multiplier = 1 + case "K": + multiplier = 1024 + case "M": + multiplier = 1024 * 1024 + case "G": + multiplier = 1024 * 1024 * 1024 + case "T": + multiplier = 1024 * 1024 * 1024 * 1024 + default: + return 0, fmt.Errorf("unknown memory unit %q in %q", unit, s) + } + + return int(val * multiplier), nil +} + +// FormatMemSize formats a byte count as a human-readable string with the +// largest unit that produces a value ≥ 1 (e.g. 1536 → "1.5K"). +func FormatMemSize(n int) string { + units := []struct { + suffix string + size int + }{ + {"T", 1024 * 1024 * 1024 * 1024}, + {"G", 1024 * 1024 * 1024}, + {"M", 1024 * 1024}, + {"K", 1024}, + } + for _, u := range units { + if n >= u.size { + v := float64(n) / float64(u.size) + if v == float64(int(v)) { + return fmt.Sprintf("%d%s", int(v), u.suffix) + } + return fmt.Sprintf("%.1f%s", v, u.suffix) + } + } + return fmt.Sprintf("%dB", n) +} From 1e1f575d1c14f57fec30ed7331ff7a4c16548646 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 13 Mar 2026 15:07:31 +0100 Subject: [PATCH 3/5] refactor: replace single batch size with min/max bounds and memory limits Introduce separate _BatchSize (min) and _BatchSizeMax (max) constants to replace the single _BatchSize variable. Update RebatchBySize to accept both maxBytes and maxCount parameters, flushing when either limit is exceeded. Set default batch size min to 1, max to 2000, and memory limit to 128 MB. Update CLI options and sequence_reader.go accordingly. --- pkg/obidefault/batch.go | 19 ++++++++++++++++-- pkg/obiiter/batchiterator.go | 23 +++++++++++++--------- pkg/obioptions/options.go | 8 ++++++-- pkg/obitools/obiconvert/sequence_reader.go | 4 +--- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/pkg/obidefault/batch.go b/pkg/obidefault/batch.go index d2cc10b..83e9d3a 100644 --- a/pkg/obidefault/batch.go +++ b/pkg/obidefault/batch.go @@ -1,6 +1,12 @@ package obidefault -var _BatchSize = 2000 +// _BatchSize is the minimum number of sequences per batch (floor). +// Used as the minSeqs argument to RebatchBySize. +var _BatchSize = 1 + +// _BatchSizeMax is the maximum number of sequences per batch (ceiling). +// A batch is flushed when this count is reached regardless of memory usage. +var _BatchSizeMax = 2000 // SetBatchSize sets the size of the sequence batches. // @@ -25,10 +31,19 @@ func BatchSizePtr() *int { return &_BatchSize } +// BatchSizeMax returns the maximum number of sequences per batch. +func BatchSizeMax() int { + return _BatchSizeMax +} + +func BatchSizeMaxPtr() *int { + return &_BatchSizeMax +} + // _BatchMem holds the maximum cumulative memory (in bytes) per batch when // memory-based batching is requested. A value of 0 disables memory-based // batching and falls back to count-based batching. -var _BatchMem = 0 +var _BatchMem = 128 * 1024 * 1024 // 128 MB default; set to 0 to disable var _BatchMemStr = "" // SetBatchMem sets the memory budget per batch in bytes. diff --git a/pkg/obiiter/batchiterator.go b/pkg/obiiter/batchiterator.go index 9e25d65..101717c 100644 --- a/pkg/obiiter/batchiterator.go +++ b/pkg/obiiter/batchiterator.go @@ -444,12 +444,17 @@ func (iterator IBioSequence) Rebatch(size int) IBioSequence { return newIter } -// RebatchBySize reorganises the stream into batches whose cumulative estimated -// memory footprint does not exceed maxBytes. A single sequence larger than -// maxBytes is emitted alone rather than dropped. minSeqs sets a lower bound on -// batch size (in number of sequences) so that very large sequences still form -// reasonably-sized work units; use 1 to disable. -func (iterator IBioSequence) RebatchBySize(maxBytes int, minSeqs int) IBioSequence { +// RebatchBySize reorganises the stream into batches bounded by two independent +// upper limits: maxCount (max number of sequences) and maxBytes (max cumulative +// estimated memory). A batch is flushed as soon as either limit would be +// exceeded. A single sequence larger than maxBytes is always emitted alone. +// Passing 0 for a limit disables that constraint; if both are 0 it falls back +// to Rebatch(obidefault.BatchSizeMax()). +func (iterator IBioSequence) RebatchBySize(maxBytes int, maxCount int) IBioSequence { + if maxBytes <= 0 && maxCount <= 0 { + return iterator.Rebatch(obidefault.BatchSizeMax()) + } + newIter := MakeIBioSequence() newIter.Add(1) @@ -479,9 +484,9 @@ func (iterator IBioSequence) RebatchBySize(maxBytes int, minSeqs int) IBioSequen source = seqs.Source() for _, s := range seqs.Slice() { sz := s.MemorySize() - // flush before adding if it would overflow, but only if - // we already meet the minimum sequence count - if bufBytes+sz > maxBytes && len(buffer) >= minSeqs { + countFull := maxCount > 0 && len(buffer) >= maxCount + memFull := maxBytes > 0 && bufBytes+sz > maxBytes && len(buffer) > 0 + if countFull || memFull { flush() } buffer = append(buffer, s) diff --git a/pkg/obioptions/options.go b/pkg/obioptions/options.go index 5a9878d..5ebb471 100644 --- a/pkg/obioptions/options.go +++ b/pkg/obioptions/options.go @@ -56,11 +56,15 @@ func RegisterGlobalOptions(options *getoptions.GetOpt) { options.IntVar(obidefault.BatchSizePtr(), "batch-size", obidefault.BatchSize(), options.GetEnv("OBIBATCHSIZE"), - options.Description("Number of sequence per batch for paralelle processing")) + options.Description("Minimum number of sequences per batch (floor, default 1)")) + + options.IntVar(obidefault.BatchSizeMaxPtr(), "batch-size-max", obidefault.BatchSizeMax(), + options.GetEnv("OBIBATCHSIZEMAX"), + options.Description("Maximum number of sequences per batch (ceiling, default 2000)")) options.StringVar(obidefault.BatchMemStrPtr(), "batch-mem", "", options.GetEnv("OBIBATCHMEM"), - options.Description("Maximum memory per batch (e.g. 128K, 64M, 1G). Overrides --batch-size when set.")) + options.Description("Maximum memory per batch (e.g. 128K, 64M, 1G; default: 128M). Set to 0 to disable.")) options.Bool("solexa", false, options.GetEnv("OBISOLEXA"), diff --git a/pkg/obitools/obiconvert/sequence_reader.go b/pkg/obitools/obiconvert/sequence_reader.go index a5e27bc..cbda802 100644 --- a/pkg/obitools/obiconvert/sequence_reader.go +++ b/pkg/obitools/obiconvert/sequence_reader.go @@ -214,9 +214,7 @@ func CLIReadBioSequences(filenames ...string) (obiiter.IBioSequence, error) { iterator = iterator.Speed("Reading sequences") - if obidefault.BatchMem() > 0 { - iterator = iterator.RebatchBySize(obidefault.BatchMem(), 1) - } + iterator = iterator.RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax()) return iterator, nil } From c188580aac8d6aef312df1d736c0d383fdae07df Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 13 Mar 2026 15:13:56 +0100 Subject: [PATCH 4/5] Replace Rebatch with RebatchBySize using default batch parameters Replace calls to Rebatch(size) with RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax()) in batchiterator.go, fragment.go, and obirefidx.go to ensure consistent use of default memory and size limits for batch rebatching. --- pkg/obiiter/batchiterator.go | 4 ++-- pkg/obiiter/fragment.go | 3 ++- pkg/obitools/obirefidx/obirefidx.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/obiiter/batchiterator.go b/pkg/obiiter/batchiterator.go index 101717c..e7d51a1 100644 --- a/pkg/obiiter/batchiterator.go +++ b/pkg/obiiter/batchiterator.go @@ -699,7 +699,7 @@ func (iterator IBioSequence) FilterOn(predicate obiseq.SequencePredicate, trueIter.MarkAsPaired() } - return trueIter.Rebatch(size) + return trueIter.RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax()) } func (iterator IBioSequence) FilterAnd(predicate obiseq.SequencePredicate, @@ -755,7 +755,7 @@ func (iterator IBioSequence) FilterAnd(predicate obiseq.SequencePredicate, trueIter.MarkAsPaired() } - return trueIter.Rebatch(size) + return trueIter.RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax()) } // Load all sequences availables from an IBioSequenceBatch iterator into diff --git a/pkg/obiiter/fragment.go b/pkg/obiiter/fragment.go index 7e2fd1b..f1b0703 100644 --- a/pkg/obiiter/fragment.go +++ b/pkg/obiiter/fragment.go @@ -3,6 +3,7 @@ package obiiter import ( log "github.com/sirupsen/logrus" + "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault" "git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq" ) @@ -70,7 +71,7 @@ func IFragments(minsize, length, overlap, size, nworkers int) Pipeable { } go f(iterator) - return newiter.SortBatches().Rebatch(size) + return newiter.SortBatches().RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax()) } return ifrg diff --git a/pkg/obitools/obirefidx/obirefidx.go b/pkg/obitools/obirefidx/obirefidx.go index fa29d29..a146fc6 100644 --- a/pkg/obitools/obirefidx/obirefidx.go +++ b/pkg/obitools/obirefidx/obirefidx.go @@ -291,5 +291,5 @@ func IndexReferenceDB(iterator obiiter.IBioSequence) obiiter.IBioSequence { go f() } - return indexed.Rebatch(obidefault.BatchSize()) + return indexed.RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax()) } From 94b08870695627f3d16bef0437c06528b7f1bf35 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 13 Mar 2026 15:16:41 +0100 Subject: [PATCH 5/5] Memory-aware Batching and Static Linux Builds ### Memory-Aware Batching - Replaced single batch size limits with configurable min/max bounds and memory limits for more precise control over resource usage. - Added `--batch-mem` CLI option to enable adaptive batching based on estimated sequence memory footprint (e.g., 128K, 64M, 1G). - Introduced `RebatchBySize()` with explicit support for both byte and count limits, flushing when either threshold is exceeded. - Implemented conservative memory estimation via `BioSequence.MemorySize()` and enhanced garbage collection to trigger explicit cleanup after large batch discards. - Updated internal batching logic across `batchiterator.go`, `fragment.go`, and `obirefidx.go` to consistently use default memory (128 MB) and size (min: 1, max: 2000) bounds. ### Linux Build Enhancements - Enabled static linking for Linux binaries using musl, producing portable, self-contained executables without external dependencies. ### Notes - This release consolidates and improves batching behavior introduced in 4.4.20, with no breaking changes to the public API. - All user-facing batching behavior is now governed by consistent memory and count constraints, improving predictability and stability during large dataset processing. --- pkg/obioptions/version.go | 2 +- version.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/obioptions/version.go b/pkg/obioptions/version.go index 7f851b0..89279ab 100644 --- a/pkg/obioptions/version.go +++ b/pkg/obioptions/version.go @@ -3,7 +3,7 @@ package obioptions // Version is automatically updated by the Makefile from version.txt // The patch number (third digit) is incremented on each push to the repository -var _Version = "Release 4.4.21" +var _Version = "Release 4.4.22" // Version returns the version of the obitools package. // diff --git a/version.txt b/version.txt index 711adc7..9ed60f8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.4.21 +4.4.22