main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package benchmark
  3. import (
  4. "bytes"
  5. "errors"
  6. "fmt"
  7. "math/rand/v2"
  8. "slices"
  9. "strings"
  10. "time"
  11. "kitty/tools/cli"
  12. "kitty/tools/tty"
  13. "kitty/tools/tui/graphics"
  14. "kitty/tools/tui/loop"
  15. "kitty/tools/utils"
  16. "golang.org/x/sys/unix"
  17. )
  18. var _ = fmt.Print
  19. type Options struct {
  20. Repetitions int
  21. WithScrollback bool
  22. Render bool
  23. }
  24. const reset = "\x1b]\x1b\\\x1bc"
  25. const ascii_printable = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \n\t `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?"
  26. const chinese_lorem_ipsum = `
  27. 旦海司有幼雞讀松鼻種比門真目怪少:扒裝虎怕您跑綠蝶黃,位香法士錯乙音造活羽詞坡村目園尺封鳥朋;法松夕點我冬停雪因科對只貓息加黃住蝶,明鴨乾春呢風乙時昔孝助?小紅女父故去。
  28. 飯躲裝個哥害共買去隻把氣年,己你校跟飛百拉!快石牙飽知唱想土人吹象毛吉每浪四又連見、欠耍外豆雞秋鼻。住步帶。
  29. 打六申幾麼:或皮又荷隻乙犬孝習秋還何氣;幾裏活打能花是入海乙山節會。種第共後陽沒喜姐三拍弟海肖,行知走亮包,他字幾,的木卜流旦乙左杯根毛。
  30. 您皮買身苦八手牛目地止哥彩第合麻讀午。原朋河乾種果「才波久住這香松」兄主衣快他玉坐要羽和亭但小山吉也吃耳怕,也爪斗斥可害朋許波怎祖葉卜。
  31. 行花兩耍許車丟學「示想百吃門高事」不耳見室九星枝買裝,枝十新央發旁品丁青給,科房火;事出出孝肉古:北裝愛升幸百東鼻到從會故北「可休笑物勿三游細斗」娘蛋占犬。我羊波雨跳風。
  32. 牛大燈兆新七馬,叫這牙後戶耳、荷北吃穿停植身玩間告或西丟再呢,他禾七愛干寺服石安:他次唱息它坐屋父見這衣發現來,苗會開條弓世者吃英定豆哭;跳風掃叫美神。
  33. 寸再了耍休壯植己,燈錯和,蝶幾欠雞定和愛,司紅後弓第樹會金拉快喝夕見往,半瓜日邊出讀雞苦歌許開;發火院爸乙;四帶亮錯鳥洋個讀。
  34. `
  35. const misc_unicode = `
  36. ‘’“”‹›«»‚„ 😀😛😇😈😉😍😎😮👍👎 —–§¶†‡©®™ →⇒•·°±−×÷¼½½¾
  37. …µ¢£€¿¡¨´¸ˆ˜ ÀÁÂÃÄÅÆÇÈÉÊË ÌÍÎÏÐÑÒÓÔÕÖØ ŒŠÙÚÛÜÝŸÞßàá âãäåæçèéêëìí
  38. îïðñòóôõöøœš ùúûüýÿþªºαΩ∞
  39. `
  40. var opts Options
  41. func benchmark_data(description string, data string, opts Options) (duration time.Duration, sent_data_size int, reps int, err error) {
  42. term, err := tty.OpenControllingTerm(tty.SetRaw)
  43. if err != nil {
  44. return 0, 0, 0, err
  45. }
  46. defer term.RestoreAndClose()
  47. write_with_retry := func(data string) (err error) {
  48. return term.WriteAllString(data)
  49. }
  50. state := loop.TerminalStateOptions{Alternate_screen: !opts.WithScrollback}
  51. if err = write_with_retry(state.SetStateEscapeCodes() + loop.DECTCEM.EscapeCodeToReset()); err != nil {
  52. return
  53. }
  54. defer func() { _ = write_with_retry(state.ResetStateEscapeCodes() + loop.DECTCEM.EscapeCodeToSet() + reset) }()
  55. const count = 3
  56. const clear_screen = "\x1b[m\x1b[H\x1b[2J"
  57. desc := clear_screen + "Running: " + description + "\r\n"
  58. const pause_rendering = "\x1b[?2026h"
  59. const resume_rendering = "\x1b[?2026l"
  60. if !opts.Render {
  61. if err = write_with_retry(desc + pause_rendering); err != nil {
  62. return
  63. }
  64. }
  65. start := time.Now()
  66. end_of_loop_reset := desc
  67. if !opts.Render {
  68. end_of_loop_reset += resume_rendering + pause_rendering
  69. }
  70. for reps < opts.Repetitions {
  71. if err = write_with_retry(data); err != nil {
  72. return
  73. }
  74. sent_data_size += len(data)
  75. reps += 1
  76. if err = write_with_retry(end_of_loop_reset); err != nil {
  77. return
  78. }
  79. }
  80. finalize := clear_screen + "Waiting for response indicating parsing finished\r\n"
  81. if !opts.Render {
  82. finalize += resume_rendering
  83. }
  84. finalize += strings.Repeat("\x1b[5n", count)
  85. if err = write_with_retry(finalize); err != nil {
  86. return
  87. }
  88. q := []byte(strings.Repeat("\x1b[0n", count))
  89. var read_data []byte
  90. buf := make([]byte, 8192)
  91. for !bytes.Contains(read_data, q) {
  92. n, err := term.Read(buf)
  93. if err != nil {
  94. if (errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR)) && n == 0 {
  95. continue
  96. }
  97. break
  98. }
  99. read_data = append(read_data, buf[:n]...)
  100. }
  101. duration = time.Since(start)
  102. return
  103. }
  104. func random_string_of_bytes(n int, alphabet string) string {
  105. b := make([]byte, n)
  106. al := len(alphabet)
  107. for i := 0; i < n; i++ {
  108. b[i] = alphabet[rand.IntN(al)]
  109. }
  110. return utils.UnsafeBytesToString(b)
  111. }
  112. type result struct {
  113. desc string
  114. data_sz int
  115. duration time.Duration
  116. repetitions int
  117. }
  118. func simple_ascii() (r result, err error) {
  119. const desc = "Only ASCII chars"
  120. data := random_string_of_bytes(1024*2048+13, ascii_printable)
  121. duration, data_sz, reps, err := benchmark_data(desc, data, opts)
  122. if err != nil {
  123. return result{}, err
  124. }
  125. return result{desc, data_sz, duration, reps}, nil
  126. }
  127. func unicode() (r result, err error) {
  128. const desc = "Unicode chars"
  129. data := strings.Repeat(chinese_lorem_ipsum+misc_unicode, 1024)
  130. duration, data_sz, reps, err := benchmark_data(desc, data, opts)
  131. if err != nil {
  132. return result{}, err
  133. }
  134. return result{desc, data_sz, duration, reps}, nil
  135. }
  136. func ascii_with_csi() (r result, err error) {
  137. const sz = 1024*1024 + 17
  138. out := make([]byte, 0, sz+48)
  139. chunk := ""
  140. for len(out) < sz {
  141. q := rand.IntN(100)
  142. switch {
  143. case (q < 10):
  144. chunk = random_string_of_bytes(rand.IntN(72)+1, ascii_printable)
  145. case (10 <= q && q < 30):
  146. chunk = "\x1b[m\x1b[?1h\x1b[H"
  147. case (30 <= q && q < 40):
  148. chunk = "\x1b[1;2;3;4:3;31m"
  149. case (40 <= q && q < 50):
  150. chunk = "\x1b[38:5:24;48:2:125:136:147m"
  151. case (50 <= q && q < 60):
  152. chunk = "\x1b[58;5;44;2m"
  153. case (60 <= q && q < 80):
  154. chunk = "\x1b[m\x1b[10A\x1b[3E\x1b[2K"
  155. case (80 <= q && q < 100):
  156. chunk = "\x1b[39m\x1b[10`a\x1b[100b\x1b[?1l"
  157. }
  158. out = append(out, utils.UnsafeStringToBytes(chunk)...)
  159. }
  160. out = append(out, "\x1b[m"...)
  161. const desc = "CSI codes with few chars"
  162. duration, data_sz, reps, err := benchmark_data(desc, utils.UnsafeBytesToString(out), opts)
  163. if err != nil {
  164. return result{}, err
  165. }
  166. return result{desc, data_sz, duration, reps}, nil
  167. }
  168. func images() (r result, err error) {
  169. g := graphics.GraphicsCommand{}
  170. g.SetImageId(12345)
  171. g.SetQuiet(graphics.GRT_quiet_silent)
  172. g.SetAction(graphics.GRT_action_transmit)
  173. g.SetFormat(graphics.GRT_format_rgba)
  174. const dim = 1024
  175. g.SetDataWidth(dim)
  176. g.SetDataHeight(dim)
  177. g.DisableCompression = true // dont want to measure the speed of zlib
  178. b := strings.Builder{}
  179. b.Grow(8 * dim * dim)
  180. _ = g.WriteWithPayloadTo(&b, make([]byte, 4*dim*dim))
  181. g.SetAction(graphics.GRT_action_delete)
  182. g.SetDelete(graphics.GRT_free_by_id)
  183. _ = g.WriteWithPayloadTo(&b, nil)
  184. data := b.String()
  185. const desc = "Images"
  186. duration, data_sz, reps, err := benchmark_data(desc, data, opts)
  187. if err != nil {
  188. return result{}, err
  189. }
  190. return result{desc, data_sz, duration, reps}, nil
  191. }
  192. func long_escape_codes() (r result, err error) {
  193. data := random_string_of_bytes(8024, ascii_printable)
  194. // OSC 6 is document reporting which kitty ignores after parsing
  195. data = strings.Repeat("\x1b]6;"+data+"\x07", 1024)
  196. const desc = "Long escape codes"
  197. duration, data_sz, reps, err := benchmark_data(desc, data, opts)
  198. if err != nil {
  199. return result{}, err
  200. }
  201. return result{desc, data_sz, duration, reps}, nil
  202. }
  203. var divs = []time.Duration{
  204. time.Duration(1), time.Duration(10), time.Duration(100), time.Duration(1000)}
  205. func round(d time.Duration, digits int) time.Duration {
  206. switch {
  207. case d > time.Second:
  208. d = d.Round(time.Second / divs[digits])
  209. case d > time.Millisecond:
  210. d = d.Round(time.Millisecond / divs[digits])
  211. case d > time.Microsecond:
  212. d = d.Round(time.Microsecond / divs[digits])
  213. }
  214. return d
  215. }
  216. func present_result(r result, col_width int) {
  217. rate := float64(r.data_sz) / r.duration.Seconds()
  218. rate /= 1024. * 1024.
  219. f := fmt.Sprintf("%%-%ds", col_width)
  220. fmt.Printf(" "+f+" : %-10v @ \x1b[32m%-7.1f\x1b[m MB/s\n", r.desc, round(r.duration, 2), rate)
  221. }
  222. func all_benchamrks() []string {
  223. return []string{
  224. "ascii", "unicode", "csi", "images", "long_escape_codes",
  225. }
  226. }
  227. func main(args []string) (err error) {
  228. if len(args) == 0 {
  229. args = all_benchamrks()
  230. }
  231. var results []result
  232. var r result
  233. // First warm up the terminal by getting it to render all chars so that font rendering
  234. // time is not polluting the benchmarks.
  235. w := Options{Repetitions: 1}
  236. if _, _, _, err = benchmark_data("Warmup", ascii_printable+chinese_lorem_ipsum+misc_unicode, w); err != nil {
  237. return err
  238. }
  239. time.Sleep(time.Second / 2)
  240. if slices.Index(args, "ascii") >= 0 {
  241. if r, err = simple_ascii(); err != nil {
  242. return err
  243. }
  244. results = append(results, r)
  245. }
  246. if slices.Index(args, "unicode") >= 0 {
  247. if r, err = unicode(); err != nil {
  248. return err
  249. }
  250. results = append(results, r)
  251. }
  252. if slices.Index(args, "csi") >= 0 {
  253. if r, err = ascii_with_csi(); err != nil {
  254. return err
  255. }
  256. results = append(results, r)
  257. }
  258. if slices.Index(args, "long_escape_codes") >= 0 {
  259. if r, err = long_escape_codes(); err != nil {
  260. return err
  261. }
  262. results = append(results, r)
  263. }
  264. if slices.Index(args, "images") >= 0 {
  265. if r, err = images(); err != nil {
  266. return err
  267. }
  268. results = append(results, r)
  269. }
  270. fmt.Print(reset)
  271. fmt.Println(
  272. "These results measure the time it takes the terminal to fully parse all the data sent to it.")
  273. if opts.Render {
  274. fmt.Println("Note that not all data transmitted will be displayed as input parsing is typically asynchronous with rendering in high performance terminals.")
  275. } else {
  276. 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.")
  277. }
  278. fmt.Println()
  279. fmt.Println("Results:")
  280. mlen := 10
  281. for _, r := range results {
  282. mlen = max(mlen, len(r.desc))
  283. }
  284. for _, r := range results {
  285. present_result(r, mlen)
  286. }
  287. return
  288. }
  289. func EntryPoint(root *cli.Command) {
  290. sc := root.AddSubCommand(&cli.Command{
  291. Name: "__benchmark__",
  292. ShortDescription: "Run various benchmarks",
  293. HelpText: "To run only particular benchmarks, specify them on the command line from the set: " + strings.Join(all_benchamrks(), ", ") + ". Benchmarking works by sending large amount of data to the TTY device and waiting for the terminal to process the data and respond to queries sent to it in the data. By default rendering is suppressed during benchmarking to focus on parser performance. Use the --render flag to enable it, but be aware that rendering in modern terminals is typically asynchronous so it wont be properly benchmarked by this kitten.",
  294. Usage: "[options] [optional benchmark to run ...]",
  295. Hidden: true,
  296. Run: func(cmd *cli.Command, args []string) (ret int, err error) {
  297. if err = cmd.GetOptionValues(&opts); err != nil {
  298. return 1, err
  299. }
  300. opts.Repetitions = max(1, opts.Repetitions)
  301. if err = main(args); err != nil {
  302. ret = 1
  303. }
  304. return
  305. },
  306. })
  307. sc.Add(cli.OptionSpec{
  308. Name: "--repetitions",
  309. Default: "100",
  310. Type: "int",
  311. Help: "The number of repetitions of each benchmark",
  312. })
  313. sc.Add(cli.OptionSpec{
  314. Name: "--with-scrollback",
  315. Type: "bool-set",
  316. Help: "Use the main screen instead of the alt screen so speed of scrollback is also tested",
  317. })
  318. sc.Add(cli.OptionSpec{
  319. Name: "--render",
  320. Type: "bool-set",
  321. Help: "Allow rendering of the data sent during tests. Note that modern terminals render asynchronously, so timings do not generally reflect render performance.",
  322. })
  323. }