Something went wrong. Try again.
Go bindings for libghostty-vt. pkg.go.dev/go.mitchellh.com/libghostty
ghostty
Something went wrong. Try again.
go-libghostty render_state_row_benchmark_test.go
4.6 kB · 179 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180package libghostty
import ( "strings" "testing")
// rowTextBenchmarkCase is a line of terminal content. The benchmark fills an// 80x30 screen with it and reads back the text of every row, which is how a// program watching a terminal would typically use AppendText.type rowTextBenchmarkCase struct { name string line string}
var rowTextBenchmarkCases = []rowTextBenchmarkCase{ { name: "Plain", line: "plain ASCII terminal content with words, numbers 0123456789, and punctuation", }, { name: "Sparse", line: "short line", }, { name: "Mixed", line: "\x1b[1;38;5;33mstatus\x1b[0m plain 日本語 é 👩🏽💻 \x1b[4munderlined\x1b[0m", },}
// appendRowTextPerCell produces the same text as AppendText using only the// per-cell API from Go, which is what callers had to do before AppendText// existed. It looks up the cell width only for empty cells, since those are// the only cells where it changes the result.//// With checkWide false, it is the cheapest per-cell loop possible, with two// calls into libghostty per cell (Next and AppendGraphemes). That version// writes an extra space after each wide character, so it is only useful to// show the lowest cost a per-cell loop can reach.func appendRowTextPerCell(dst []byte, ri *RenderStateRowIterator, rc *RenderStateRowCells, checkWide bool) ([]byte, error) { if err := ri.Cells(rc); err != nil { return dst, err }
blanks := 0 for rc.Next() { // Write the pending spaces first. If this cell turns out to be // empty too, they are removed again below. mark := len(dst) for range blanks { dst = append(dst, ' ') } before := len(dst)
var err error dst, err = rc.AppendGraphemes(dst) if err != nil { return dst, err } if len(dst) > before { blanks = 0 continue } dst = dst[:mark]
if checkWide { cell, err := rc.Raw() if err != nil { return dst, err } wide, err := cell.Wide() if err != nil { return dst, err } if wide == CellWideSpacerTail || wide == CellWideSpacerHead { continue } } blanks++ } return dst, nil}
// BenchmarkRenderStateRowText compares reading the text of a full screen with// AppendText, which makes one call into libghostty per row, against the// per-cell loops above, which make two or more calls per cell.func BenchmarkRenderStateRowText(b *testing.B) { methods := []struct { name string fn func([]byte, *RenderStateRowIterator, *RenderStateRowCells) ([]byte, error) }{ { name: "AppendText", fn: func(dst []byte, ri *RenderStateRowIterator, rc *RenderStateRowCells) ([]byte, error) { return ri.AppendText(dst, rc) }, }, { name: "PerCell", fn: func(dst []byte, ri *RenderStateRowIterator, rc *RenderStateRowCells) ([]byte, error) { return appendRowTextPerCell(dst, ri, rc, true) }, }, { name: "PerCellMinimal", fn: func(dst []byte, ri *RenderStateRowIterator, rc *RenderStateRowCells) ([]byte, error) { return appendRowTextPerCell(dst, ri, rc, false) }, }, }
for _, tc := range rowTextBenchmarkCases { for _, m := range methods { b.Run(tc.name+"/"+m.name, func(b *testing.B) { // Fill an 80x30 screen with the line on every row. term, err := NewTerminal(WithSize(80, 30), WithMaxScrollbackLines(0)) if err != nil { b.Fatal(err) } defer term.Close() term.VTWrite([]byte(strings.Repeat(tc.line+"\r\n", 29) + tc.line))
rs, err := NewRenderState() if err != nil { b.Fatal(err) } defer rs.Close() if err := rs.Update(term); err != nil { b.Fatal(err) }
ri, err := NewRenderStateRowIterator() if err != nil { b.Fatal(err) } defer ri.Close()
rc, err := NewRenderStateRowCells() if err != nil { b.Fatal(err) } defer rc.Close()
// Make sure AppendText and the correct per-cell loop produce // the same text before timing anything. if err := rs.RowIterator(ri); err != nil { b.Fatal(err) } for ri.Next() { want, err := ri.AppendText(nil, rc) if err != nil { b.Fatal(err) } got, err := appendRowTextPerCell(nil, ri, rc, true) if err != nil { b.Fatal(err) } if string(want) != string(got) { b.Fatalf("methods disagree: %q vs %q", want, got) } }
buf := make([]byte, 0, 4096) b.ReportAllocs() for b.Loop() { if err := rs.RowIterator(ri); err != nil { b.Fatal(err) } for ri.Next() { buf, err = m.fn(buf[:0], ri, rc) if err != nil { b.Fatal(err) } } } }) } }}