package doc
import (
"crypto/sha1"
"fmt"
"strings"
"testing"
"github.com/go-git/go-git/v5/plumbing"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
"sourcecraft.dev/bigbes/sr-ht-spec/gitx"
)
// --- what these measure --------------------------------------------------------
//
// Serving one revision of a space is two pieces of work over the whole corpus,
// and neither of them touches git: FromDocuments turns the blobs a caller
// already read into an Archive — headers parsed, ids contested, hierarchy
// linked, aliases and stems indexed — and LinkPass renders every document
// through the markdown renderer to fill in the link graph the backlinks,
// orphan and catalog views read.
//
// Both grow with the corpus rather than with the request, which is exactly why
// they are worth a number: a space that doubles in size doubles what every page
// of it costs, and nothing about a single document says so.
//
// The corpus below is the shape a specification space actually has: sections of
// documents with frontmatter ids, a parent hierarchy, aliases, wikilinks that
// resolve and wikilinks that do not, relative links, tables and fenced blocks.
// benchSections and benchPerSection give benchDocs documents in all — a space
// larger than any this instance holds today, so the number says what growth
// costs rather than what today costs.
const (
benchSections = 8
benchPerSection = 25
benchDocs = benchSections * benchPerSection
)
var benchSpace = core.SpaceRef{Owner: "bigbes", Name: "rfcs"}
// benchRev is a plausible revision string; nothing here resolves it.
const benchRev = "3f786850e387550fdab836ed7e6dc881de23001b"
// benchBlob derives a stable, distinct blob hash per path. FromDocuments only
// carries it through to Page.Blob, but a shared zero hash across every document
// would be a corpus no git tree could produce.
func benchBlob(path string) plumbing.Hash {
return plumbing.Hash(sha1.Sum([]byte(path)))
}
// benchDocuments builds the corpus: one index per section plus benchPerSection
// documents under it, each carrying a header, prose, links and a code block.
func benchDocuments() []gitx.Document {
docs := make([]gitx.Document, 0, benchDocs+benchSections)
add := func(path, body string) {
docs = append(docs, gitx.Document{
Path: path,
Blob: benchBlob(path),
Data: []byte(body),
})
}
for s := 0; s < benchSections; s++ {
section := fmt.Sprintf("section-%d", s)
indexPath := section + "/index.md"
add(indexPath, fmt.Sprintf(`---
id: SPEC-%04d
title: Section %d
status: approved
aliases:
- sec-%d
---
# Section %d
The index of this section. It links every document under it, which is what
makes it a catalog and what keeps its own backlinks out of the orphan count.
%s
`, 9000+s, s, s, s, sectionIndexList(s)))
for i := 0; i < benchPerSection; i++ {
n := s*benchPerSection + i
path := fmt.Sprintf("%s/doc-%03d.md", section, i)
add(path, benchDocument(s, i, n))
}
}
return docs
}
// sectionIndexList is the bullet list a section index carries: a wikilink per
// document under it.
func sectionIndexList(s int) string {
var b strings.Builder
for i := 0; i < benchPerSection; i++ {
fmt.Fprintf(&b, "- [[doc-%03d]] — the %dth document of this section\n", i, i)
}
return b.String()
}
// benchDocument is one document of the corpus.
func benchDocument(section, i, n int) string {
var b strings.Builder
fmt.Fprintf(&b, `---
id: SPEC-%04d
title: Document %d of section %d
status: %s
parent: "[[index]]"
aliases:
- d-%d-%d
tags:
- benchmark
- section-%d
---
# Document %d of section %d
`, n, i, section, []string{"draft", "review", "approved"}[n%3], section, i, section, i, section)
b.WriteString(`This document is prose of the length a specification chapter has,
hard-wrapped the way one is written, so the renderer walks a paragraph
of several lines rather than a single long one.
`)
// Links: two that resolve inside this section, one that resolves in another
// section, one relative link, one external, and one that resolves to nothing
// — which is the case the renderer marks rather than drops, and the case a
// space accumulates as it grows.
fmt.Fprintf(&b, "See [[doc-%03d]] and [[doc-%03d]] for the neighbouring rules, "+
"and [[index]] for the section itself.\n\n",
(i+1)%benchPerSection, (i+2)%benchPerSection)
fmt.Fprintf(&b, "Across sections: [[../section-%d/doc-%03d]] and the alias [[d-%d-%d]].\n\n",
(section+1)%benchSections, i, (section+1)%benchSections, i)
fmt.Fprintf(&b, "A relative link to [the sibling](doc-%03d.md), an external one to\n"+
"<https://spec.srht.bigb.es/>, and [[a-document-nobody-wrote-%d]] which\n"+
"resolves to nothing at all.\n\n", (i+3)%benchPerSection, i)
b.WriteString(`## Requirements
1. The reader must be able to address a revision by its hash.
2. The writer must not be able to rewrite the approved branch.
3. A proposal must name the revision it was cut from.
| field | required | note |
| ----- | -------- | ---- |
| id | yes | stable across revisions |
| title | yes | shown in every listing |
| status | no | draft when absent |
` + "```yaml" + `
id: SPEC-0000
title: the example
status: draft
` + "```" + `
> A block quote, because a specification always has one.
`)
return b.String()
}
// benchBodies is the path → raw markdown map LinkPass reads, which is the map
// a caller already holds from the same read that produced the documents.
func benchBodies(docs []gitx.Document) map[string][]byte {
bodies := make(map[string][]byte, len(docs))
for _, d := range docs {
bodies[d.Path] = d.Data
}
return bodies
}
// BenchmarkFromDocuments builds the archive: every header parsed, the id
// contest decided over the whole revision, the hierarchy linked, and the alias
// and stem indexes filled.
func BenchmarkFromDocuments(b *testing.B) {
docs := benchDocuments()
b.ReportAllocs()
for b.Loop() {
arc := FromDocuments(benchSpace, benchRev, docs)
// A build that indexed nothing would be the fastest one here.
if len(arc.All()) != len(docs) {
b.Fatalf("archive holds %d pages, want %d", len(arc.All()), len(docs))
}
}
}
// BenchmarkLinkPass renders every document in the archive through the markdown
// renderer and resolves its links — the pass that fills the link graph the
// backlink and orphan views read.
func BenchmarkLinkPass(b *testing.B) {
docs := benchDocuments()
bodies := benchBodies(docs)
r := NewRenderer()
b.ReportAllocs()
for b.Loop() {
b.StopTimer()
// A fresh archive per iteration: LinkPass writes Page.Links, so reusing
// one would measure the second pass over an already-linked graph.
arc := FromDocuments(benchSpace, benchRev, docs)
b.StartTimer()
if err := arc.LinkPass(r, bodies); err != nil {
b.Fatalf("link pass: %v", err)
}
linked := 0
for _, p := range arc.All() {
linked += len(p.Links)
}
if linked == 0 {
b.Fatal("the link pass resolved no outbound link at all")
}
}
}
// BenchmarkRenderDocument is one document rendered on its own, against the
// archive as resolver — the per-request half of the two passes above, and what
// a reader opening a single page pays.
func BenchmarkRenderDocument(b *testing.B) {
docs := benchDocuments()
arc := FromDocuments(benchSpace, benchRev, docs)
r := NewRenderer()
// Named rather than indexed: a document from the middle of the corpus, and
// deliberately not a section index — an index carries only wikilinks that
// resolve, so it would leave the missing-link path of the renderer unmeasured.
want := fmt.Sprintf("section-%d/doc-%03d.md", benchSections/2, benchPerSection/2)
var src gitx.Document
for _, d := range docs {
if d.Path == want {
src = d
break
}
}
if src.Data == nil {
b.Fatalf("no document at %s in the corpus", want)
}
_, body := ParseFront(src.Data)
dir := DirOf(src.Path)
b.ReportAllocs()
b.SetBytes(int64(len(body)))
for b.Loop() {
res := r.Render(body, dir, arc)
if len(res.LinkedIDs) == 0 {
b.Fatal("the document resolved no outbound link; the resolver is not being exercised")
}
if len(res.MissingWikilinks) == 0 {
b.Fatal("the deliberately unresolvable wikilink resolved; the fixture drifted")
}
}
}