// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "fmt" "testing" "time" "unicode/utf8" "git.stormux.org/storm/cthulhu/internal/atspi" ) // Include focus synchronization and worker bookkeeping, which the primitive // heading benchmark intentionally excludes. Native provider work is separate. func BenchmarkDocumentWorkerHeadingNavigation(b *testing.B) { for _, contentNodes := range []int{100, 1000, 10000} { b.Run(fmt.Sprintf("nodes-%d", contentNodes), func(b *testing.B) { tree, ids := benchmark_document_tree(b, contentNodes) cursor, err := NewDocumentCursor(tree) if err != nil { b.Fatal(err) } worker, err := NewWorker(&fakeCapturer{}, time.Second) if err != nil { b.Fatal(err) } middle := contentNodes / 40 * 20 worker.SyncFocus(ids[middle]) ctx := context.Background() if _, err := cursor.SyncFocus(ctx, ids[middle]); err != nil { b.Fatal(err) } b.ReportAllocs() for b.Loop() { for _, backwards := range []bool{false, true} { result, next := worker.moveDocument(ctx, cursor, DocumentHeading, 2, backwards, 0, 0) cursor = next want := ids[middle+20] if backwards { want = ids[middle] } if result.Err != nil || !result.Moved || result.DocumentPosition.Object.ID() != want || result.DocumentDiagnostics.CaptureAttempts != 0 { b.Fatalf("worker move: moved=%v error=%v", result.Moved, result.Err) } } } }) } } func BenchmarkDocumentCaretNavigation(b *testing.B) { for _, contentNodes := range []int{100, 1000, 10000} { b.Run(fmt.Sprintf("nodes-%d", contentNodes), func(b *testing.B) { tree, ids := benchmark_document_tree(b, contentNodes) cursor, err := NewDocumentCursor(tree) if err != nil { b.Fatal(err) } middle := contentNodes/2 + 2 ctx := context.Background() if _, err := cursor.SyncFocus(ctx, ids[middle]); err != nil { b.Fatal(err) } reader := caretTextReader{ids[middle]: fmt.Sprintf("Café sample %d", middle)} // Build the view's reusable ranges before measuring repeated keys. for _, command := range []DocumentCaretCommand{CaretNextCharacter, CaretPreviousCharacter} { if _, err := cursor.MoveCaret(ctx, reader, command); err != nil { b.Fatal(err) } } b.ReportAllocs() for b.Loop() { for _, command := range []DocumentCaretCommand{CaretNextCharacter, CaretPreviousCharacter} { move, err := cursor.MoveCaret(ctx, reader, command) want := 1 if command == CaretPreviousCharacter { want = 0 } if err != nil || !move.Moved || move.Position.Object.ID() != ids[middle] || move.Position.Offset != want { b.Fatalf("caret move: moved=%v error=%v", move.Moved, err) } } } }) } } // benchmark_document_tree models an already captured page, not native AT-SPI // capture. Each section has twenty content objects: a heading, a link, and // ordinary text. Pairs of objects share a visual row, including Unicode text. func benchmark_document_tree(b *testing.B, contentNodes int) (*atspi.AccessibleTree, []atspi.ObjectID) { b.Helper() rootID := webID("/benchmark/document") root := atspi.NewSnapshot(rootID.Path, "Benchmark page", "document web", "", "") root.BusName = rootID.BusName nodes := []atspi.TreeNode{{Snapshot: root}} contentIDs := make([]atspi.ObjectID, contentNodes) sectionIndex := 0 for index := range contentNodes { if index%20 == 0 { sectionID := webID(fmt.Sprintf("/benchmark/section_%d", index/20)) section := atspi.NewSnapshot(sectionID.Path, "", "section", "", "") section.BusName = sectionID.BusName nodes[0].Children = append(nodes[0].Children, sectionID) sectionIndex = len(nodes) nodes = append(nodes, atspi.TreeNode{Snapshot: section}) } id := webID(fmt.Sprintf("/benchmark/content_%d", index)) contentIDs[index] = id role, text := "static", fmt.Sprintf("Café sample %d", index) switch index % 20 { case 0: role, text = "heading", fmt.Sprintf("Heading %d", index/20) case 1: role, text = "link", fmt.Sprintf("Link %d", index/20) } snapshot := atspi.NewSnapshot(id.Path, text, role, "", text) snapshot.BusName = id.BusName snapshot.CharacterCount = utf8.RuneCountInString(text) if role == "heading" { snapshot.HeadingLevel = 2 } bounds := atspi.Rectangle{X: int32(index%2) * 200, Y: int32(index/2) * 24, Width: 180, Height: 20} nodes[sectionIndex].Children = append(nodes[sectionIndex].Children, id) nodes = append(nodes, atspi.TreeNode{ Snapshot: snapshot, Geometry: atspi.Geometry{Bounds: bounds, BoundsKnown: true, VisibilityKnown: true, Visible: true, Showing: true}, Text: atspi.VisibleText{Known: true, Ranges: []atspi.TextRange{{ Start: 0, End: snapshot.CharacterCount, Text: text, Bounds: bounds, }}}, }) } tree, err := atspi.NewAccessibleTree(rootID, nodes) if err != nil { b.Fatal(err) } return tree, contentIDs } func BenchmarkDocumentSnapshot(b *testing.B) { for _, contentNodes := range []int{100, 1000, 10000} { b.Run(fmt.Sprintf("nodes-%d", contentNodes), func(b *testing.B) { tree, _ := benchmark_document_tree(b, contentNodes) root, err := tree.Root() if err != nil { b.Fatal(err) } ctx := context.Background() wantNodes := contentNodes + contentNodes/20 + 1 b.ReportAllocs() for b.Loop() { document, err := tree.Document(ctx, root) if err != nil || len(document.Nodes) != wantNodes { b.Fatalf("document snapshot: nodes=%d, error=%v", len(document.Nodes), err) } } }) } } func BenchmarkDocumentHeadingNavigation(b *testing.B) { for _, contentNodes := range []int{100, 1000, 10000} { for _, scenario := range []string{"adjacent", "wrap"} { b.Run(fmt.Sprintf("nodes-%d/%s", contentNodes, scenario), func(b *testing.B) { tree, contentIDs := benchmark_document_tree(b, contentNodes) cursor, err := NewDocumentCursor(tree) if err != nil { b.Fatal(err) } ctx := context.Background() startIndex := contentNodes / 40 * 20 wantWrapped := scenario == "wrap" if wantWrapped { startIndex = contentNodes - 20 } nextIndex := (startIndex + 20) % contentNodes if _, err := cursor.SyncFocus(ctx, contentIDs[startIndex]); err != nil { b.Fatal(err) } b.ReportAllocs() for b.Loop() { // One operation is a round trip. Returning to the same heading // keeps the workload independent of the calibrated loop count. move, err := cursor.Move(ctx, DocumentHeading, 2, false) if err != nil || !move.Moved || move.Position.Object.ID() != contentIDs[nextIndex] || move.Wrapped != wantWrapped { b.Fatalf("forward heading navigation: moved=%v, error=%v", move.Moved, err) } move, err = cursor.Move(ctx, DocumentHeading, 2, true) if err != nil || !move.Moved || move.Position.Object.ID() != contentIDs[startIndex] || move.Wrapped != wantWrapped { b.Fatalf("backward heading navigation: moved=%v, error=%v", move.Moved, err) } } }) } } } func BenchmarkDocumentLineAssembly(b *testing.B) { for _, contentNodes := range []int{100, 1000, 10000} { b.Run(fmt.Sprintf("nodes-%d", contentNodes), func(b *testing.B) { tree, contentIDs := benchmark_document_tree(b, contentNodes) middle := contentNodes / 2 reference, err := tree.Reference(contentIDs[middle]) if err != nil { b.Fatal(err) } ctx := context.Background() b.ReportAllocs() for b.Loop() { line, err := tree.DocumentLineAtOffset(ctx, reference, 1) if err != nil || len(line.Contents) != 2 { b.Fatalf("line assembly: contents=%d, error=%v", len(line.Contents), err) } if line.Contents[0].Reference.ID() != contentIDs[middle] || line.Contents[1].Reference.ID() != contentIDs[middle+1] { b.Fatal("line assembly returned the wrong visual row") } } }) } }