summaryrefslogtreecommitdiff
path: root/test/differential/main.go
blob: 7a2538d4b99770ce20eb8e628552a0e88a8055c2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// Valid-traffic differential conformance and measurement runner. No network targets.
package main

import (
	p9 "9fans.net/go/plan9"
	"bytes"
	"encoding/binary"
	"encoding/json"
	"flag"
	"fmt"
	other "github.com/knusbaum/go9p/proto"
	"io"
	"math/rand"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"time"
)

type reader struct {
	data         []byte
	chunk, calls int
}

func (r *reader) Read(p []byte) (int, error) {
	r.calls++
	if len(r.data) == 0 {
		return 0, io.EOF
	}
	n := min(len(p), len(r.data), r.chunk)
	copy(p, r.data[:n])
	r.data = r.data[n:]
	return n, nil
}
func check(err error) {
	if err != nil {
		panic(err)
	}
}
func marshal(v any) []byte { b, e := json.MarshalIndent(v, "", "  "); check(e); return append(b, '\n') }
func corpus(seed int64, rounds int) [][]byte {
	r := rand.New(rand.NewSource(seed))
	frames := [][]byte{}
	names := []string{"a", "file", "directory", "日本語", "ação"}
	for i := 0; i < rounds; i++ {
		q := p9.Qid{Type: 0, Vers: r.Uint32(), Path: r.Uint64()}
		d := p9.Dir{Qid: q, Mode: 0600, Atime: r.Uint32(), Mtime: r.Uint32(), Length: r.Uint64(), Name: names[i%len(names)], Uid: "user", Gid: "group", Muid: "user"}
		stat, e := d.Bytes()
		check(e)
		if i%2 == 1 {
			d.Null()
			stat, e = d.Bytes()
			check(e)
		}
		data := make([]byte, []int{0, 1, 7, 127, 1024, 8192}[i%6])
		_, e = r.Read(data)
		check(e)
		wn := make([]string, i%17)
		wq := make([]p9.Qid, i%17)
		for j := range wn {
			wn[j] = names[r.Intn(len(names))]
			wq[j] = q
		}
		for typ := uint8(100); typ <= 127; typ++ {
			if typ == 106 {
				continue
			}
			f := p9.Fcall{Type: typ, Tag: uint16(r.Intn(65535)), Fid: r.Uint32(), Newfid: r.Uint32(), Afid: p9.NOFID, Uname: "user", Aname: "", Version: "9P2000", Msize: 65536, Oldtag: uint16(r.Intn(65535)), Ename: "permission denied", Qid: q, Aqid: p9.Qid{Type: p9.QTAUTH, Vers: q.Vers, Path: q.Path}, Iounit: 0, Name: d.Name, Perm: 0600, Mode: uint8(i % 4), Offset: r.Uint64(), Count: uint32(len(data)), Data: data, Wname: wn, Wqid: wq, Stat: stat}
			if typ == 100 || typ == 101 {
				f.Tag = p9.NOTAG
			}
			if typ == 114 {
				f.Name = names[i%len(names)]
			}
			b, e := f.Bytes()
			check(e)
			frames = append(frames, b)
		}
	}
	return frames
}

type metric struct {
	Implementation string `json:"implementation"`
	Operations     uint64 `json:"operations"`
	CodecNS        int64  `json:"codec_ns"`
	Allocations    uint64 `json:"codec_allocations"`
	AllocatedBytes uint64 `json:"allocated_bytes"`
	ReadCalls      uint64 `json:"read_calls"`
	Bytes          uint64 `json:"bytes"`
}

