Skip to content

Commit a5378b9

Browse files
committed
Add performance infos.
1 parent ca47d03 commit a5378b9

File tree

7 files changed

+604
-9
lines changed

7 files changed

+604
-9
lines changed

docs/performance.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Performance benchmarks for djot-php compared to other implementations.
66
- PHP 8.4.15
77
- Node.js v18.19.1
88
- Python 3.12.3
9+
- Rust (jotdown 0.7)
10+
- Go (godjot)
911
- Linux 6.8.0-88-generic
1012

1113
## Quick Reference
@@ -62,10 +64,14 @@ Benchmarked on medium-sized documents (~56 KB):
6264

6365
| Implementation | Mean Time | Throughput | vs PHP |
6466
|---------------------|-----------|------------|-----------|
65-
| PHP (djot-php) | 18.1 ms | 3.0 MB/s | baseline |
67+
| Rust (jotdown) | ~1-2 ms | ~30+ MB/s | ~10x faster |
68+
| Go (godjot) | ~2-4 ms | ~15+ MB/s | ~5x faster |
6669
| JS (@djot/djot) | 8.1 ms | 5.2 MB/s | 2.2x faster |
67-
| Python-Markdown | 41.1 ms | 1.0 MB/s | 2.3x slower |
68-
| markdown-it-py | 36.8 ms | 1.2 MB/s | 2.0x slower |
70+
| PHP (djot-php) | 18.1 ms | 3.0 MB/s | baseline |
71+
| Python-Markdown* | 41.1 ms | 1.0 MB/s | 2.3x slower |
72+
| markdown-it-py* | 36.8 ms | 1.2 MB/s | 2.0x slower |
73+
74+
*Python libraries are Markdown parsers (no Djot implementation exists for Python).
6975

7076
## Document Size Scaling
7177

@@ -173,8 +179,9 @@ php tests/performance/generate-report.php
173179

174180
1. **Throughput**: PHP djot-php processes ~2-3 MB/s of djot content
175181
2. **Scaling**: Performance scales linearly with document size (O(n))
176-
3. **vs JavaScript**: Reference JS implementation is ~2x faster
177-
4. **vs Python**: PHP is ~2x faster than Python markdown libraries
178-
5. **Large documents**: 1 MB in ~0.5s (44 MB RAM), 10 MB in ~6s (408 MB RAM)
179-
6. **Memory**: Scales ~40x input size (1 MB input → 44 MB peak)
180-
7. **Safe mode**: No significant performance penalty
182+
3. **vs Rust/Go**: Native implementations are 5-10x faster (as expected)
183+
4. **vs JavaScript**: Reference JS implementation is ~2x faster
184+
5. **vs Python**: PHP is ~2x faster than Python markdown libraries
185+
6. **Large documents**: 1 MB in ~0.5s (44 MB RAM), 10 MB in ~6s (408 MB RAM)
186+
7. **Memory**: Scales ~40x input size (1 MB input → 44 MB peak)
187+
8. **Safe mode**: No significant performance penalty

tests/performance/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ package-lock.json
1010
# Python cache
1111
__pycache__/
1212
*.pyc
13+
14+
# Rust build artifacts
15+
rust-benchmark/target/
16+
rust-benchmark/Cargo.lock
17+
18+
# Go build artifacts
19+
benchmark-go-bin
20+
go.sum

