From ef70b9252eb50444ec4e16d97a7943b655443568 Mon Sep 17 00:00:00 2001 From: Viktor Date: Sat, 20 Jun 2020 23:29:47 +0200 Subject: [PATCH] internal/rendertest: create new standard benchmark Create a standard, representative set of benchmarks for the rendering pipeline to allow for measurement of performance improvement/regressions due to changes. The benchmarks are intended to be representative of the types of drawing different gio uses should encounter. BenchmarkDrawUI: Draw text, instanced shaped and unique shapes in a mix that is reasonable for a simple UI. BenchmarkDrawUICached: Same as BenchmarkDrawUI but not reset between iterations to benchmark the rendering pipeline when using maximum caching. Benchmark1000Circles: Draw 1000 circles individually to benchmark the rendering performance with no caching. Represents usages such as animating shapes or drawing complex shapes. Benchmark1000CirclesInstanced: Draw 1000 circles by calling a Macro op, each one with an offset transform. Represents cases such as drawing spirits etc. Signed-off-by: Viktor --- internal/rendertest/bench_test.go | 293 ++++++++++++++++++++++++++++++ internal/rendertest/util_test.go | 137 ++++++++++++++ 2 files changed, 430 insertions(+) create mode 100644 internal/rendertest/bench_test.go create mode 100644 internal/rendertest/util_test.go diff --git a/internal/rendertest/bench_test.go b/internal/rendertest/bench_test.go new file mode 100644 index 00000000..fbed640b --- /dev/null +++ b/internal/rendertest/bench_test.go @@ -0,0 +1,293 @@ +package rendertest + +import ( + "image" + "image/color" + "math" + "testing" + + "gioui.org/app/headless" + "gioui.org/f32" + "gioui.org/font/gofont" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/widget/material" +) + +// use some global variables for benchmarking so as to not pollute +// the reported allocs with allocations that we do not want to count. +var ( + c1, c2, c3 = make(chan op.CallOp, 0), make(chan op.CallOp, 0), make(chan op.CallOp, 0) + op1, op2, op3 op.Ops +) + +func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, *material.Theme) { + sz := image.Point{X: 1024, Y: 1200} + w, err := headless.NewWindow(sz.X, sz.Y) + if err != nil { + b.Error(err) + } + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Exact(sz), + } + th := material.NewTheme(gofont.Collection()) + return gtx, w, th +} + +func resetOps(gtx layout.Context) { + gtx.Ops.Reset() + op1.Reset() + op2.Reset() + op3.Reset() +} + +func finishBenchmark(b *testing.B, w *headless.Window) { + b.StopTimer() + if *dumpImages { + img, err := w.Screenshot() + w.Release() + if err != nil { + b.Error(err) + } + if err := saveImage(b.Name()+".png", img); err != nil { + b.Error(err) + } + } +} + +func BenchmarkDrawUICached(b *testing.B) { + // As BecnhmarkDraw but the same op.Ops every time that is not reset - this + // should thus allow for maximal cache usage. + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ResetTimer() + for i := 0; i < b.N; i++ { + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func BenchmarkDrawUI(b *testing.B) { + // BenchmarkDraw is intended as a reasonable overall benchmark for + // the drawing performance of the full drawing pipeline, in each iteration + // resetting the ops and drawing, similar to how a typical UI would function. + // This will allow font caching across frames. + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + + p := op.Push(gtx.Ops) + off := float32(math.Mod(float64(i)/10, 10)) + op.TransformOp{}.Offset(f32.Pt(off, off)).Add(gtx.Ops) + + drawCore(gtx, th) + + p.Pop() + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func Benchmark1000Circles(b *testing.B) { + // Benchmark1000Shapes draws 1000 individual shapes such that no caching between + // shapes will be possible and resets buffers on each operation to prevent caching + // between frames. + gtx, w, _ := setupBenchmark(b) + draw1000Circles(gtx) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + draw1000Circles(gtx) + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func Benchmark1000CirclesInstanced(b *testing.B) { + // Like Benchmark1000Circles but will record them and thus allow for caching between + // them. + gtx, w, _ := setupBenchmark(b) + draw1000CirclesInstanced(gtx) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + draw1000CirclesInstanced(gtx) + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func draw1000Circles(gtx layout.Context) { + ops := gtx.Ops + for x := 0; x < 100; x++ { + p := op.Push(ops) + op.TransformOp{}.Offset(f32.Pt(float32(x*10), 0)).Add(ops) + for y := 0; y < 10; y++ { + pi := op.Push(ops) + paint.ColorOp{Color: color.RGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}}.Add(ops) + clip.Rect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Add(ops) + paint.PaintOp{Rect: f32.Rect(0, 0, 10, 10)}.Add(ops) + pi.Pop() + op.TransformOp{}.Offset(f32.Pt(0, float32(100))).Add(ops) + } + p.Pop() + } +} + +func draw1000CirclesInstanced(gtx layout.Context) { + ops := gtx.Ops + + r := op.Record(ops) + clip.Rect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Add(ops) + paint.PaintOp{Rect: f32.Rect(0, 0, 10, 10)}.Add(ops) + c := r.Stop() + + for x := 0; x < 100; x++ { + p := op.Push(ops) + op.TransformOp{}.Offset(f32.Pt(float32(x*10), 0)).Add(ops) + for y := 0; y < 10; y++ { + pi := op.Push(ops) + paint.ColorOp{Color: color.RGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}}.Add(ops) + c.Add(ops) + pi.Pop() + op.TransformOp{}.Offset(f32.Pt(0, float32(100))).Add(ops) + } + p.Pop() + } +} + +func drawCore(gtx layout.Context, th *material.Theme) { + c1 := drawIndividualShapes(gtx, th) + c2 := drawShapeInstances(gtx, th) + c3 := drawText(gtx, th) + + (<-c1).Add(gtx.Ops) + (<-c2).Add(gtx.Ops) + (<-c3).Add(gtx.Ops) +} + +func drawIndividualShapes(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 81 rounded rectangles of different solid colors - each one individually + go func() { + ops := &op1 + c := op.Record(ops) + for x := 0; x < 9; x++ { + p := op.Push(ops) + op.TransformOp{}.Offset(f32.Pt(float32(x*50), 0)).Add(ops) + for y := 0; y < 9; y++ { + pi := op.Push(ops) + paint.ColorOp{Color: color.RGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}}.Add(ops) + clip.Rect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Add(ops) + paint.PaintOp{Rect: f32.Rect(0, 0, 25, 25)}.Add(ops) + pi.Pop() + op.TransformOp{}.Offset(f32.Pt(0, float32(50))).Add(ops) + } + p.Pop() + } + c1 <- c.Stop() + }() + return c1 +} + +func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 400 textured circle instances, each with individual transform + go func() { + ops := &op2 + co := op.Record(ops) + + r := op.Record(ops) + clip.Rect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Add(ops) + paint.PaintOp{Rect: f32.Rect(0, 0, 25, 25)}.Add(ops) + c := r.Stop() + + squares.Add(ops) + rad := float32(0) + for x := 0; x < 20; x++ { + for y := 0; y < 20; y++ { + p := op.Push(ops) + op.TransformOp{}.Offset(f32.Pt(float32(x*50+25), float32(y*50+25))).Add(ops) + c.Add(ops) + p.Pop() + rad += math.Pi * 2 / 400 + } + } + c2 <- co.Stop() + }() + return c2 +} + +func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 40 lines of text with different transforms. + go func() { + ops := &op3 + c := op.Record(ops) + + txt := material.H6(th, "") + for x := 0; x < 40; x++ { + txt.Text = textRows[x] + p := op.Push(ops) + op.TransformOp{}.Offset(f32.Pt(float32(0), float32(24*x))).Add(ops) + gtx.Ops = ops + txt.Layout(gtx) + p.Pop() + } + c3 <- c.Stop() + }() + return c3 +} + +var textRows = []string{ + "1. I learned from my grandfather, Verus, to use good manners, and to", + "put restraint on anger. 2. In the famous memory of my father I had a", + "pattern of modesty and manliness. 3. Of my mother I learned to be", + "pious and generous; to keep myself not only from evil deeds, but even", + "from evil thoughts; and to live with a simplicity which is far from", + "customary among the rich. 4. I owe it to my great-grandfather that I", + "did not attend public lectures and discussions, but had good and able", + "teachers at home; and I owe him also the knowledge that for things of", + "this nature a man should count no expense too great.", + "5. My tutor taught me not to favour either green or blue at the", + "chariot races, nor, in the contests of gladiators, to be a supporter", + "either of light or heavy armed. He taught me also to endure labour;", + "not to need many things; to serve myself without troubling others; not", + "to intermeddle in the affairs of others, and not easily to listen to", + "slanders against them.", + "6. Of Diognetus I had the lesson not to busy myself about vain things;", + "not to credit the great professions of such as pretend to work", + "wonders, or of sorcerers about their charms, and their expelling of", + "Demons and the like; not to keep quails (for fighting or divination),", + "nor to run after such things; to suffer freedom of speech in others,", + "and to apply myself heartily to philosophy. Him also I must thank for", + "my hearing first Bacchius, then Tandasis and Marcianus; that I wrote", + "dialogues in my youth, and took a liking to the philosopher's pallet", + "and skins, and to the other things which, by the Grecian discipline,", + "belong to that profession.", + "7. To Rusticus I owe my first apprehensions that my nature needed", + "reform and cure; and that I did not fall into the ambition of the", + "common Sophists, either by composing speculative writings or by", + "declaiming harangues of exhortation in public; further, that I never", + "strove to be admired by ostentation of great patience in an ascetic", + "life, or by display of activity and application; that I gave over the", + "study of rhetoric, poetry, and the graces of language; and that I did", + "not pace my house in my senatorial robes, or practise any similar", + "affectation. I observed also the simplicity of style in his letters,", + "particularly in that which he wrote to my mother from Sinuessa. I", + "learned from him to be easily appeased, and to be readily reconciled", + "with those who had displeased me or given cause of offence, so soon as", + "they inclined to make their peace; to read with care; not to rest", + "satisfied with a slight and superficial knowledge; nor quickly to", + "assent to great talkers. I have him to thank that I met with the", +} diff --git a/internal/rendertest/util_test.go b/internal/rendertest/util_test.go new file mode 100644 index 00000000..f0ee0ed7 --- /dev/null +++ b/internal/rendertest/util_test.go @@ -0,0 +1,137 @@ +package rendertest + +import ( + "bytes" + "flag" + "image" + "image/color" + "image/draw" + "image/png" + "io/ioutil" + "path/filepath" + "testing" + + "gioui.org/app/headless" + "gioui.org/op" + "gioui.org/op/paint" + "golang.org/x/image/colornames" +) + +var ( + dumpImages = flag.Bool("saveimages", false, "save test images") + squares paint.ImageOp +) + +func init() { + // build the texture we use for testing + size := 512 + sub := size / 4 + im := image.NewNRGBA(image.Rect(0, 0, size, size)) + c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue) + for r := 0; r < 4; r++ { + for c := 0; c < 4; c++ { + c1, c2 = c2, c1 + draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1, image.Point{}, draw.Over) + } + c1, c2 = c2, c1 + } + squares = paint.NewImageOp(im) +} + +func drawImage(size int, draw func(o *op.Ops)) (im *image.RGBA, err error) { + sz := image.Point{X: size, Y: size} + w, err := headless.NewWindow(sz.X, sz.Y) + if err != nil { + return im, err + } + ops := new(op.Ops) + draw(ops) + w.Frame(ops) + return w.Screenshot() +} + +func run(t *testing.T, f func(o *op.Ops)) result { + img, err := drawImage(128, f) + if err != nil { + t.Error("error rendering:", err) + } + + // check for a reference image and make sure we are identical. + ok := verifyRef(t, img) + + if *dumpImages || !ok { + if err := saveImage(t.Name()+".png", img); err != nil { + t.Error(err) + } + } + return result{t: t, img: img} +} + +func verifyRef(t *testing.T, img *image.RGBA) (ok bool) { + // ensure identical to ref data + path := filepath.Join("refs", t.Name()+".png") + b, err := ioutil.ReadFile(path) + if err != nil { + t.Error("could not open ref:", err) + return + } + r, err := png.Decode(bytes.NewReader(b)) + if err != nil { + t.Error("could not decode ref:", err) + return + } + ref, ok := r.(*image.RGBA) + if !ok { + t.Error("ref image note RGBA") + return + } + if len(ref.Pix) != len(img.Pix) { + t.Error("not equal to ref (len)") + return false + } + bnd := img.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + c1, c2 := ref.RGBAAt(x, y), img.RGBAAt(x, y) + if !colorsClose(c1, c2) { + t.Error("not equal to ref at", x, y, " ", c1, c2) + } + } + } + return true +} + +func colorsClose(c1, c2 color.RGBA) bool { + return close(c1.A, c2.A) && close(c1.R, c2.R) && close(c1.G, c2.G) && close(c1.B, c2.B) +} + +func close(b1, b2 uint8) bool { + if b1 > b2 { + b1, b2 = b2, b1 + } + diff := b2 - b1 + return diff < 20 +} + +func (r result) expect(x, y int, col color.RGBA) { + if r.img == nil { + return + } + c := r.img.RGBAAt(x, y) + if !colorsClose(c, col) { + r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c) + } +} + +type result struct { + t *testing.T + img *image.RGBA +} + +func saveImage(file string, img image.Image) error { + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return err + } + return ioutil.WriteFile(file, buf.Bytes(), 0666) +}