kitty/tools/cmd/benchmark/main.go
Kovid Goyal c1793d8781
Pause rendering per repetition
Needed when number of repetitions is large enough to cause
paused rendering to be aborted
2024-02-25 09:57:33 +05:30

361 lines
11 KiB
Go

// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package benchmark
import (
"bytes"
"fmt"
"math/rand"
"strings"
"sync"
"time"
"kitty/tools/cli"
"kitty/tools/tty"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
type Options struct {
Repetitions int
WithScrollback bool
Render bool
}
const reset = "\x1b]\x1b\\\x1bc"
const ascii_printable = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \n\t `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?"
const chinese_lorem_ipsum = `
旦海司有幼雞讀松鼻種比門真目怪少:扒裝虎怕您跑綠蝶黃,位香法士錯乙音造活羽詞坡村目園尺封鳥朋;法松夕點我冬停雪因科對只貓息加黃住蝶,明鴨乾春呢風乙時昔孝助?小紅女父故去。
飯躲裝個哥害共買去隻把氣年,己你校跟飛百拉!快石牙飽知唱想土人吹象毛吉每浪四又連見、欠耍外豆雞秋鼻。住步帶。
打六申幾麼:或皮又荷隻乙犬孝習秋還何氣;幾裏活打能花是入海乙山節會。種第共後陽沒喜姐三拍弟海肖,行知走亮包,他字幾,的木卜流旦乙左杯根毛。
您皮買身苦八手牛目地止哥彩第合麻讀午。原朋河乾種果「才波久住這香松」兄主衣快他玉坐要羽和亭但小山吉也吃耳怕,也爪斗斥可害朋許波怎祖葉卜。
行花兩耍許車丟學「示想百吃門高事」不耳見室九星枝買裝,枝十新央發旁品丁青給,科房火;事出出孝肉古:北裝愛升幸百東鼻到從會故北「可休笑物勿三游細斗」娘蛋占犬。我羊波雨跳風。
牛大燈兆新七馬,叫這牙後戶耳、荷北吃穿停植身玩間告或西丟再呢,他禾七愛干寺服石安:他次唱息它坐屋父見這衣發現來,苗會開條弓世者吃英定豆哭;跳風掃叫美神。
寸再了耍休壯植己,燈錯和,蝶幾欠雞定和愛,司紅後弓第樹會金拉快喝夕見往,半瓜日邊出讀雞苦歌許開;發火院爸乙;四帶亮錯鳥洋個讀。
`
const misc_unicode = `
‘’“”‹›«»‚„ 😀😛😇😈😉😍😎😮👍👎 —–§¶†‡©®™ →⇒•·°±−×÷¼½½¾
…µ¢£€¿¡¨´¸ˆ˜ ÀÁÂÃÄÅÆÇÈÉÊË ÌÍÎÏÐÑÒÓÔÕÖØ ŒŠÙÚÛÜÝŸÞßàá âãäåæçèéêëìí
îïðñòóôõöøœš ùúûüýÿþªºαΩ∞
`
var opts Options
func benchmark_data(description string, data string, opts Options) (duration time.Duration, sent_data_size int, reps int, err error) {
term, err := tty.OpenControllingTerm(tty.SetRaw)
if err != nil {
return 0, 0, 0, err
}
defer term.RestoreAndClose()
state := loop.TerminalStateOptions{Alternate_screen: !opts.WithScrollback}
if _, err = term.WriteString(state.SetStateEscapeCodes()); err != nil {
return
}
defer func() { _, _ = term.WriteString(state.ResetStateEscapeCodes() + reset) }()
lock := sync.Mutex{}
const count = 3
goroutine_started := make(chan byte)
go func() {
lock.Lock()
defer lock.Unlock()
buf := make([]byte, 8192)
var data []byte
q := []byte(strings.Repeat("\x1b[0n", count))
goroutine_started <- 'y'
for !bytes.Contains(data, q) {
n, err := term.Read(buf)
if err != nil {
break
}
data = append(data, buf[:n]...)
}
}()
<-goroutine_started
const clear_screen = "\x1b[H\x1b[2J\x1b[m"
desc := clear_screen + "Running: " + description + "\r\n"
const pause_rendering = "\x1b[?2026h"
const resume_rendering = "\x1b[?2026l"
if !opts.Render {
if _, err = term.WriteString(desc + pause_rendering); err != nil {
return
}
}
start := time.Now()
for reps < opts.Repetitions {
if _, err = term.WriteString(data); err != nil {
return
}
sent_data_size += len(data)
reps += 1
if !opts.Render {
if _, err = term.WriteString(desc + resume_rendering + pause_rendering); err != nil {
return
}
}
}
if _, err = term.WriteString(clear_screen + "Waiting for response indicating parsing finished\r\n" + strings.Repeat("\x1b[5n", count)); err != nil {
return
}
if !opts.Render {
if _, err = term.WriteString(resume_rendering); err != nil {
return
}
}
lock.Lock()
duration = time.Since(start)
lock.Unlock()
return
}
var rand_src = sync.OnceValue(func() *rand.Rand {
return rand.New(rand.NewSource(time.Now().UnixNano()))
})
func random_string_of_bytes(n int, alphabet string) string {
b := make([]byte, n)
al := len(alphabet)
src := rand_src()
for i := 0; i < n; i++ {
b[i] = alphabet[src.Intn(al)]
}
return utils.UnsafeBytesToString(b)
}
type result struct {
desc string
data_sz int
duration time.Duration
repetitions int
}
func simple_ascii() (r result, err error) {
const desc = "Only ASCII chars"
data := random_string_of_bytes(1024*2048+13, ascii_printable)
duration, data_sz, reps, err := benchmark_data(desc, data, opts)
if err != nil {
return result{}, err
}
return result{desc, data_sz, duration, reps}, nil
}
func unicode() (r result, err error) {
const desc = "Unicode chars"
data := strings.Repeat(chinese_lorem_ipsum+misc_unicode, 1024)
duration, data_sz, reps, err := benchmark_data(desc, data, opts)
if err != nil {
return result{}, err
}
return result{desc, data_sz, duration, reps}, nil
}
func ascii_with_csi() (r result, err error) {
const sz = 1024*1024 + 17
out := make([]byte, 0, sz+48)
src := rand_src()
chunk := ""
for len(out) < sz {
q := src.Intn(100)
switch {
case (q < 10):
chunk = random_string_of_bytes(src.Intn(72)+1, ascii_printable)
case (10 <= q && q < 30):
chunk = "\x1b[m\x1b[?1h\x1b[H"
case (30 <= q && q < 40):
chunk = "\x1b[1;2;3;4:3;31m"
case (40 <= q && q < 50):
chunk = "\x1b[38:5:24;48:2:125:136:147m"
case (50 <= q && q < 60):
chunk = "\x1b[58;5;44;2m"
case (60 <= q && q < 80):
chunk = "\x1b[m\x1b[10A\x1b[3E\x1b[2K"
case (80 <= q && q < 100):
chunk = "\x1b[39m\x1b[10`a\x1b[100b\x1b[?1l"
}
out = append(out, utils.UnsafeStringToBytes(chunk)...)
}
const desc = "CSI codes with few chars"
duration, data_sz, reps, err := benchmark_data(desc, utils.UnsafeBytesToString(out), opts)
if err != nil {
return result{}, err
}
return result{desc, data_sz, duration, reps}, nil
}
func images() (r result, err error) {
g := graphics.GraphicsCommand{}
g.SetImageId(12345)
g.SetQuiet(graphics.GRT_quiet_silent)
g.SetAction(graphics.GRT_action_transmit)
g.SetFormat(graphics.GRT_format_rgba)
const dim = 1024
g.SetDataWidth(dim)
g.SetDataHeight(dim)
g.DisableCompression = true // dont want to measure the speed of zlib
b := strings.Builder{}
b.Grow(8 * dim * dim)
_ = g.WriteWithPayloadTo(&b, make([]byte, 4*dim*dim))
g.SetAction(graphics.GRT_action_delete)
g.SetDelete(graphics.GRT_free_by_id)
_ = g.WriteWithPayloadTo(&b, nil)
data := b.String()
const desc = "Images"
duration, data_sz, reps, err := benchmark_data(desc, data, opts)
if err != nil {
return result{}, err
}
return result{desc, data_sz, duration, reps}, nil
}
func long_escape_codes() (r result, err error) {
data := random_string_of_bytes(8024, ascii_printable)
// OSC 6 is document reporting which kitty ignores after parsing
data = strings.Repeat("\x1b]6;"+data+"\x07", 1024)
const desc = "Long escape codes"
duration, data_sz, reps, err := benchmark_data(desc, data, opts)
if err != nil {
return result{}, err
}
return result{desc, data_sz, duration, reps}, nil
}
var divs = []time.Duration{
time.Duration(1), time.Duration(10), time.Duration(100), time.Duration(1000)}
func round(d time.Duration, digits int) time.Duration {
switch {
case d > time.Second:
d = d.Round(time.Second / divs[digits])
case d > time.Millisecond:
d = d.Round(time.Millisecond / divs[digits])
case d > time.Microsecond:
d = d.Round(time.Microsecond / divs[digits])
}
return d
}
func present_result(r result, col_width int) {
rate := float64(r.data_sz) / r.duration.Seconds()
rate /= 1024. * 1024.
f := fmt.Sprintf("%%-%ds", col_width)
fmt.Printf(" "+f+" : %-10v @ \x1b[32m%-7.1f\x1b[m MB/s\n", r.desc, round(r.duration, 2), rate)
}
func all_benchamrks() []string {
return []string{
"ascii", "unicode", "csi", "images", "long_escape_codes",
}
}
func main(args []string) (err error) {
if len(args) == 0 {
args = all_benchamrks()
}
var results []result
var r result
// First warm up the terminal by getting it to render all chars so that font rendering
// time is not polluting the benchmarks.
w := Options{Repetitions: 1}
if _, _, _, err = benchmark_data("Warmup", ascii_printable+chinese_lorem_ipsum+misc_unicode, w); err != nil {
return err
}
time.Sleep(time.Second / 2)
if slices.Index(args, "ascii") >= 0 {
if r, err = simple_ascii(); err != nil {
return err
}
results = append(results, r)
}
if slices.Index(args, "unicode") >= 0 {
if r, err = unicode(); err != nil {
return err
}
results = append(results, r)
}
if slices.Index(args, "csi") >= 0 {
if r, err = ascii_with_csi(); err != nil {
return err
}
results = append(results, r)
}
if slices.Index(args, "long_escape_codes") >= 0 {
if r, err = long_escape_codes(); err != nil {
return err
}
results = append(results, r)
}
if slices.Index(args, "images") >= 0 {
if r, err = images(); err != nil {
return err
}
results = append(results, r)
}
fmt.Print(reset)
fmt.Println(
"These results measure the time it takes the terminal to fully parse all the data sent to it.")
if opts.Render {
fmt.Println("Note that not all data transmitted will be displayed as input parsing is typically asynchronous with rendering in high performance terminals.")
} else {
fmt.Println("Note that \x1b[31mrendering is suppressed\x1b[m (if the terminal supports the synchronized output escape code) to better benchmark parser performance. Use the --render flag to enable rendering.")
}
fmt.Println()
fmt.Println("Results:")
mlen := 10
for _, r := range results {
mlen = max(mlen, len(r.desc))
}
for _, r := range results {
present_result(r, mlen)
}
return
}
func EntryPoint(root *cli.Command) {
sc := root.AddSubCommand(&cli.Command{
Name: "__benchmark__",
ShortDescription: "Run various benchmarks",
HelpText: "To run only particular benchmarks, specify them on the command line from the set: " + strings.Join(all_benchamrks(), ", "),
Usage: "[options] [optional benchmark to run ...]",
Hidden: true,
Run: func(cmd *cli.Command, args []string) (ret int, err error) {
if err = cmd.GetOptionValues(&opts); err != nil {
return 1, err
}
opts.Repetitions = max(1, opts.Repetitions)
if err = main(args); err != nil {
ret = 1
}
return
},
})
sc.Add(cli.OptionSpec{
Name: "--repetitions",
Default: "10",
Type: "int",
Help: "The number of repetitions of each benchmark",
})
sc.Add(cli.OptionSpec{
Name: "--with-scrollback",
Type: "bool-set",
Help: "Use the main screen instead of the alt screen so speed of scrollback is also tested",
})
sc.Add(cli.OptionSpec{
Name: "--render",
Type: "bool-set",
Help: "Allow rendering of the data sent during tests. Note that modern terminals render asynchronously, so timings do not generally reflect render performance.",
})
}