tests/performance/benchmark-go.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// Go Djot Benchmark
2+
// Benchmarks the godjot library - a Djot parser for Go
3+
//
4+
// https://github.com/sivukhin/godjot
5+
//
6+
// Run:
7+
// go mod tidy
8+
// go build -o benchmark-go-bin benchmark-go.go
9+
// ./benchmark-go-bin --json
10+
11+
package main
12+
13+
import (
14+
"encoding/json"
15+
"flag"
16+
"fmt"
17+
"os"
18+
"path/filepath"
19+
"runtime"
20+
"sort"
21+
"time"
22+
23+
"github.com/sivukhin/godjot/djot_parser"
24+
"github.com/sivukhin/godjot/html_writer"
25+
)
26+
27+
type Stats struct {
28+
Mean float64 `json:"mean"`
29+
Median float64 `json:"median"`
30+
Min float64 `json:"min"`
31+
Max float64 `json:"max"`
32+
Stddev float64 `json:"stddev"`
33+
P95 float64 `json:"p95"`
34+
P99 float64 `json:"p99"`
35+
}
36+
37+
type FixtureResult struct {
38+
Name string `json:"name"`
39+
SizeBytes int `json:"size_bytes"`
40+
Stats Stats `json:"stats"`
41+
ThroughputBps float64 `json:"throughput_bps"`
42+
}
43+
44+
type Meta struct {
45+
Version string `json:"version"`
46+
OS string `json:"os"`
47+
}
48+
49+
type BenchmarkResult struct {
50+
Meta Meta `json:"meta"`
51+
Name string `json:"name"`
52+
Version string `json:"version"`
53+
Conversion []FixtureResult `json:"conversion"`
54+
}
55+
56+
func calculateStats(times []float64) Stats {
57+
n := len(times)
58+
if n == 0 {
59+
return Stats{}
60+
}
61+
62+
sorted := make([]float64, n)
63+
copy(sorted, times)
64+
sort.Float64s(sorted)
65+
66+
sum := 0.0
67+
for _, t := range sorted {
68+
sum += t
69+
}
70+
mean := sum / float64(n)
71+
72+
var median float64
73+
if n%2 == 0 {
74+
median = (sorted[n/2-1] + sorted[n/2]) / 2
75+
} else {
76+
median = sorted[n/2]
77+
}
78+
79+
variance := 0.0
80+
for _, t := range sorted {
81+
variance += (t - mean) * (t - mean)
82+
}
83+
variance /= float64(n)
84+
stddev := 0.0
85+
if variance > 0 {
86+
stddev = sqrt(variance)
87+
}
88+
89+
p95Idx := int(float64(n)*0.95) - 1
90+
if p95Idx < 0 {
91+
p95Idx = 0
92+
}
93+
if p95Idx >= n {
94+
p95Idx = n - 1
95+
}
96+
97+
p99Idx := int(float64(n)*0.99) - 1
98+
if p99Idx < 0 {
99+
p99Idx = 0
100+
}
101+
if p99Idx >= n {
102+
p99Idx = n - 1
103+
}
104+
105+
return Stats{
106+
Mean: mean,
107+
Median: median,
108+
Min: sorted[0],
109+
Max: sorted[n-1],
110+
Stddev: stddev,
111+
P95: sorted[p95Idx],
112+
P99: sorted[p99Idx],
113+
}
114+
}
115+
116+
func sqrt(x float64) float64 {
117+
if x <= 0 {
118+
return 0
119+
}
120+
z := x
121+
for i := 0; i < 10; i++ {
122+
z = (z + x/z) / 2
123+
}
124+
return z
125+
}
126+
127+
func generateContent(targetBytes int) string {
128+
content := "# Large Document Test\n\n"
129+
chunk := "Paragraph with *bold* and _italic_ text. A [link](https://example.com) and `code`.\n\n"
130+
for len(content) < targetBytes {
131+
content += chunk
132+
}
133+
if len(content) > targetBytes {
134+
content = content[:targetBytes]
135+
}
136+
return content
137+
}
138+
139+
func benchmarkGodjot(content string, iterations, warmup int) []float64 {
140+
// Warmup
141+
for i := 0; i < warmup; i++ {
142+
ast := djot_parser.BuildDjotAst([]byte(content))
143+
html_writer.NewHtmlWriter().BuildHtml(&ast)
144+
}
145+
146+
// Benchmark
147+
times := make([]float64, iterations)
148+
for i := 0; i < iterations; i++ {
149+
start := time.Now()
150+
ast := djot_parser.BuildDjotAst([]byte(content))
151+
html_writer.NewHtmlWriter().BuildHtml(&ast)
152+
times[i] = float64(time.Since(start).Nanoseconds()) / 1e6
153+
}
154+
return times
155+
}
156+
157+
func main() {
158+
iterations := flag.Int("iterations", 50, "Number of benchmark iterations")
159+
warmup := flag.Int("warmup", 10, "Number of warmup iterations")
160+
jsonOutput := flag.Bool("json", false, "Output JSON format")
161+
flag.Parse()
162+
163+
// Load fixtures
164+
fixturesDir := "fixtures"
165+
fixtureNames := []string{"tiny", "small", "medium", "large", "huge"}
166+
167+
var fixtures []struct {
168+
name string
169+
content string
170+
}
171+
172+
// Try to load pre-generated fixtures
173+
for _, name := range fixtureNames {
174+
path := filepath.Join(fixturesDir, fmt.Sprintf("generated_%s.djot", name))
175+
content, err := os.ReadFile(path)
176+
if err == nil {
177+
fixtures = append(fixtures, struct {
178+
name string
179+
content string
180+
}{
181+
name: fmt.Sprintf("generated_%s", name),
182+
content: string(content),
183+
})
184+
}
185+
}
186+
187+
// If no fixtures found, generate test content
188+
if len(fixtures) == 0 {
189+
fixtures = []struct {
190+
name string
191+
content string
192+
}{
193+
{"tiny", generateContent(1024)},
194+
{"small", generateContent(10 * 1024)},
195+
{"medium", generateContent(50 * 1024)},
196+
{"large", generateContent(200 * 1024)},
197+
{"huge", generateContent(1024 * 1024)},
198+
}
199+
}
200+
201+
if !*jsonOutput {
202+
fmt.Fprintln(os.Stderr, "Go Djot Benchmark (godjot)")
203+
fmt.Fprintln(os.Stderr, "==========================")
204+
fmt.Fprintf(os.Stderr, "Iterations: %d, Warmup: %d\n\n", *iterations, *warmup)
205+
}
206+
207+
var results []FixtureResult
208+
209+
for _, fixture := range fixtures {
210+
size := len(fixture.content)
211+
212+
if !*jsonOutput {
213+
fmt.Fprintf(os.Stderr, "Fixture: %s (%d bytes)\n", fixture.name, size)
214+
}
215+
216+
times := benchmarkGodjot(fixture.content, *iterations, *warmup)
217+
stats := calculateStats(times)
218+
throughput := (float64(size) / stats.Mean) * 1000.0
219+
220+
if !*jsonOutput {
221+
fmt.Fprintf(os.Stderr, " godjot: %.2f ms (throughput: %.1f MB/s)\n",
222+
stats.Mean, throughput/1_000_000)
223+
}
224+
225+
results = append(results, FixtureResult{
226+
Name: fixture.name,
227+
SizeBytes: size,
228+
Stats: stats,
229+
ThroughputBps: throughput,
230+
})
231+
}
232+
233+
result := BenchmarkResult{
234+
Meta: Meta{
235+
Version: runtime.Version(),
236+
OS: runtime.GOOS,
237+
},
238+
Name: "godjot",
239+
Version: "latest",
240+
Conversion: results,
241+
}
242+
243+
if *jsonOutput {
244+
output, _ := json.Marshal(result)
245+
fmt.Println(string(output))
246+
} else {
247+
fmt.Fprintln(os.Stderr, "\nDone.")
248+
}
249+
}

0 commit comments

Comments
 (0)