func measure(name string, frames [][]byte, repeats, chunk int) metric {
	m := metric{Implementation: name}
	var before, after runtime.MemStats
	runtime.GC()
	runtime.ReadMemStats(&before)
	for index, frame := range frames {
		start := time.Now()
		for j := 0; j < repeats; j++ {
			rd := reader{data: frame, chunk: chunk}
			var out []byte
			if name == "9fans v0.0.7" {
				f, e := p9.ReadFcall(&rd)
				check(e)
				out, e = f.Bytes()
				check(e)
			} else {
				f, e := other.ParseCall(&rd)
				check(e)
				out = f.Compose()
			}
			if !bytes.Equal(frame, out) {
				panic(fmt.Sprintf("%s frame %d type %d differs", name, index, frame[4]))
			}
			m.ReadCalls += uint64(rd.calls)
			m.Bytes += uint64(len(frame))
			m.Operations++
		}
		m.CodecNS += time.Since(start).Nanoseconds()
	}
	runtime.ReadMemStats(&after)
	m.Allocations = after.Mallocs - before.Mallocs
	m.AllocatedBytes = after.TotalAlloc - before.TotalAlloc
	return m
}
func main() {
	seed := flag.Int64("seed", 4200, "deterministic valid-traffic seed")
	rounds := flag.Int("rounds", 100, "27 messages per round")
	repeats := flag.Int("repeats", 20, "codec repetitions")
	chunk := flag.Int("chunk", 65536, "maximum read/write chunk; use 1 to fragment every byte")
	session := flag.String("session-probe", "", "run live sessions against go9p")
	probe := flag.String("probe", "../../zig-out/bin/cloud9-probe", "cloud9 executable")
	output := flag.String("output", "results", "artifact directory")
	flag.Parse()
	if *rounds < 1 || *rounds > 10000 || *repeats < 1 || *repeats > 10000 || *chunk < 1 {
		panic("invalid bounds")
	}
	frames := corpus(*seed, *rounds)
	all := bytes.Join(frames, nil)
	check(os.MkdirAll(*output, 0755))
	check(os.WriteFile(filepath.Join(*output, "corpus.9p"), all, 0644))
	config := map[string]any{"seed": *seed, "rounds": *rounds, "repeats": *repeats, "chunk": *chunk, "frames": len(frames), "go": runtime.Version(), "platform": runtime.GOOS + "/" + runtime.GOARCH}
	check(os.WriteFile(filepath.Join(*output, "config.json"), marshal(config), 0644))
	metrics := []metric{measure("9fans v0.0.7", frames, *repeats, *chunk), measure("go9p v1.18.0", frames, *repeats, *chunk)}
	var input bytes.Buffer
	for _, v := range []uint32{uint32(len(frames)), uint32(*repeats), uint32(*chunk)} {
		check(binary.Write(&input, binary.LittleEndian, v))
	}
	input.Write(all)
	command := exec.Command(*probe)
	command.Stdin = &input
	var stderr bytes.Buffer
	command.Stderr = &stderr
	out, e := command.Output()
	if e != nil {
		panic(fmt.Sprintf("cloud9: %v: %s", e, stderr.String()))
	}
	if !bytes.Equal(all, out) {
		panic("cloud9 round-trip differs; replay corpus.9p with saved configuration")
	}
	var cloud map[string]any
	check(json.Unmarshal(stderr.Bytes(), &cloud))
	report := map[string]any{"configuration": config, "correctness": "all generated base-9P2000 frames agree byte-for-byte", "cloud9": cloud, "references": metrics, "measurement_notes": []string{"Cloud9 codec timer excludes pipe I/O; Go timers include in-memory stream parsing and byte comparison. Do not treat these as equivalent throughput benchmarks.", "Go allocations use runtime.MemStats deltas on a single-process run; cloud9 codec has no allocation API.", "Cloud9 I/O counters are actual stdin/stdout syscalls; Go read counters are io.Reader calls, not syscalls.", "These are wire-conformance probes, not differential filesystem-server semantics."}}
	if *session != "" {
		report["session"] = sessionProbe(*session, *seed, *rounds)
	}
	b := marshal(report)
	check(os.WriteFile(filepath.Join(*output, "report.json"), b, 0644))
	os.Stdout.Write(b)
}