@ 261101.7
current
package main

import (
	"context"
	"fmt"
	"math/rand/v2"
	"os"
	"path"
	"path/filepath"
	"strings"
	"testing"
)

// Benchmarks cover only the paths a reader can feel: search as you type,
// and the first view of a code page, before codeCache holds it. Warm pages
// sit far under pageBudget, which TestPageBudget guards. Run them with make
// bench.

// BenchmarkSearch scans a generated tree of 4,000 text files and 24 MB,
// about the size of ~/scratch. A rare word reads every file; a common one
// stops at maxLineHits.
func BenchmarkSearch(b *testing.B) {
	dir := b.TempDir()
	words := strings.Fields("the a of to and in is it that for on with as by this be not are from or at an " +
		"page tree line file code search server render lexer cache commit history listing branch share query")
	rng := rand.New(rand.NewPCG(1, 2))
	var size int64
	for i := range 4000 {
		var text strings.Builder
		for text.Len() < 6000 {
			for n := 6 + rng.IntN(7); n > 0; n-- {
				text.WriteString(words[rng.IntN(len(words))])
				text.WriteByte(' ')
			}
			text.WriteByte('\n')
		}
		p := filepath.Join(dir, fmt.Sprintf("d%02d", i/50), fmt.Sprintf("f%04d.md", i))
		os.MkdirAll(filepath.Dir(p), 0o755)
		if err := os.WriteFile(p, []byte(text.String()), 0o644); err != nil {
			b.Fatal(err)
		}
		size += int64(text.Len())
	}
	root, err := os.OpenRoot(dir)
	if err != nil {
		b.Fatal(err)
	}
	defer root.Close()
	t := newTree(root, dir, "/", "bench")
	for _, q := range []struct{ name, query string }{{"rare", "zyzzyva"}, {"common", "server"}} {
		b.Run(q.name, func(b *testing.B) {
			if q.name == "rare" {
				b.SetBytes(size)
			}
			b.ReportAllocs()
			for b.Loop() {
				t.search(context.Background(), ".", q.query)
			}
		})
	}
}

// BenchmarkCodePage times a code page's first view per language: lex
// alone, then the whole page, whose difference is the page writer, and
// the page with scopes, whose difference is the scope boxes. Go and
// CSS come from this repo, CSS through chroma; Python, JavaScript and
// shell from Debian's packages, the files doc/speed.md names, skipped where
// they are not installed.
func BenchmarkCodePage(b *testing.B) {
	pythons, _ := filepath.Glob("/usr/lib/python3*/argparse.py")
	files := append([]string{"semantic.go", "assets/style.css",
		"/usr/share/javascript/jquery/jquery.js", "/usr/share/bash-completion/bash_completion"}, pythons...)
	for _, name := range files {
		b.Run(path.Base(name), func(b *testing.B) {
			src, err := os.ReadFile(name)
			if err != nil {
				b.Skip(err)
			}
			code := string(src)
			lines := float64(strings.Count(code, "\n"))
			lexer := codeLexer(name)
			tokenise(lexer, "x\n")
			perLine := func(b *testing.B) {
				b.ReportMetric(float64(b.Elapsed().Nanoseconds())/float64(b.N)/lines, "ns/line")
			}
			b.Run("lex", func(b *testing.B) {
				for b.Loop() {
					tokenise(lexer, code)
				}
				perLine(b)
			})
			for _, view := range []string{"page", "scopes"} {
				b.Run(view, func(b *testing.B) {
					b.ReportAllocs()
					for b.Loop() {
						var out strings.Builder
						highlight(&out, lexer, code, true, view == "scopes")
					}
					perLine(b)
				})
			}
		})
	}
}

// BenchmarkSyntaxTree times a tree page's first view on jquery.js in its
// stages: the helper with reading its output, rendering the rows, and
// the whole page as a request returns it, where the two overlap. It
// skips where the helper or the file is missing.
func BenchmarkSyntaxTree(b *testing.B) {
	src, err := os.ReadFile("/usr/share/javascript/jquery/jquery.js")
	if err != nil {
		b.Skip(err)
	}
	if _, err := syntaxHelper(); err != nil {
		b.Skip("no helper; make syntax builds it")
	}
	nodes, err := syntaxNodes(context.Background(), "jquery.js", src)
	if err != nil {
		b.Fatal(err)
	}
	b.Run("helper", func(b *testing.B) {
		for b.Loop() {
			parseSyntax(context.Background(), "jquery.js", src, func(synNode) {})
		}
	})
	b.Run("rows", func(b *testing.B) {
		b.ReportAllocs()
		for b.Loop() {
			w := newSynRows("jquery.js", src)
			for _, n := range nodes {
				w.add(n)
			}
			w.done()
		}
	})
	b.Run("page", func(b *testing.B) {
		dir := b.TempDir()
		if err := os.WriteFile(filepath.Join(dir, "jquery.js"), src, 0o644); err != nil {
			b.Fatal(err)
		}
		root, err := os.OpenRoot(dir)
		if err != nil {
			b.Fatal(err)
		}
		defer root.Close()
		s := newServer()
		s.add(newTree(root, dir, "/", "bench"))
		h := s.handler()
		b.ReportAllocs()
		for b.Loop() {
			syntaxCache = newCache(8 << 20)
			if rec := get(h, "/jquery.js?tree"); rec.Code != 200 {
				b.Fatal(rec.Code)
			}
		}
	})
}