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"+ ", 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") } } }