summaryrefslogtreecommitdiff
path: root/test/differential/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'test/differential/main.go')
-rw-r--r--test/differential/main.go172
1 files changed, 172 insertions, 0 deletions
diff --git a/test/differential/main.go b/test/differential/main.go
new file mode 100644
index 0000000..7a2538d
--- /dev/null
+++ b/test/differential/main.go
@@ -0,0 +1,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)
+}