From 89600009999480b4b9449e4eab993549660df8bf Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Fri, 17 Jul 2026 08:26:15 +0000
Subject: [PATCH 1/2] docs: publish reproducible performance benchmarks
---
.github/workflows/test.yml | 5 +
README.md | 23 +-
docs/readme/benchmarks.go | 478 +++++++++++++++++++++++++++++++++
docs/readme/benchmarks.txt | 108 ++++++++
docs/readme/benchmarks_test.go | 244 +++++++++++++++++
docs/readme/main.go | 73 ++++-
string_benchmark_test.go | 162 +++++++++--
7 files changed, 1063 insertions(+), 30 deletions(-)
create mode 100644 docs/readme/benchmarks.go
create mode 100644 docs/readme/benchmarks.txt
create mode 100644 docs/readme/benchmarks_test.go
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8ad57d8..c5b9d4b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -43,6 +43,11 @@ jobs:
go -C docs test -race ./...
go -C examples test ./...
+ - name: Verify generated README sections
+ run: |
+ go -C docs run ./readme
+ git diff --exit-code -- README.md docs/readme/benchmarks.txt
+
- name: Require full library coverage
run: test "$(go tool cover -func=coverage.txt | awk '/^total:/ {print $3}')" = "100.0%"
diff --git a/README.md b/README.md
index c276bfb..15a3eaf 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -129,6 +129,27 @@ filename := exportFilename("Q3 Sales & Returns — North America")
`str` uses the standard library underneath and has no dependencies. Use whichever version makes the rules easiest to see.
+
+
+## Performance
+
+These comparisons measure equivalent standard-library and `str` operations. Each cell reports the median of 10 samples as `ns/op · B/op · allocs/op`.
+
+Recorded with `go1.26.1` on `linux/arm64` using `-cpu=1` (`GOMAXPROCS=1`).
+
+| Workload | Standard library | `str` chain |
+| --- | ---: | ---: |
+| Trim | 3.2 ns/op · 0 B/op · 0 allocs/op | 3.3 ns/op · 0 B/op · 0 allocs/op |
+| ToLower | 76.9 ns/op · 48 B/op · 1 allocs/op | 77.3 ns/op · 48 B/op · 1 allocs/op |
+| NormalizeSpace (Fields + Join) | 190.6 ns/op · 208 B/op · 2 allocs/op | 176.1 ns/op · 80 B/op · 1 allocs/op |
+| Trim → ToLower | 67.4 ns/op · 32 B/op · 1 allocs/op | 69.8 ns/op · 32 B/op · 1 allocs/op |
+| ReplaceAll × 3 | 130.4 ns/op · 96 B/op · 3 allocs/op | 128.8 ns/op · 96 B/op · 3 allocs/op |
+
+Timing is machine-specific; use it to understand the scale of these operations, not as a universal speed claim. Treat small timing differences within the raw sample spread as noise. Allocation counts are less sensitive to machine speed and show how much heap work each composition performs. In these workloads, wrapping and unwrapping added no heap allocations; allocations came from transformations that produced new text. `NormalizeSpace` is algorithmically different: the standard-library composition builds a field slice before joining it, while `str` uses one builder pass.
+
+The [benchmark source](string_benchmark_test.go) and [committed raw output](docs/readme/benchmarks.txt) record exactly what ran, including the Go version and command. Refresh the measurements explicitly with `go -C docs run ./readme -record-benchmarks`; ordinary README generation only renders that frozen snapshot.
+
+
## API index
diff --git a/docs/readme/benchmarks.go b/docs/readme/benchmarks.go
new file mode 100644
index 0000000..6e2c5b4
--- /dev/null
+++ b/docs/readme/benchmarks.go
@@ -0,0 +1,478 @@
+package main
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "math"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+const (
+ benchmarkSnapshotPath = "docs/readme/benchmarks.txt"
+ benchmarkSampleCount = 10
+ benchmarkVersionPrefix = "# Go version: "
+ benchmarkCommandPrefix = "# Command: "
+)
+
+var (
+ benchmarkDefinitions = []benchmarkDefinition{
+ {name: "TrimComparison", label: "Trim"},
+ {name: "ToLowerComparison", label: "ToLower"},
+ {name: "NormalizeSpaceComparison", label: "NormalizeSpace (Fields + Join)"},
+ {name: "NormalizationPipeline", label: "Trim → ToLower"},
+ {name: "ReplaceAllPipeline", label: "ReplaceAll × 3"},
+ }
+ benchmarkImplementations = []string{"StandardLibrary", "Fluent"}
+ benchmarkNamePattern = regexp.MustCompile(`^Benchmark([^/]+)/(StandardLibrary|Fluent)(?:-([0-9]+))?$`)
+)
+
+// benchmarkDefinition fixes the reader-facing order and label for a comparison workload.
+type benchmarkDefinition struct {
+ name string
+ label string
+}
+
+// benchmarkSnapshot contains validated metadata and results from one benchmark recording.
+type benchmarkSnapshot struct {
+ goVersion string
+ command string
+ goos string
+ goarch string
+ results []benchmarkResult
+}
+
+// benchmarkResult contains one workload's standard-library and fluent measurements.
+type benchmarkResult struct {
+ name string
+ label string
+ standardLibrary benchmarkMeasurement
+ fluent benchmarkMeasurement
+}
+
+// benchmarkMeasurement contains the median timing and stable allocation metrics for one implementation.
+type benchmarkMeasurement struct {
+ nanoseconds float64
+ bytes uint64
+ allocations uint64
+ samples int
+}
+
+// benchmarkSample contains one raw benchmark output line before repeated samples are summarized.
+type benchmarkSample struct {
+ nanoseconds float64
+ bytes uint64
+ allocations uint64
+ cpuSuffix string
+}
+
+// benchmarkCommandArgs returns the exact argument vector used to record comparisons.
+func benchmarkCommandArgs() []string {
+ names := make([]string, 0, len(benchmarkDefinitions))
+ for _, definition := range benchmarkDefinitions {
+ names = append(names, "Benchmark"+definition.name)
+ }
+
+ return []string{
+ "test",
+ "-run", "^$",
+ "-bench", "^(" + strings.Join(names, "|") + ")$",
+ "-benchmem",
+ "-count=" + strconv.Itoa(benchmarkSampleCount),
+ "-benchtime=500ms",
+ "-cpu=1",
+ ".",
+ }
+}
+
+// benchmarkCommandDisplay returns a copyable shell representation of the recording command.
+func benchmarkCommandDisplay() string {
+ arguments := benchmarkCommandArgs()
+ quoted := make([]string, 0, len(arguments)+1)
+ quoted = append(quoted, "go")
+ for _, argument := range arguments {
+ if strings.ContainsAny(argument, "$()|*?[]{} ") {
+ quoted = append(quoted, "'"+strings.ReplaceAll(argument, "'", `'\''`)+"'")
+ continue
+ }
+ quoted = append(quoted, argument)
+ }
+
+ return strings.Join(quoted, " ")
+}
+
+// recordBenchmarkSnapshot runs only the documented comparisons and combines their raw output with reproduction metadata.
+func recordBenchmarkSnapshot(root string) ([]byte, error) {
+ versionCommand := exec.Command("go", "version")
+ versionOutput, err := versionCommand.CombinedOutput()
+ if err != nil {
+ return nil, fmt.Errorf("go version failed: %w\n%s", err, versionOutput)
+ }
+
+ arguments := benchmarkCommandArgs()
+ command := exec.Command("go", arguments...)
+ command.Dir = root
+ output, err := command.CombinedOutput()
+ if err != nil {
+ return nil, fmt.Errorf("%s failed: %w\n%s", benchmarkCommandDisplay(), err, output)
+ }
+
+ var snapshot bytes.Buffer
+ fmt.Fprintf(&snapshot, "%s%s\n", benchmarkVersionPrefix, strings.TrimSpace(string(versionOutput)))
+ fmt.Fprintf(&snapshot, "%s%s\n\n", benchmarkCommandPrefix, benchmarkCommandDisplay())
+ snapshot.Write(bytes.TrimSpace(output))
+ snapshot.WriteByte('\n')
+
+ if _, err := parseBenchmarkSnapshot(snapshot.Bytes()); err != nil {
+ return nil, fmt.Errorf("validate recorded output: %w", err)
+ }
+
+ return snapshot.Bytes(), nil
+}
+
+// parseBenchmarkSnapshot validates metadata, ordering, samples, and metrics before producing README results.
+func parseBenchmarkSnapshot(snapshot []byte) (benchmarkSnapshot, error) {
+ metadata, output, err := splitBenchmarkSnapshot(snapshot)
+ if err != nil {
+ return benchmarkSnapshot{}, err
+ }
+
+ goVersion := strings.TrimPrefix(metadata[0], benchmarkVersionPrefix)
+ if !strings.HasPrefix(goVersion, "go version go") {
+ return benchmarkSnapshot{}, fmt.Errorf("invalid Go version metadata %q", goVersion)
+ }
+
+ command := strings.TrimPrefix(metadata[1], benchmarkCommandPrefix)
+ if command != benchmarkCommandDisplay() {
+ return benchmarkSnapshot{}, fmt.Errorf("benchmark command = %q, want %q", command, benchmarkCommandDisplay())
+ }
+
+ samples := make(map[string]map[string][]benchmarkSample, len(benchmarkDefinitions))
+ for _, definition := range benchmarkDefinitions {
+ samples[definition.name] = make(map[string][]benchmarkSample, len(benchmarkImplementations))
+ }
+
+ goos, goarch, err := parseBenchmarkEnvironment(output)
+ if err != nil {
+ return benchmarkSnapshot{}, err
+ }
+
+ lastOrder := -1
+ cpuSuffix := ""
+ suffixInitialized := false
+ for _, line := range strings.Split(output, "\n") {
+ fields := strings.Fields(line)
+ if len(fields) == 0 || !strings.HasPrefix(fields[0], "Benchmark") {
+ continue
+ }
+
+ name, implementation, sample, err := parseBenchmarkLine(fields)
+ if err != nil {
+ return benchmarkSnapshot{}, err
+ }
+ order, ok := benchmarkOrder(name, implementation)
+ if !ok {
+ return benchmarkSnapshot{}, fmt.Errorf("unexpected benchmark %s/%s", name, implementation)
+ }
+ if order < lastOrder {
+ return benchmarkSnapshot{}, fmt.Errorf("benchmark %s/%s is out of order", name, implementation)
+ }
+ lastOrder = order
+
+ if !suffixInitialized {
+ cpuSuffix = sample.cpuSuffix
+ suffixInitialized = true
+ }
+ if sample.cpuSuffix != cpuSuffix {
+ return benchmarkSnapshot{}, fmt.Errorf("benchmark name suffix changed from %q to %q", cpuSuffix, sample.cpuSuffix)
+ }
+ samples[name][implementation] = append(samples[name][implementation], sample)
+ }
+
+ if !suffixInitialized {
+ return benchmarkSnapshot{}, errors.New("snapshot contains no comparison benchmarks")
+ }
+
+ results := make([]benchmarkResult, 0, len(benchmarkDefinitions))
+ for _, definition := range benchmarkDefinitions {
+ standardSamples := samples[definition.name][benchmarkImplementations[0]]
+ fluentSamples := samples[definition.name][benchmarkImplementations[1]]
+ if len(standardSamples) == 0 || len(fluentSamples) == 0 {
+ return benchmarkSnapshot{}, fmt.Errorf("benchmark %s must contain StandardLibrary and Fluent samples", definition.name)
+ }
+ if len(standardSamples) != len(fluentSamples) {
+ return benchmarkSnapshot{}, fmt.Errorf("benchmark %s sample counts differ: StandardLibrary=%d Fluent=%d", definition.name, len(standardSamples), len(fluentSamples))
+ }
+ if len(standardSamples) != benchmarkSampleCount {
+ return benchmarkSnapshot{}, fmt.Errorf("benchmark %s has %d samples per implementation, want %d", definition.name, len(standardSamples), benchmarkSampleCount)
+ }
+
+ standardLibrary, err := summarizeBenchmarkSamples(definition.name, benchmarkImplementations[0], standardSamples)
+ if err != nil {
+ return benchmarkSnapshot{}, err
+ }
+ fluent, err := summarizeBenchmarkSamples(definition.name, benchmarkImplementations[1], fluentSamples)
+ if err != nil {
+ return benchmarkSnapshot{}, err
+ }
+
+ results = append(results, benchmarkResult{
+ name: definition.name,
+ label: definition.label,
+ standardLibrary: standardLibrary,
+ fluent: fluent,
+ })
+ }
+
+ return benchmarkSnapshot{
+ goVersion: goVersion,
+ command: command,
+ goos: goos,
+ goarch: goarch,
+ results: results,
+ }, nil
+}
+
+// parseBenchmarkEnvironment extracts the target operating system and architecture from raw go test output.
+func parseBenchmarkEnvironment(output string) (string, string, error) {
+ values := map[string]string{}
+ for _, line := range strings.Split(output, "\n") {
+ for _, key := range []string{"goos", "goarch"} {
+ prefix := key + ": "
+ if !strings.HasPrefix(line, prefix) {
+ continue
+ }
+ if values[key] != "" {
+ return "", "", fmt.Errorf("benchmark output contains repeated %s metadata", key)
+ }
+ values[key] = strings.TrimSpace(strings.TrimPrefix(line, prefix))
+ }
+ }
+ if values["goos"] == "" || values["goarch"] == "" {
+ return "", "", errors.New("benchmark output must contain goos and goarch metadata")
+ }
+
+ return values["goos"], values["goarch"], nil
+}
+
+// splitBenchmarkSnapshot separates the two required metadata lines from raw go test output.
+func splitBenchmarkSnapshot(snapshot []byte) ([2]string, string, error) {
+ normalized := strings.ReplaceAll(string(snapshot), "\r\n", "\n")
+ parts := strings.SplitN(normalized, "\n\n", 2)
+ if len(parts) != 2 {
+ return [2]string{}, "", errors.New("snapshot must separate metadata from benchmark output with a blank line")
+ }
+
+ metadata := strings.Split(parts[0], "\n")
+ if len(metadata) != 2 {
+ return [2]string{}, "", fmt.Errorf("snapshot has %d metadata lines, want 2", len(metadata))
+ }
+ if !strings.HasPrefix(metadata[0], benchmarkVersionPrefix) {
+ return [2]string{}, "", fmt.Errorf("snapshot first line must start with %q", benchmarkVersionPrefix)
+ }
+ if !strings.HasPrefix(metadata[1], benchmarkCommandPrefix) {
+ return [2]string{}, "", fmt.Errorf("snapshot second line must start with %q", benchmarkCommandPrefix)
+ }
+ if strings.TrimSpace(parts[1]) == "" {
+ return [2]string{}, "", errors.New("snapshot benchmark output is empty")
+ }
+
+ return [2]string{metadata[0], metadata[1]}, parts[1], nil
+}
+
+// parseBenchmarkLine extracts the comparison name, implementation, CPU suffix, and benchmark metrics.
+func parseBenchmarkLine(fields []string) (string, string, benchmarkSample, error) {
+ match := benchmarkNamePattern.FindStringSubmatch(fields[0])
+ if match == nil {
+ return "", "", benchmarkSample{}, fmt.Errorf("invalid benchmark name %q", fields[0])
+ }
+
+ if match[3] != "" {
+ cpuSuffix, err := strconv.Atoi(match[3])
+ if err != nil || cpuSuffix <= 0 {
+ return "", "", benchmarkSample{}, fmt.Errorf("invalid benchmark name suffix in %q", fields[0])
+ }
+ }
+ nanoseconds, err := parseFloatMetric(fields, "ns/op")
+ if err != nil {
+ return "", "", benchmarkSample{}, fmt.Errorf("%s: %w", fields[0], err)
+ }
+ bytesPerOperation, err := parseUintMetric(fields, "B/op")
+ if err != nil {
+ return "", "", benchmarkSample{}, fmt.Errorf("%s: %w", fields[0], err)
+ }
+ allocations, err := parseUintMetric(fields, "allocs/op")
+ if err != nil {
+ return "", "", benchmarkSample{}, fmt.Errorf("%s: %w", fields[0], err)
+ }
+
+ return match[1], match[2], benchmarkSample{
+ nanoseconds: nanoseconds,
+ bytes: bytesPerOperation,
+ allocations: allocations,
+ cpuSuffix: match[3],
+ }, nil
+}
+
+// parseFloatMetric finds a finite non-negative floating-point metric immediately before its unit.
+func parseFloatMetric(fields []string, unit string) (float64, error) {
+ value, err := metricValue(fields, unit)
+ if err != nil {
+ return 0, err
+ }
+
+ parsed, err := strconv.ParseFloat(value, 64)
+ if err != nil || math.IsInf(parsed, 0) || math.IsNaN(parsed) || parsed < 0 {
+ return 0, fmt.Errorf("invalid %s value %q", unit, value)
+ }
+ return parsed, nil
+}
+
+// parseUintMetric finds an unsigned integer metric immediately before its unit.
+func parseUintMetric(fields []string, unit string) (uint64, error) {
+ value, err := metricValue(fields, unit)
+ if err != nil {
+ return 0, err
+ }
+
+ parsed, err := strconv.ParseUint(value, 10, 64)
+ if err != nil {
+ return 0, fmt.Errorf("invalid %s value %q", unit, value)
+ }
+ return parsed, nil
+}
+
+// metricValue returns the field immediately before a requested benchmark unit.
+func metricValue(fields []string, unit string) (string, error) {
+ for index, field := range fields {
+ if field != unit {
+ continue
+ }
+ if index == 0 {
+ break
+ }
+ return fields[index-1], nil
+ }
+ return "", fmt.Errorf("missing %s metric", unit)
+}
+
+// benchmarkOrder maps expected workload and implementation names to their stable output position.
+func benchmarkOrder(name, implementation string) (int, bool) {
+ for workloadIndex, definition := range benchmarkDefinitions {
+ if definition.name != name {
+ continue
+ }
+ for implementationIndex, expected := range benchmarkImplementations {
+ if expected == implementation {
+ return workloadIndex*len(benchmarkImplementations) + implementationIndex, true
+ }
+ }
+ }
+ return 0, false
+}
+
+// summarizeBenchmarkSamples uses the median timing while requiring allocation metrics to remain deterministic.
+func summarizeBenchmarkSamples(name, implementation string, samples []benchmarkSample) (benchmarkMeasurement, error) {
+ timings := make([]float64, len(samples))
+ bytesPerOperation := samples[0].bytes
+ allocations := samples[0].allocations
+ for index, sample := range samples {
+ if sample.bytes != bytesPerOperation || sample.allocations != allocations {
+ return benchmarkMeasurement{}, fmt.Errorf("benchmark %s/%s allocation metrics are unstable", name, implementation)
+ }
+ timings[index] = sample.nanoseconds
+ }
+ sort.Float64s(timings)
+
+ median := timings[len(timings)/2]
+ if len(timings)%2 == 0 {
+ median = (timings[len(timings)/2-1] + median) / 2
+ }
+
+ return benchmarkMeasurement{
+ nanoseconds: median,
+ bytes: bytesPerOperation,
+ allocations: allocations,
+ samples: len(samples),
+ }, nil
+}
+
+// renderPerformance presents absolute benchmark costs without implying that timings transfer across machines.
+func renderPerformance(snapshot benchmarkSnapshot) string {
+ var output strings.Builder
+ output.WriteString("## Performance\n\n")
+ output.WriteString("These comparisons measure equivalent standard-library and `str` operations. Each cell reports the median of ")
+ output.WriteString(strconv.Itoa(benchmarkSampleCount))
+ output.WriteString(" samples as `ns/op · B/op · allocs/op`.\n\n")
+ goVersionFields := strings.Fields(snapshot.goVersion)
+ fmt.Fprintf(
+ &output,
+ "Recorded with `%s` on `%s/%s` using `-cpu=1` (`GOMAXPROCS=1`).\n\n",
+ goVersionFields[2],
+ snapshot.goos,
+ snapshot.goarch,
+ )
+ output.WriteString("| Workload | Standard library | `str` chain |\n")
+ output.WriteString("| --- | ---: | ---: |\n")
+ for _, result := range snapshot.results {
+ fmt.Fprintf(
+ &output,
+ "| %s | %s | %s |\n",
+ result.label,
+ formatBenchmarkMeasurement(result.standardLibrary),
+ formatBenchmarkMeasurement(result.fluent),
+ )
+ }
+
+ output.WriteString("\nTiming is machine-specific; use it to understand the scale of these operations, not as a universal speed claim. Treat small timing differences within the raw sample spread as noise. Allocation counts are less sensitive to machine speed and show how much heap work each composition performs. In these workloads, wrapping and unwrapping added no heap allocations; allocations came from transformations that produced new text. `NormalizeSpace` is algorithmically different: the standard-library composition builds a field slice before joining it, while `str` uses one builder pass.\n\n")
+ output.WriteString("The [benchmark source](string_benchmark_test.go) and [committed raw output](docs/readme/benchmarks.txt) record exactly what ran, including the Go version and command. Refresh the measurements explicitly with `go -C docs run ./readme -record-benchmarks`; ordinary README generation only renders that frozen snapshot.")
+
+ return output.String()
+}
+
+// formatBenchmarkMeasurement formats one absolute result at stable one-decimal timing precision.
+func formatBenchmarkMeasurement(measurement benchmarkMeasurement) string {
+ return fmt.Sprintf(
+ "%s ns/op · %d B/op · %d allocs/op",
+ strconv.FormatFloat(measurement.nanoseconds, 'f', 1, 64),
+ measurement.bytes,
+ measurement.allocations,
+ )
+}
+
+// atomicWriteFile replaces a generated file only after its complete content is durable in the same directory.
+func atomicWriteFile(path string, content []byte, mode os.FileMode) error {
+ temporary, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+"-*")
+ if err != nil {
+ return err
+ }
+ temporaryPath := temporary.Name()
+ defer func() {
+ _ = temporary.Close()
+ _ = os.Remove(temporaryPath)
+ }()
+
+ if err := temporary.Chmod(mode); err != nil {
+ return err
+ }
+ if _, err := temporary.Write(content); err != nil {
+ return err
+ }
+ if err := temporary.Sync(); err != nil {
+ return err
+ }
+ if err := temporary.Close(); err != nil {
+ return err
+ }
+ if err := os.Rename(temporaryPath, path); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/docs/readme/benchmarks.txt b/docs/readme/benchmarks.txt
new file mode 100644
index 0000000..fa73779
--- /dev/null
+++ b/docs/readme/benchmarks.txt
@@ -0,0 +1,108 @@
+# Go version: go version go1.26.1 linux/arm64
+# Command: go test -run '^$' -bench '^(BenchmarkTrimComparison|BenchmarkToLowerComparison|BenchmarkNormalizeSpaceComparison|BenchmarkNormalizationPipeline|BenchmarkReplaceAllPipeline)$' -benchmem -count=10 -benchtime=500ms -cpu=1 .
+
+goos: linux
+goarch: arm64
+pkg: github.com/goforj/str/v2
+BenchmarkTrimComparison/StandardLibrary 183970962 3.244 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 185686015 3.213 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 184220110 3.226 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 181069550 3.237 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 191199663 3.190 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 190655265 3.189 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 175977206 3.367 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 185205409 3.318 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 182208324 3.284 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 186352513 3.241 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 185337969 3.247 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 185420518 3.287 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 186639068 3.238 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 183801771 3.256 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 189553234 3.201 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 187122338 3.211 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 183919126 3.256 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 178826119 3.272 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 181908288 3.270 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 182728324 3.282 ns/op 0 B/op 0 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 8041405 76.92 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 7931132 76.98 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 7868384 76.59 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 7887022 76.97 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 7546566 78.40 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 7674072 77.94 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 7786072 78.60 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 7817716 76.84 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 7770988 76.60 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 8069696 74.62 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 7849126 77.42 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 7456654 79.39 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 8053372 76.04 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 8020948 77.46 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 7775091 77.58 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 7889784 76.95 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 7738846 77.21 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 7797414 76.75 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 7693078 76.51 ns/op 48 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 7728895 78.53 ns/op 48 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3176962 190.4 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3143799 193.6 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 2919790 198.3 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3130888 191.7 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3177224 190.7 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3124177 191.0 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3179091 188.7 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3186408 189.5 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3120853 189.6 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3259677 185.4 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3383655 182.3 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3384769 176.1 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3409942 174.2 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3466602 174.0 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3410059 174.7 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3457311 173.6 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3412810 176.3 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3397281 176.7 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3378294 177.1 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3376244 176.2 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8918898 67.27 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 9043378 67.28 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 9112165 67.45 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8910421 67.26 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 9013908 68.23 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8858690 68.74 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 9035310 67.55 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 9113894 67.73 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8908100 66.89 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8958230 67.12 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8899297 66.80 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8982628 69.32 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8692294 67.97 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 9089571 69.55 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8607890 68.25 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8398479 70.31 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8431693 71.25 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8879492 71.28 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8424811 70.14 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8640062 69.95 ns/op 32 B/op 1 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4530718 130.6 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4692512 132.3 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4490119 130.0 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4685865 128.7 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4287490 130.2 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4682410 129.0 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4555342 130.6 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4661804 130.2 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4642352 132.0 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4593618 131.5 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4685128 128.8 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4632000 131.4 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4624298 134.5 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4508350 132.5 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4687993 129.0 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4689482 126.1 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4723741 128.1 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4697964 128.8 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4568785 128.5 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4660621 126.4 ns/op 96 B/op 3 allocs/op
+PASS
+ok github.com/goforj/str/v2 60.210s
diff --git a/docs/readme/benchmarks_test.go b/docs/readme/benchmarks_test.go
new file mode 100644
index 0000000..257290e
--- /dev/null
+++ b/docs/readme/benchmarks_test.go
@@ -0,0 +1,244 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// TestParseBenchmarkSnapshot verifies CPU-suffixed benchmark names, median timings, and stable allocation metrics.
+func TestParseBenchmarkSnapshot(t *testing.T) {
+ t.Parallel()
+
+ snapshot, err := parseBenchmarkSnapshot(benchmarkFixture())
+ if err != nil {
+ t.Fatalf("parseBenchmarkSnapshot() error = %v", err)
+ }
+ if snapshot.goVersion != "go version go1.24.5 linux/arm64" {
+ t.Fatalf("goVersion = %q", snapshot.goVersion)
+ }
+ if snapshot.command != benchmarkCommandDisplay() {
+ t.Fatalf("command = %q, want %q", snapshot.command, benchmarkCommandDisplay())
+ }
+ if snapshot.goos != "linux" || snapshot.goarch != "arm64" {
+ t.Fatalf("platform = %s/%s, want linux/arm64", snapshot.goos, snapshot.goarch)
+ }
+ if len(snapshot.results) != len(benchmarkDefinitions) {
+ t.Fatalf("len(results) = %d, want %d", len(snapshot.results), len(benchmarkDefinitions))
+ }
+
+ first := snapshot.results[0]
+ if first.name != "TrimComparison" || first.label != "Trim" {
+ t.Fatalf("first result = %#v", first)
+ }
+ if first.standardLibrary.nanoseconds != 15.5 || first.standardLibrary.bytes != 16 || first.standardLibrary.allocations != 1 {
+ t.Fatalf("standard-library measurement = %#v", first.standardLibrary)
+ }
+ if first.fluent.nanoseconds != 25.5 || first.fluent.bytes != 32 || first.fluent.allocations != 2 {
+ t.Fatalf("fluent measurement = %#v", first.fluent)
+ }
+ if first.standardLibrary.samples != benchmarkSampleCount || first.fluent.samples != benchmarkSampleCount {
+ t.Fatalf("sample counts = %d and %d", first.standardLibrary.samples, first.fluent.samples)
+ }
+
+ withoutSuffix := strings.ReplaceAll(string(benchmarkFixture()), "-1\t", "\t")
+ if _, err := parseBenchmarkSnapshot([]byte(withoutSuffix)); err != nil {
+ t.Fatalf("parseBenchmarkSnapshot() without name suffix error = %v", err)
+ }
+}
+
+// TestParseBenchmarkSnapshotRejectsInvalidComparisons verifies that incomplete or unstable data cannot enter the README.
+func TestParseBenchmarkSnapshotRejectsInvalidComparisons(t *testing.T) {
+ t.Parallel()
+
+ valid := string(benchmarkFixture())
+ tests := []struct {
+ name string
+ snapshot string
+ want string
+ }{
+ {
+ name: "missing pair",
+ snapshot: removeLinesContaining(valid, "BenchmarkToLowerComparison/Fluent-1"),
+ want: "must contain StandardLibrary and Fluent",
+ },
+ {
+ name: "unequal samples",
+ snapshot: strings.Replace(valid, "BenchmarkTrimComparison/Fluent-1\t1000\t30 ns/op\t32 B/op\t2 allocs/op\n", "", 1),
+ want: "sample counts differ",
+ },
+ {
+ name: "unstable allocations",
+ snapshot: strings.Replace(valid, "BenchmarkTrimComparison/StandardLibrary-1\t1000\t20 ns/op\t16 B/op\t1 allocs/op", "BenchmarkTrimComparison/StandardLibrary-1\t1000\t20 ns/op\t17 B/op\t1 allocs/op", 1),
+ want: "allocation metrics are unstable",
+ },
+ {
+ name: "changed name suffix",
+ snapshot: strings.Replace(valid, "BenchmarkTrimComparison/StandardLibrary-1", "BenchmarkTrimComparison/StandardLibrary-2", 1),
+ want: "name suffix changed",
+ },
+ {
+ name: "workload order",
+ snapshot: strings.Replace(
+ valid,
+ "BenchmarkToLowerComparison/StandardLibrary-1",
+ "BenchmarkTrimComparison/StandardLibrary-1",
+ 1,
+ ),
+ want: "out of order",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ _, err := parseBenchmarkSnapshot([]byte(test.snapshot))
+ if err == nil {
+ t.Fatal("parseBenchmarkSnapshot() error = nil, want validation error")
+ }
+ if !strings.Contains(err.Error(), test.want) {
+ t.Fatalf("parseBenchmarkSnapshot() error = %q, want text %q", err, test.want)
+ }
+ })
+ }
+}
+
+// TestRenderPerformance verifies absolute metrics, stable workload order, and interpretation guidance.
+func TestRenderPerformance(t *testing.T) {
+ t.Parallel()
+
+ snapshot, err := parseBenchmarkSnapshot(benchmarkFixture())
+ if err != nil {
+ t.Fatalf("parseBenchmarkSnapshot() error = %v", err)
+ }
+
+ got := renderPerformance(snapshot)
+ wantRows := []string{
+ "| Trim | 15.5 ns/op · 16 B/op · 1 allocs/op | 25.5 ns/op · 32 B/op · 2 allocs/op |",
+ "| ToLower | 115.5 ns/op · 32 B/op · 2 allocs/op | 125.5 ns/op · 64 B/op · 3 allocs/op |",
+ "| NormalizeSpace (Fields + Join) | 215.5 ns/op · 48 B/op · 3 allocs/op | 225.5 ns/op · 96 B/op · 4 allocs/op |",
+ "| Trim → ToLower | 315.5 ns/op · 64 B/op · 4 allocs/op | 325.5 ns/op · 128 B/op · 5 allocs/op |",
+ "| ReplaceAll × 3 | 415.5 ns/op · 80 B/op · 5 allocs/op | 425.5 ns/op · 160 B/op · 6 allocs/op |",
+ }
+ lastPosition := -1
+ for _, row := range wantRows {
+ position := strings.Index(got, row)
+ if position < 0 {
+ t.Fatalf("renderPerformance() missing row %q\n%s", row, got)
+ }
+ if position <= lastPosition {
+ t.Fatalf("renderPerformance() row %q is out of order", row)
+ }
+ lastPosition = position
+ }
+ for _, phrase := range []string{"Recorded with `go1.24.5` on `linux/arm64` using `-cpu=1` (`GOMAXPROCS=1`)", "Timing is machine-specific", "Treat small timing differences within the raw sample spread as noise", "wrapping and unwrapping added no heap allocations", "builds a field slice", "ordinary README generation only renders that frozen snapshot"} {
+ if !strings.Contains(got, phrase) {
+ t.Fatalf("renderPerformance() missing guidance %q", phrase)
+ }
+ }
+}
+
+// TestBenchmarkCommandDisplay verifies that the snapshot records a precise, copyable comparison command.
+func TestBenchmarkCommandDisplay(t *testing.T) {
+ t.Parallel()
+
+ want := "go test -run '^$' -bench '^(BenchmarkTrimComparison|BenchmarkToLowerComparison|BenchmarkNormalizeSpaceComparison|BenchmarkNormalizationPipeline|BenchmarkReplaceAllPipeline)$' -benchmem -count=10 -benchtime=500ms -cpu=1 ."
+ if got := benchmarkCommandDisplay(); got != want {
+ t.Fatalf("benchmarkCommandDisplay() = %q, want %q", got, want)
+ }
+}
+
+// TestCommittedBenchmarkSnapshot verifies that the checked-in source for the README remains complete and parseable.
+func TestCommittedBenchmarkSnapshot(t *testing.T) {
+ t.Parallel()
+
+ content, err := os.ReadFile("benchmarks.txt")
+ if err != nil {
+ t.Fatalf("os.ReadFile() error = %v", err)
+ }
+ snapshot, err := parseBenchmarkSnapshot(content)
+ if err != nil {
+ t.Fatalf("parseBenchmarkSnapshot() error = %v", err)
+ }
+ if len(snapshot.results) != len(benchmarkDefinitions) {
+ t.Fatalf("len(results) = %d, want %d", len(snapshot.results), len(benchmarkDefinitions))
+ }
+ for _, result := range snapshot.results {
+ if result.standardLibrary.samples != benchmarkSampleCount || result.fluent.samples != benchmarkSampleCount {
+ t.Fatalf("%s sample counts = %d and %d, want %d each", result.name, result.standardLibrary.samples, result.fluent.samples, benchmarkSampleCount)
+ }
+ }
+}
+
+// TestAtomicWriteFile verifies that generated snapshots replace existing content without leaving temporary files.
+func TestAtomicWriteFile(t *testing.T) {
+ t.Parallel()
+
+ directory := t.TempDir()
+ path := filepath.Join(directory, "benchmarks.txt")
+ if err := os.WriteFile(path, []byte("old"), 0o600); err != nil {
+ t.Fatalf("os.WriteFile() error = %v", err)
+ }
+ if err := atomicWriteFile(path, []byte("new\n"), 0o644); err != nil {
+ t.Fatalf("atomicWriteFile() error = %v", err)
+ }
+ content, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("os.ReadFile() error = %v", err)
+ }
+ if string(content) != "new\n" {
+ t.Fatalf("content = %q, want %q", content, "new\\n")
+ }
+ entries, err := os.ReadDir(directory)
+ if err != nil {
+ t.Fatalf("os.ReadDir() error = %v", err)
+ }
+ if len(entries) != 1 || entries[0].Name() != "benchmarks.txt" {
+ t.Fatalf("directory entries = %#v", entries)
+ }
+}
+
+// benchmarkFixture builds complete deterministic raw output without running performance-sensitive tests.
+func benchmarkFixture() []byte {
+ var output strings.Builder
+ fmt.Fprintf(&output, "%s%s\n", benchmarkVersionPrefix, "go version go1.24.5 linux/arm64")
+ fmt.Fprintf(&output, "%s%s\n\n", benchmarkCommandPrefix, benchmarkCommandDisplay())
+ output.WriteString("goos: linux\ngoarch: arm64\npkg: github.com/goforj/str/v2\ncpu: fixture\n")
+
+ timingOrder := []int{10, 1, 8, 3, 6, 2, 9, 4, 7, 5}
+ for workloadIndex, definition := range benchmarkDefinitions {
+ for implementationIndex, implementation := range benchmarkImplementations {
+ base := workloadIndex*100 + implementationIndex*10 + 10
+ bytesPerOperation := (workloadIndex + 1) * (implementationIndex + 1) * 16
+ allocations := workloadIndex + implementationIndex + 1
+ for _, timing := range timingOrder {
+ fmt.Fprintf(
+ &output,
+ "Benchmark%s/%s-1\t1000\t%d ns/op\t%d B/op\t%d allocs/op\n",
+ definition.name,
+ implementation,
+ base+timing,
+ bytesPerOperation,
+ allocations,
+ )
+ }
+ }
+ }
+ output.WriteString("PASS\nok\tgithub.com/goforj/str/v2\t1.000s\n")
+
+ return []byte(output.String())
+}
+
+// removeLinesContaining removes fixture lines matching text while preserving the rest byte-for-byte.
+func removeLinesContaining(input, text string) string {
+ var output strings.Builder
+ for _, line := range strings.SplitAfter(input, "\n") {
+ if !strings.Contains(line, text) {
+ output.WriteString(line)
+ }
+ }
+ return output.String()
+}
diff --git a/docs/readme/main.go b/docs/readme/main.go
index 9907cfa..d28b70e 100644
--- a/docs/readme/main.go
+++ b/docs/readme/main.go
@@ -5,6 +5,7 @@ import (
"bytes"
"encoding/json"
"errors"
+ "flag"
"fmt"
"go/ast"
"go/parser"
@@ -19,11 +20,13 @@ import (
)
const (
- apiStart = ""
- apiEnd = ""
- testCountStart = ""
- testCountEnd = ""
- documentation = "https://pkg.go.dev/github.com/goforj/str/v2"
+ apiStart = ""
+ apiEnd = ""
+ performanceStart = ""
+ performanceEnd = ""
+ testCountStart = ""
+ testCountEnd = ""
+ documentation = "https://pkg.go.dev/github.com/goforj/str/v2"
)
var (
@@ -49,21 +52,47 @@ type apiExample struct {
// main reports generation errors without a stack trace because this command is intended for routine documentation updates.
func main() {
- if err := run(); err != nil {
+ if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "readme generator:", err)
os.Exit(1)
}
- fmt.Println("README.md API index and test count updated")
+ fmt.Println("README.md performance table, API index, and test count updated")
}
// run computes every generated value before writing so a failed parse or test run cannot partially update README.md.
-func run() error {
+func run(arguments []string) error {
+ flags := flag.NewFlagSet("readme", flag.ContinueOnError)
+ flags.SetOutput(io.Discard)
+ recordBenchmarks := flags.Bool("record-benchmarks", false, "record a fresh benchmark snapshot before rebuilding README.md")
+ if err := flags.Parse(arguments); err != nil {
+ return err
+ }
+ if flags.NArg() != 0 {
+ return fmt.Errorf("unexpected arguments: %s", strings.Join(flags.Args(), " "))
+ }
+
root, err := findRoot()
if err != nil {
return err
}
+ benchmarkPath := filepath.Join(root, benchmarkSnapshotPath)
+ benchmarkSnapshot, err := os.ReadFile(benchmarkPath)
+ if err != nil && !*recordBenchmarks {
+ return fmt.Errorf("read benchmark snapshot: %w", err)
+ }
+ if *recordBenchmarks {
+ benchmarkSnapshot, err = recordBenchmarkSnapshot(root)
+ if err != nil {
+ return fmt.Errorf("record benchmark snapshot: %w", err)
+ }
+ }
+ benchmarks, err := parseBenchmarkSnapshot(benchmarkSnapshot)
+ if err != nil {
+ return fmt.Errorf("parse benchmark snapshot: %w", err)
+ }
+
symbols, err := parseAPISymbols(root)
if err != nil {
return fmt.Errorf("parse API declarations: %w", err)
@@ -77,6 +106,9 @@ func run() error {
if _, _, err := markerBounds(string(readme), apiStart, apiEnd, "API index"); err != nil {
return err
}
+ if _, _, err := markerBounds(string(readme), performanceStart, performanceEnd, "performance"); err != nil {
+ return err
+ }
if _, _, err := markerBounds(string(readme), testCountStart, testCountEnd, "test count"); err != nil {
return err
}
@@ -88,6 +120,17 @@ func run() error {
updated, err := replaceMarkedSection(
string(readme),
+ performanceStart,
+ performanceEnd,
+ "\n\n"+renderPerformance(benchmarks)+"\n",
+ "performance",
+ )
+ if err != nil {
+ return err
+ }
+
+ updated, err = replaceMarkedSection(
+ updated,
apiStart,
apiEnd,
"\n\n"+renderAPI(symbols)+"\n",
@@ -109,12 +152,20 @@ func run() error {
return err
}
- if bytes.Equal(readme, []byte(updated)) {
+ readmeChanged := !bytes.Equal(readme, []byte(updated))
+ if !*recordBenchmarks && !readmeChanged {
return nil
}
- if err := os.WriteFile(readmePath, []byte(updated), 0o644); err != nil {
- return fmt.Errorf("write README.md: %w", err)
+ if *recordBenchmarks {
+ if err := atomicWriteFile(benchmarkPath, benchmarkSnapshot, 0o644); err != nil {
+ return fmt.Errorf("write benchmark snapshot: %w", err)
+ }
+ }
+ if readmeChanged {
+ if err := atomicWriteFile(readmePath, []byte(updated), 0o644); err != nil {
+ return fmt.Errorf("write README.md: %w", err)
+ }
}
return nil
diff --git a/string_benchmark_test.go b/string_benchmark_test.go
index d6d8866..2e5714a 100644
--- a/string_benchmark_test.go
+++ b/string_benchmark_test.go
@@ -8,6 +8,14 @@ import (
var benchmarkStringResult String
var benchmarkRawStringResult string
+const (
+ benchmarkTrimInput = " GoForj builds practical Go applications "
+ benchmarkToLowerInput = "GoForj Builds Practical Go Applications"
+ benchmarkNormalizeSpaceInput = " SELECT users.id, users.email\nFROM users\tWHERE users.status = ? "
+ benchmarkTrimToLowerInput = " AUTH_OAuth_Provider-Name.Test "
+ benchmarkReplaceAllInput = "archive-logs cold.storage"
+)
+
// BenchmarkAppend measures a common multi-part fluent composition.
func BenchmarkAppend(b *testing.B) {
for b.Loop() {
@@ -36,6 +44,34 @@ func BenchmarkTrim(b *testing.B) {
}
}
+// BenchmarkTrimComparison compares fluent trimming with strings.TrimSpace.
+func BenchmarkTrimComparison(b *testing.B) {
+ b.Run("StandardLibrary", func(b *testing.B) {
+ for b.Loop() {
+ benchmarkRawStringResult = benchmarkTrimStandard(benchmarkTrimInput)
+ }
+ })
+ b.Run("Fluent", func(b *testing.B) {
+ for b.Loop() {
+ benchmarkRawStringResult = benchmarkTrimFluent(benchmarkTrimInput)
+ }
+ })
+}
+
+// BenchmarkToLowerComparison compares fluent lowercasing with strings.ToLower.
+func BenchmarkToLowerComparison(b *testing.B) {
+ b.Run("StandardLibrary", func(b *testing.B) {
+ for b.Loop() {
+ benchmarkRawStringResult = benchmarkToLowerStandard(benchmarkToLowerInput)
+ }
+ })
+ b.Run("Fluent", func(b *testing.B) {
+ for b.Loop() {
+ benchmarkRawStringResult = benchmarkToLowerFluent(benchmarkToLowerInput)
+ }
+ })
+}
+
// BenchmarkNormalizeSpace measures Unicode-aware whitespace normalization across representative inputs.
func BenchmarkNormalizeSpace(b *testing.B) {
benchmarks := []struct {
@@ -61,58 +97,148 @@ func BenchmarkNormalizeSpace(b *testing.B) {
// BenchmarkNormalizeSpaceComparison compares fluent normalization with the equivalent standard-library composition.
func BenchmarkNormalizeSpaceComparison(b *testing.B) {
- const value = " SELECT users.id, users.email\nFROM users\tWHERE users.status = ? "
-
b.Run("StandardLibrary", func(b *testing.B) {
for b.Loop() {
- benchmarkRawStringResult = strings.Join(strings.Fields(value), " ")
+ benchmarkRawStringResult = benchmarkNormalizeSpaceStandard(benchmarkNormalizeSpaceInput)
}
})
b.Run("Fluent", func(b *testing.B) {
for b.Loop() {
- benchmarkRawStringResult = Of(value).NormalizeSpace().String()
+ benchmarkRawStringResult = benchmarkNormalizeSpaceFluent(benchmarkNormalizeSpaceInput)
}
})
}
// BenchmarkNormalizationPipeline compares fluent composition with the equivalent standard-library pipeline.
func BenchmarkNormalizationPipeline(b *testing.B) {
- const value = " AUTH_OAuth_Provider-Name.Test "
-
b.Run("StandardLibrary", func(b *testing.B) {
for b.Loop() {
- benchmarkRawStringResult = strings.ToLower(strings.TrimSpace(value))
+ benchmarkRawStringResult = benchmarkTrimToLowerStandard(benchmarkTrimToLowerInput)
}
})
b.Run("Fluent", func(b *testing.B) {
for b.Loop() {
- benchmarkRawStringResult = Of(value).Trim().ToLower().String()
+ benchmarkRawStringResult = benchmarkTrimToLowerFluent(benchmarkTrimToLowerInput)
}
})
}
// BenchmarkReplaceAllPipeline compares ordered fluent replacements with equivalent standard-library calls.
func BenchmarkReplaceAllPipeline(b *testing.B) {
- const value = "archive-logs cold.storage"
-
b.Run("StandardLibrary", func(b *testing.B) {
for b.Loop() {
- result := strings.ReplaceAll(value, "-", "_")
- result = strings.ReplaceAll(result, " ", "_")
- benchmarkRawStringResult = strings.ReplaceAll(result, ".", "_")
+ benchmarkRawStringResult = benchmarkReplaceAllStandard(benchmarkReplaceAllInput)
}
})
b.Run("Fluent", func(b *testing.B) {
for b.Loop() {
- benchmarkRawStringResult = Of(value).
- ReplaceAll("-", "_").
- ReplaceAll(" ", "_").
- ReplaceAll(".", "_").
- String()
+ benchmarkRawStringResult = benchmarkReplaceAllFluent(benchmarkReplaceAllInput)
}
})
}
+// TestBenchmarkComparisonHelpers verifies that each fluent benchmark performs the same work as its standard-library baseline.
+func TestBenchmarkComparisonHelpers(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ standard func(string) string
+ fluent func(string) string
+ }{
+ {name: "Trim", input: benchmarkTrimInput, standard: benchmarkTrimStandard, fluent: benchmarkTrimFluent},
+ {name: "ToLower", input: benchmarkToLowerInput, standard: benchmarkToLowerStandard, fluent: benchmarkToLowerFluent},
+ {name: "NormalizeSpace", input: benchmarkNormalizeSpaceInput, standard: benchmarkNormalizeSpaceStandard, fluent: benchmarkNormalizeSpaceFluent},
+ {name: "TrimToLower", input: benchmarkTrimToLowerInput, standard: benchmarkTrimToLowerStandard, fluent: benchmarkTrimToLowerFluent},
+ {name: "ReplaceAll", input: benchmarkReplaceAllInput, standard: benchmarkReplaceAllStandard, fluent: benchmarkReplaceAllFluent},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ standard := test.standard(test.input)
+ fluent := test.fluent(test.input)
+ if standard != fluent {
+ t.Fatalf("standard result %q does not match fluent result %q", standard, fluent)
+ }
+ })
+ }
+}
+
+// benchmarkTrimStandard gives the baseline the same B.Loop call boundary as the fluent variant.
+//
+//go:noinline
+func benchmarkTrimStandard(value string) string {
+ return strings.TrimSpace(value)
+}
+
+// benchmarkTrimFluent gives the fluent trim the same B.Loop call boundary as its baseline.
+//
+//go:noinline
+func benchmarkTrimFluent(value string) string {
+ return Of(value).Trim().String()
+}
+
+// benchmarkToLowerStandard gives the baseline the same B.Loop call boundary as the fluent variant.
+//
+//go:noinline
+func benchmarkToLowerStandard(value string) string {
+ return strings.ToLower(value)
+}
+
+// benchmarkToLowerFluent gives the fluent lowercase operation the same B.Loop call boundary as its baseline.
+//
+//go:noinline
+func benchmarkToLowerFluent(value string) string {
+ return Of(value).ToLower().String()
+}
+
+// benchmarkNormalizeSpaceStandard gives the baseline the same B.Loop call boundary as the fluent variant.
+//
+//go:noinline
+func benchmarkNormalizeSpaceStandard(value string) string {
+ return strings.Join(strings.Fields(value), " ")
+}
+
+// benchmarkNormalizeSpaceFluent gives fluent normalization the same B.Loop call boundary as its baseline.
+//
+//go:noinline
+func benchmarkNormalizeSpaceFluent(value string) string {
+ return Of(value).NormalizeSpace().String()
+}
+
+// benchmarkTrimToLowerStandard gives the baseline the same B.Loop call boundary as the fluent variant.
+//
+//go:noinline
+func benchmarkTrimToLowerStandard(value string) string {
+ return strings.ToLower(strings.TrimSpace(value))
+}
+
+// benchmarkTrimToLowerFluent gives the fluent pipeline the same B.Loop call boundary as its baseline.
+//
+//go:noinline
+func benchmarkTrimToLowerFluent(value string) string {
+ return Of(value).Trim().ToLower().String()
+}
+
+// benchmarkReplaceAllStandard gives the baseline the same B.Loop call boundary as the fluent variant.
+//
+//go:noinline
+func benchmarkReplaceAllStandard(value string) string {
+ result := strings.ReplaceAll(value, "-", "_")
+ result = strings.ReplaceAll(result, " ", "_")
+ return strings.ReplaceAll(result, ".", "_")
+}
+
+// benchmarkReplaceAllFluent gives the fluent pipeline the same B.Loop call boundary as its baseline.
+//
+//go:noinline
+func benchmarkReplaceAllFluent(value string) string {
+ return Of(value).
+ ReplaceAll("-", "_").
+ ReplaceAll(" ", "_").
+ ReplaceAll(".", "_").
+ String()
+}
+
// BenchmarkReplaceFold measures repeated Unicode simple-fold replacement.
func BenchmarkReplaceFold(b *testing.B) {
value := Of("Go Σ go ς GO σ gopher")
From 2a272241bd2d5692b05c1dda8b3c4b5741204b18 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Fri, 17 Jul 2026 08:33:14 +0000
Subject: [PATCH 2/2] test: use neutral benchmark inputs
---
README.md | 10 +-
docs/readme/benchmarks.txt | 202 ++++++++++++++++++-------------------
string_benchmark_test.go | 20 ++--
3 files changed, 116 insertions(+), 116 deletions(-)
diff --git a/README.md b/README.md
index 15a3eaf..878a3e5 100644
--- a/README.md
+++ b/README.md
@@ -139,11 +139,11 @@ Recorded with `go1.26.1` on `linux/arm64` using `-cpu=1` (`GOMAXPROCS=1`).
| Workload | Standard library | `str` chain |
| --- | ---: | ---: |
-| Trim | 3.2 ns/op · 0 B/op · 0 allocs/op | 3.3 ns/op · 0 B/op · 0 allocs/op |
-| ToLower | 76.9 ns/op · 48 B/op · 1 allocs/op | 77.3 ns/op · 48 B/op · 1 allocs/op |
-| NormalizeSpace (Fields + Join) | 190.6 ns/op · 208 B/op · 2 allocs/op | 176.1 ns/op · 80 B/op · 1 allocs/op |
-| Trim → ToLower | 67.4 ns/op · 32 B/op · 1 allocs/op | 69.8 ns/op · 32 B/op · 1 allocs/op |
-| ReplaceAll × 3 | 130.4 ns/op · 96 B/op · 3 allocs/op | 128.8 ns/op · 96 B/op · 3 allocs/op |
+| Trim | 3.2 ns/op · 0 B/op · 0 allocs/op | 3.2 ns/op · 0 B/op · 0 allocs/op |
+| ToLower | 61.2 ns/op · 32 B/op · 1 allocs/op | 63.0 ns/op · 32 B/op · 1 allocs/op |
+| NormalizeSpace (Fields + Join) | 191.6 ns/op · 208 B/op · 2 allocs/op | 185.2 ns/op · 80 B/op · 1 allocs/op |
+| Trim → ToLower | 71.8 ns/op · 32 B/op · 1 allocs/op | 69.3 ns/op · 32 B/op · 1 allocs/op |
+| ReplaceAll × 3 | 136.6 ns/op · 96 B/op · 3 allocs/op | 133.1 ns/op · 96 B/op · 3 allocs/op |
Timing is machine-specific; use it to understand the scale of these operations, not as a universal speed claim. Treat small timing differences within the raw sample spread as noise. Allocation counts are less sensitive to machine speed and show how much heap work each composition performs. In these workloads, wrapping and unwrapping added no heap allocations; allocations came from transformations that produced new text. `NormalizeSpace` is algorithmically different: the standard-library composition builds a field slice before joining it, while `str` uses one builder pass.
diff --git a/docs/readme/benchmarks.txt b/docs/readme/benchmarks.txt
index fa73779..31f8673 100644
--- a/docs/readme/benchmarks.txt
+++ b/docs/readme/benchmarks.txt
@@ -4,105 +4,105 @@
goos: linux
goarch: arm64
pkg: github.com/goforj/str/v2
-BenchmarkTrimComparison/StandardLibrary 183970962 3.244 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/StandardLibrary 185686015 3.213 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/StandardLibrary 184220110 3.226 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/StandardLibrary 181069550 3.237 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/StandardLibrary 191199663 3.190 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/StandardLibrary 190655265 3.189 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/StandardLibrary 175977206 3.367 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/StandardLibrary 185205409 3.318 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/StandardLibrary 182208324 3.284 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/StandardLibrary 186352513 3.241 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 185337969 3.247 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 185420518 3.287 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 186639068 3.238 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 183801771 3.256 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 189553234 3.201 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 187122338 3.211 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 183919126 3.256 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 178826119 3.272 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 181908288 3.270 ns/op 0 B/op 0 allocs/op
-BenchmarkTrimComparison/Fluent 182728324 3.282 ns/op 0 B/op 0 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 8041405 76.92 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 7931132 76.98 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 7868384 76.59 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 7887022 76.97 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 7546566 78.40 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 7674072 77.94 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 7786072 78.60 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 7817716 76.84 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 7770988 76.60 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/StandardLibrary 8069696 74.62 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 7849126 77.42 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 7456654 79.39 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 8053372 76.04 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 8020948 77.46 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 7775091 77.58 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 7889784 76.95 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 7738846 77.21 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 7797414 76.75 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 7693078 76.51 ns/op 48 B/op 1 allocs/op
-BenchmarkToLowerComparison/Fluent 7728895 78.53 ns/op 48 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 3176962 190.4 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 3143799 193.6 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 2919790 198.3 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 3130888 191.7 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 3177224 190.7 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 3124177 191.0 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 3179091 188.7 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 3186408 189.5 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 3120853 189.6 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/StandardLibrary 3259677 185.4 ns/op 208 B/op 2 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3383655 182.3 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3384769 176.1 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3409942 174.2 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3466602 174.0 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3410059 174.7 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3457311 173.6 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3412810 176.3 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3397281 176.7 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3378294 177.1 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizeSpaceComparison/Fluent 3376244 176.2 ns/op 80 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 8918898 67.27 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 9043378 67.28 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 9112165 67.45 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 8910421 67.26 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 9013908 68.23 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 8858690 68.74 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 9035310 67.55 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 9113894 67.73 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 8908100 66.89 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/StandardLibrary 8958230 67.12 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 8899297 66.80 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 8982628 69.32 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 8692294 67.97 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 9089571 69.55 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 8607890 68.25 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 8398479 70.31 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 8431693 71.25 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 8879492 71.28 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 8424811 70.14 ns/op 32 B/op 1 allocs/op
-BenchmarkNormalizationPipeline/Fluent 8640062 69.95 ns/op 32 B/op 1 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4530718 130.6 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4692512 132.3 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4490119 130.0 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4685865 128.7 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4287490 130.2 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4682410 129.0 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4555342 130.6 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4661804 130.2 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4642352 132.0 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/StandardLibrary 4593618 131.5 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4685128 128.8 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4632000 131.4 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4624298 134.5 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4508350 132.5 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4687993 129.0 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4689482 126.1 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4723741 128.1 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4697964 128.8 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4568785 128.5 ns/op 96 B/op 3 allocs/op
-BenchmarkReplaceAllPipeline/Fluent 4660621 126.4 ns/op 96 B/op 3 allocs/op
+BenchmarkTrimComparison/StandardLibrary 190946571 3.164 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 193467627 3.161 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 185068549 3.274 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 194559249 3.091 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 190095249 3.156 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 192387723 3.153 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 186099930 3.205 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 184818572 3.234 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 191117737 3.151 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/StandardLibrary 191142344 3.116 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 191202139 3.142 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 193703203 3.123 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 190540266 3.224 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 184779488 3.207 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 186113424 3.164 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 193557684 3.134 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 192382069 3.142 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 183566298 3.231 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 177093850 3.393 ns/op 0 B/op 0 allocs/op
+BenchmarkTrimComparison/Fluent 186325248 3.181 ns/op 0 B/op 0 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 10812687 60.76 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 10184080 61.63 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 9697700 64.25 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 10004844 61.76 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 9690957 61.82 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 9964923 60.61 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 9844861 60.84 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 9873550 60.87 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 9885758 60.61 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/StandardLibrary 10054821 62.15 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 9349152 65.11 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 9076411 62.94 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 9692647 69.16 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 8884040 66.84 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 9854678 63.19 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 9579446 63.06 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 9745434 62.22 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 9758139 60.92 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 9700168 62.25 ns/op 32 B/op 1 allocs/op
+BenchmarkToLowerComparison/Fluent 9541723 62.55 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3142360 191.9 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3135952 191.2 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3170942 191.5 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3164911 191.7 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3135394 191.9 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3116246 193.5 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3127114 191.8 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3201840 188.8 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3170710 190.3 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/StandardLibrary 3172434 189.7 ns/op 208 B/op 2 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3266970 180.9 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 2917446 199.6 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3319747 182.6 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3191629 211.3 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3242720 191.1 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3200499 188.4 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3113152 186.6 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3307267 182.0 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3273117 183.1 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizeSpaceComparison/Fluent 3308498 183.8 ns/op 80 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8728428 69.41 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 9030975 71.66 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8298609 73.13 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8417182 71.91 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8257778 73.42 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8637910 68.71 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8797850 72.69 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8356753 70.17 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 8675128 68.27 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/StandardLibrary 9128564 74.12 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 7613887 71.65 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8513475 69.36 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8644646 69.23 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8726205 69.49 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8686191 70.40 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8799112 67.89 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8852857 70.92 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8606089 69.30 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 8901904 68.10 ns/op 32 B/op 1 allocs/op
+BenchmarkNormalizationPipeline/Fluent 9101210 67.21 ns/op 32 B/op 1 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4494642 139.8 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4382300 134.3 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4452501 163.4 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4171320 138.9 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4335949 136.3 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4461729 135.6 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4598102 134.0 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4229871 139.1 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4430920 133.9 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/StandardLibrary 4437145 136.9 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4555426 131.2 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4507062 135.8 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4395556 136.5 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4448461 134.0 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4648602 132.8 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4493638 132.8 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4550988 131.5 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4769672 132.0 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4557463 133.3 ns/op 96 B/op 3 allocs/op
+BenchmarkReplaceAllPipeline/Fluent 4473340 133.5 ns/op 96 B/op 3 allocs/op
PASS
-ok github.com/goforj/str/v2 60.210s
+ok github.com/goforj/str/v2 60.700s
diff --git a/string_benchmark_test.go b/string_benchmark_test.go
index 2e5714a..bee464f 100644
--- a/string_benchmark_test.go
+++ b/string_benchmark_test.go
@@ -9,10 +9,10 @@ var benchmarkStringResult String
var benchmarkRawStringResult string
const (
- benchmarkTrimInput = " GoForj builds practical Go applications "
- benchmarkToLowerInput = "GoForj Builds Practical Go Applications"
+ benchmarkTrimInput = " /var/log/app/events.log "
+ benchmarkToLowerInput = "Content-Type: Application/JSON"
benchmarkNormalizeSpaceInput = " SELECT users.id, users.email\nFROM users\tWHERE users.status = ? "
- benchmarkTrimToLowerInput = " AUTH_OAuth_Provider-Name.Test "
+ benchmarkTrimToLowerInput = " API_GATEWAY-Primary.Region "
benchmarkReplaceAllInput = "archive-logs cold.storage"
)
@@ -29,9 +29,9 @@ func BenchmarkTrim(b *testing.B) {
name string
value string
}{
- {name: "ASCII", value: " GoForj builds practical Go applications "},
- {name: "Unicode", value: "\u2003GoForj builds practical Go applications\u00a0"},
- {name: "Clean", value: "GoForj builds practical Go applications"},
+ {name: "ASCII", value: " /var/log/app/events.log "},
+ {name: "Unicode", value: "\u2003/var/log/app/events.log\u00a0"},
+ {name: "Clean", value: "/var/log/app/events.log"},
}
for _, benchmark := range benchmarks {
@@ -78,10 +78,10 @@ func BenchmarkNormalizeSpace(b *testing.B) {
name string
value string
}{
- {name: "ASCII", value: " GoForj\tbuilds\n practical Go applications "},
- {name: "Unicode", value: "\u2003GoForj\tbuilds\n practical\u2003Go applications\u00a0"},
- {name: "Clean", value: "GoForj builds practical Go applications"},
- {name: "Trimmed", value: " GoForj builds practical Go applications "},
+ {name: "ASCII", value: " SELECT\tusers.id\n FROM users "},
+ {name: "Unicode", value: "\u2003SELECT\tusers.id\n FROM\u2003users\u00a0"},
+ {name: "Clean", value: "SELECT users.id FROM users"},
+ {name: "Trimmed", value: " SELECT users.id FROM users "},
{name: "Whitespace", value: " \t\n\u2003\u00a0 "},
}