diff options
Diffstat (limited to 'test/differential/main.go')
| -rw-r--r-- | test/differential/main.go | 172 |
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) +} |
