Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/benchmark.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Benchmark

on:
pull_request:
branches:
- main
push:
branches:
- main
schedule:
# every 12 hours
- cron: '0 */12 * * *'

jobs:
corpus:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.26.6"

- name: Clone + install Laravel corpus
run: |
git clone --depth 1 https://github.com/laravel/framework laravel
composer install --no-interaction --no-progress --ignore-platform-reqs --working-dir=laravel
# drop intentionally-broken PHP fixtures
rm -rf laravel/tests
find laravel/vendor -depth -type d -name tests -exec rm -rf {} +

- run: go build -o benchmark-corpus ./benchmark

- name: Run benchmark (timed + memory)
run: |
# average wall-clock over 5 runs
bench() {
total=0
for i in $(seq 1 5); do
start=$(date +%s%3N)
"$@" > /dev/null
end=$(date +%s%3N)
total=$((total + end - start))
done
echo $((total / 5))
}
# peak resident set size (MB) of a single run
mem() {
/usr/bin/time -v "$@" 2>mem.log > /dev/null
awk '/Maximum resident set size/{printf "%d", $NF/1024}' mem.log
}
ms=$(bench ./benchmark-corpus laravel)
mb=$(mem ./benchmark-corpus laravel)
files=$(./benchmark-corpus laravel | awk '{print $2}')
{
echo "## Corpus benchmark"
echo ""
echo "Parsing the full Laravel framework (\`src/\` + Composer \`vendor/\`), average wall-clock over 5 runs."
echo ""
echo "| Files | Avg (5 runs) | Peak mem |"
echo "|------:|-------------:|---------:|"
echo "| $files | $ms ms | $mb MB |"
} | tee -a "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
.idea
**/*.test

/laravel/
/benchmark-corpus

cpu.pprof
mem.pprof
trace.out
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ bench:
go test -benchmem -bench=. ./internal/php5
go test -benchmem -bench=. ./internal/php7

# wall-clock parse of a corpus, e.g. `make bench-corpus DIR=./laravel`
bench-corpus:
go run ./benchmark $(DIR)

compile: ./internal/php5/php5.go ./internal/php7/php7.go ./internal/php8/php8.go ./internal/php8/scanner.go ./internal/scanner/scanner.go
sed -i '' -e 's/yyErrorVerbose = false/yyErrorVerbose = true/g' ./internal/php5/php5.go
sed -i '' -e 's/yyErrorVerbose = false/yyErrorVerbose = true/g' ./internal/php7/php7.go
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ func main() {
- `pkg/visitor` — traverser, printer, dumper, namespace and class resolvers, formatter.
- `pkg/token`, `pkg/position`, `pkg/version`, `pkg/errors`, `pkg/conf`.

## Benchmark

`benchmark/` parses every `.php` file under a directory and reports the wall-clock time. Point it at any corpus:

```bash
make bench-corpus DIR=./laravel
# parsed 2966 files in 1215 ms
```

CI runs it against a full Laravel framework checkout (`src/` + Composer `vendor/`) on every push and every 12 hours, reporting the average over 5 runs and peak memory in the run's **Summary**.

## Generated code

`internal/*/php*.go` and `internal/*/scanner.go` are generated. Edit the grammar source instead:
Expand Down
84 changes: 84 additions & 0 deletions benchmark/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Corpus benchmark: parse every .php file under the given path and report timing.
// Parsing runs across GOMAXPROCS workers with the GC disabled for the short-lived run.
package main

import (
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
"time"

"github.com/rectorphp/php-parser-in-go/pkg/conf"
"github.com/rectorphp/php-parser-in-go/pkg/parser"
"github.com/rectorphp/php-parser-in-go/pkg/version"
)

func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}

phpVersion, err := version.New("8.3")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
config := conf.Config{Version: phpVersion}

// short-lived process: parse throughput matters, not steady-state memory
debug.SetGCPercent(-1)

files := collectPHPFiles(root)

start := time.Now()
parsed := parseAll(files, config)
elapsed := time.Since(start)

fmt.Printf("parsed %d files in %d ms\n", parsed, elapsed.Milliseconds())
}

// parseAll parses every file across GOMAXPROCS workers and returns the count parsed.
func parseAll(files []string, config conf.Config) int64 {
jobs := make(chan string, runtime.GOMAXPROCS(0))
var parsed int64

var wg sync.WaitGroup
for range runtime.GOMAXPROCS(0) {
wg.Go(func() {
for path := range jobs {
content, err := os.ReadFile(path)
if err != nil {
continue
}
// broken fixtures return an error; the parse work is what we time
parser.Parse(content, config)
atomic.AddInt64(&parsed, 1)
}
})
}

for _, path := range files {
jobs <- path
}
close(jobs)
wg.Wait()

return parsed
}

func collectPHPFiles(root string) []string {
var files []string
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err == nil && !d.IsDir() && filepath.Ext(path) == ".php" {
files = append(files, path)
}
return nil
})
return files
}
Loading