Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

ladder_test.gno

4.92 Kb · 140 lines
  1package bf
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/moul/x/vm/vmkit/v0"
  7	"gno.land/p/nt/uassert/v0"
  8)
  9
 10// heavy is the benchmark program: 200 increments, then an outer loop that on
 11// each of its 200 iterations moves right, adds 200, clears that cell with the
 12// "[-]" idiom, and moves back.
 13//
 14// It writes nothing, so a measurement of it is a measurement of the dispatch
 15// loop and not of the output path.
 16var heavy = rep("+", 200) + "[" + "-" + ">" + rep("+", 200) + "[-]" + "<" + "]"
 17
 18// GuestOps and NaiveDispatches are the two counts for [heavy], and the
 19// difference between them is the reason the ladder needs both named.
 20//
 21// A guest instruction is one operator the language executes, counted the way
 22// any jump-table interpreter counts it. The naive interpreter dispatches more
 23// often than that for the same program: its "]" handler scans back to the
 24// matching "[" and lands on it, so "[" is re-evaluated on every iteration of
 25// every loop. On heavy that is 1.33 dispatches per guest instruction.
 26//
 27// Rung 1 removes exactly that excess, so a ladder that used the naive
 28// dispatch count as its denominator would be crediting rung 1 twice. Both
 29// numbers come from an independent simulator, not from this package, so the
 30// denominator is not derived from the thing being measured.
 31const (
 32	GuestOps        = 121201
 33	NaiveDispatches = 161200
 34)
 35
 36func rep(s string, n int) string {
 37	out := ""
 38	for i := 0; i < n; i++ {
 39		out += s
 40	}
 41	return out
 42}
 43
 44// nullHost is a Host that costs as close to nothing as a Host can, so the
 45// ladder measures the dispatch loop rather than the storage or output path.
 46type nullHost struct{}
 47
 48func (nullHost) Caller() address                     { return address("") }
 49func (nullHost) Origin() address                     { return address("") }
 50func (nullHost) Now() int64                          { return 0 }
 51func (nullHost) Height() int64                       { return 0 }
 52func (nullHost) Get(key []byte) []byte               { return nil }
 53func (nullHost) Set(key, val []byte)                 {}
 54func (nullHost) Input() []byte                       { return nil }
 55func (nullHost) Output(p []byte)                     {}
 56func (nullHost) Emit(typ string, kv ...string)       {}
 57func (nullHost) Send(to address, amount int64) error { return vmkit.ErrNotGranted }
 58func (nullHost) Log(msg string)                      {}
 59
 60// runHeavy compiles heavy at lvl and runs it on the shipped machine,
 61// returning how many machine ops it executed. The count is the fuel the
 62// machine charged itself, so the ladder's "machine ops" column is measured by
 63// the machine and not counted by hand.
 64func runHeavy(t *testing.T, lvl Level) int64 {
 65	t.Helper()
 66	prog, err := Compile(heavy, lvl)
 67	uassert.NoError(t, err)
 68	if prog == nil {
 69		return 0
 70	}
 71	m := NewMachine(prog)
 72	used, status := m.Step(nullHost{}, vmkit.Unmetered)
 73	uassert.Equal(t, "halted", status.String())
 74	return used
 75}
 76
 77// The rungs. One test each, so `gno test -print-runtime-metrics` prints one
 78// cycle count per rung and the README's table is a transcription rather than
 79// an estimate. They assert that the run completed and how many machine ops it
 80// took: what is being recorded is the metric line.
 81
 82func TestRung0Naive(t *testing.T) {
 83	uassert.Equal(t, "", Execute(heavy))
 84}
 85
 86func TestRung0Naive4x(t *testing.T) {
 87	for i := 0; i < 4; i++ {
 88		Execute(heavy)
 89	}
 90}
 91
 92func TestRung1Jumps(t *testing.T) {
 93	uassert.Equal(t, int64(GuestOps+1), runHeavy(t, LevelJumps))
 94}
 95
 96func TestRung2Fuse(t *testing.T) {
 97	uassert.True(t, runHeavy(t, LevelFuse) < GuestOps)
 98}
 99
100func TestRung3Idioms(t *testing.T) {
101	uassert.True(t, runHeavy(t, LevelIdioms) < GuestOps)
102}
103
104func TestRung3Idioms4x(t *testing.T) {
105	for i := 0; i < 4; i++ {
106		runHeavy(t, LevelIdioms)
107	}
108}
109
110func TestRung5Metered(t *testing.T) {
111	// Same program, same rung, but every op goes through the fuel meter's
112	// budget branch instead of its unmetered one. The delta against
113	// TestRung3Idioms is the price of being a machine that can be stopped.
114	prog, err := Compile(heavy, LevelIdioms)
115	uassert.NoError(t, err)
116	m := NewMachine(prog)
117	_, status := m.Step(nullHost{}, 1<<40)
118	uassert.Equal(t, "halted", status.String())
119}
120
121// TestLadderOpCounts records the compile-time half of the ladder: how many
122// machine ops each rung executes for the same 121,201 guest instructions.
123// These are the numbers the README's table quotes, asserted here so the table
124// cannot silently go stale.
125func TestLadderOpCounts(t *testing.T) {
126	jumps := runHeavy(t, LevelJumps)
127	fuse := runHeavy(t, LevelFuse)
128	idioms := runHeavy(t, LevelIdioms)
129
130	// Rung 1 executes exactly one machine op per guest instruction, plus
131	// the halt: the independent simulator and this machine agree to the op.
132	uassert.Equal(t, int64(121202), jumps)
133	// Fusing runs of + and > collapses the two 200-long runs.
134	uassert.Equal(t, int64(81203), fuse)
135	// Recognizing "[-]" turns the inner clear loop into a single op.
136	uassert.Equal(t, int64(1203), idioms)
137
138	uassert.True(t, fuse < jumps)
139	uassert.True(t, idioms < fuse)
140}