Skip to content

Commit 2f14c92

Browse files
committed
bench: add streaming write benchmarks
Add comprehensive benchmarks for streaming write performance: - BenchmarkExcelize{rows}x{cols}: 9 size variants from 10x10 to 10000x1000 - BenchmarkBioSizeSweep: measures impact of bufio.Writer buffer size - TestBioSizeIOProfile: reports write syscall counts per buffer size - BenchmarkStringCellClean/Special: measures writeEscaped fast/slow paths
1 parent c80f43a commit 2f14c92

1 file changed

Lines changed: 231 additions & 0 deletions

File tree

stream_bench_test.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package excelize
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"os"
8+
"strconv"
9+
"testing"
10+
)
11+
12+
// writeExcelBench is adapted from github.com/mzimmerman/excelizetest to run
13+
// inside this repo as an in-package benchmark (no external dependency).
14+
func writeExcelBench(data [][]string, out io.Writer) error {
15+
file := NewFile()
16+
if len(data) == 0 {
17+
return nil
18+
}
19+
sw, err := file.NewStreamWriter("Sheet1")
20+
if err != nil {
21+
return err
22+
}
23+
lineInterface := make([]interface{}, len(data[0]))
24+
for excelLineNum, line := range data {
25+
lineInterface = lineInterface[:0]
26+
for x := range line {
27+
lineInterface = append(lineInterface, line[x])
28+
}
29+
cell, _ := CoordinatesToCellName(1, excelLineNum+1)
30+
if err = sw.SetRow(cell, lineInterface); err != nil {
31+
return err
32+
}
33+
}
34+
if err = sw.Flush(); err != nil {
35+
return err
36+
}
37+
_, err = file.WriteTo(out)
38+
return err
39+
}
40+
41+
func benchmarkExcelize(rows, cols int, b *testing.B) {
42+
buf := bytes.Buffer{}
43+
for n := 0; n < b.N; n++ {
44+
b.StopTimer()
45+
buf.Reset()
46+
count := 0
47+
data := make([][]string, rows)
48+
for x := range data {
49+
data[x] = make([]string, cols)
50+
for y := range data[x] {
51+
data[x][y] = strconv.Itoa(count)
52+
count++
53+
}
54+
}
55+
b.StartTimer()
56+
if err := writeExcelBench(data, &buf); err != nil {
57+
b.Fatalf("error writing excel - %v", err)
58+
}
59+
}
60+
}
61+
62+
func BenchmarkExcelize10x10(b *testing.B) { benchmarkExcelize(10, 10, b) }
63+
func BenchmarkExcelize100x100(b *testing.B) { benchmarkExcelize(100, 100, b) }
64+
func BenchmarkExcelize1000x1000(b *testing.B) { benchmarkExcelize(1000, 1000, b) }
65+
func BenchmarkExcelize10000x10000(b *testing.B) { benchmarkExcelize(10000, 10000, b) }
66+
func BenchmarkExcelize1000x10(b *testing.B) { benchmarkExcelize(1000, 10, b) }
67+
func BenchmarkExcelize10000x10(b *testing.B) { benchmarkExcelize(10000, 10, b) }
68+
func BenchmarkExcelize100000x10(b *testing.B) { benchmarkExcelize(100000, 10, b) }
69+
func BenchmarkExcelize100000x100(b *testing.B) { benchmarkExcelize(100000, 100, b) }
70+
func BenchmarkExcelize10000x1000(b *testing.B) { benchmarkExcelize(10000, 1000, b) }
71+
72+
// BenchmarkBioSizeSweep measures ns/op and B/op across a range of bufio.Writer
73+
// buffer sizes for a large sheet (~75 MB XML) that exceeds StreamChunkSize.
74+
// Run with: go test -bench=BenchmarkBioSizeSweep -benchmem -count=3 -run='^$'
75+
func BenchmarkBioSizeSweep(b *testing.B) {
76+
sizes := []int{
77+
4 << 10, // 4 KB
78+
8 << 10, // 8 KB
79+
16 << 10, // 16 KB
80+
32 << 10, // 32 KB
81+
64 << 10, // 64 KB
82+
128 << 10, // 128 KB
83+
256 << 10, // 256 KB
84+
512 << 10, // 512 KB
85+
1 << 20, // 1 MB
86+
4 << 20, // 4 MB
87+
}
88+
row := make([]interface{}, 100)
89+
for colID := range row {
90+
row[colID] = colID * 12345
91+
}
92+
for _, sz := range sizes {
93+
sz := sz
94+
b.Run(fmt.Sprintf("bio=%s", fmtSize(sz)), func(b *testing.B) {
95+
b.ReportAllocs()
96+
for n := 0; n < b.N; n++ {
97+
file := NewFile()
98+
sw, _ := file.NewStreamWriter("Sheet1")
99+
sw.rawData.bioSize = sz
100+
for rowID := 1; rowID <= 50000; rowID++ {
101+
cell, _ := CoordinatesToCellName(1, rowID)
102+
_ = sw.SetRow(cell, row)
103+
}
104+
_ = sw.Flush()
105+
_ = file.Close()
106+
}
107+
})
108+
}
109+
}
110+
111+
// countingWriter wraps an os.File and records every Write call made to it.
112+
type countingWriter struct {
113+
f *os.File
114+
calls int
115+
bytes int64
116+
}
117+
118+
// fmtSize returns a human-readable label for a byte size.
119+
func fmtSize(n int) string {
120+
switch {
121+
case n >= 1<<20:
122+
return fmt.Sprintf("%dMiB", n>>20)
123+
case n >= 1<<10:
124+
return fmt.Sprintf("%dKiB", n>>10)
125+
default:
126+
return fmt.Sprintf("%dB", n)
127+
}
128+
}
129+
130+
func (c *countingWriter) Write(p []byte) (int, error) {
131+
c.calls++
132+
c.bytes += int64(len(p))
133+
return c.f.Write(p)
134+
}
135+
136+
// TestBioSizeIOProfile is not a benchmark — it runs once per bio size and
137+
// prints: total bytes written to disk, number of write syscalls, and average
138+
// write size. Run with: go test -v -run=TestBioSizeIOProfile -count=1
139+
func TestBioSizeIOProfile(t *testing.T) {
140+
sizes := []int{
141+
4 << 10, // 4 KB
142+
8 << 10, // 8 KB
143+
16 << 10, // 16 KB
144+
32 << 10, // 32 KB
145+
64 << 10, // 64 KB
146+
128 << 10, // 128 KB
147+
256 << 10, // 256 KB
148+
512 << 10, // 512 KB
149+
1 << 20, // 1 MB
150+
4 << 20, // 4 MB
151+
}
152+
row := make([]interface{}, 100)
153+
for i := range row {
154+
row[i] = i * 12345
155+
}
156+
157+
t.Logf("%-10s %12s %8s %10s", "bio size", "bytes to disk", "# writes", "avg write")
158+
t.Logf("%-10s %12s %8s %10s", "--------", "-------------", "--------", "---------")
159+
for _, sz := range sizes {
160+
file := NewFile()
161+
sw, _ := file.NewStreamWriter("Sheet1")
162+
sw.rawData.bioSize = sz
163+
sw.rawData.flushSize = 1
164+
165+
f, err := os.CreateTemp("", "excelize-profile-")
166+
if err != nil {
167+
t.Fatal(err)
168+
}
169+
cw := &countingWriter{f: f}
170+
sw.rawData.tmp = f
171+
for rowID := 1; rowID <= 50000; rowID++ {
172+
cell, _ := CoordinatesToCellName(1, rowID)
173+
_ = sw.SetRow(cell, row)
174+
if rowID == 1 && sw.rawData.bio != nil {
175+
sw.rawData.bio.Reset(cw)
176+
cw.calls = 0
177+
cw.bytes = 0
178+
}
179+
}
180+
_ = sw.Flush()
181+
182+
avg := int64(0)
183+
if cw.calls > 0 {
184+
avg = cw.bytes / int64(cw.calls)
185+
}
186+
t.Logf("%-10s %12s %8d %10s",
187+
fmtSize(sz), fmtSize(int(cw.bytes)), cw.calls, fmtSize(int(avg)))
188+
189+
_ = file.Close()
190+
f.Close()
191+
os.Remove(f.Name())
192+
}
193+
}
194+
195+
// BenchmarkStringCellClean and BenchmarkStringCellSpecial measure the
196+
// writeEscaped fast path (no special chars) vs slow path (has <, >, &, etc.).
197+
func BenchmarkStringCellClean(b *testing.B) {
198+
row := make([]interface{}, 50)
199+
for i := range row {
200+
row[i] = "normal cell content without special chars"
201+
}
202+
b.ReportAllocs()
203+
for n := 0; n < b.N; n++ {
204+
file := NewFile()
205+
sw, _ := file.NewStreamWriter("Sheet1")
206+
for rowID := 1; rowID <= 10000; rowID++ {
207+
cell, _ := CoordinatesToCellName(1, rowID)
208+
_ = sw.SetRow(cell, row)
209+
}
210+
_ = sw.Flush()
211+
_ = file.Close()
212+
}
213+
}
214+
215+
func BenchmarkStringCellSpecial(b *testing.B) {
216+
row := make([]interface{}, 50)
217+
for i := range row {
218+
row[i] = "content with <special> & \"chars\""
219+
}
220+
b.ReportAllocs()
221+
for n := 0; n < b.N; n++ {
222+
file := NewFile()
223+
sw, _ := file.NewStreamWriter("Sheet1")
224+
for rowID := 1; rowID <= 10000; rowID++ {
225+
cell, _ := CoordinatesToCellName(1, rowID)
226+
_ = sw.SetRow(cell, row)
227+
}
228+
_ = sw.Flush()
229+
_ = file.Close()
230+
}
231+
}

0 commit comments

Comments
 (0)