mirror of
https://github.com/zap-proto/bench.git
synced 2026-10-30 15:06:06 +00:00
The arm this harness called "native ZAP" was a TCP server that read a
4-byte length prefix and wrote the same bytes back without looking at
them. It imported no ZAP package and carried no message format, no
schema and no headers, while the HTTP arm it was ranked against built a
request, parsed a header set and returned a body. Its numbers are where
622x less memory and 115x fewer allocations came from, and 622 is
9954/16: an HTTP allocation floor over a payload size.
The real native arm is a typed zap-proto/go message — body as a bytes
field, the header set as a variable-entry list, a status word — over the
same framing, with the server reading the body and every header and
echoing the X- headers, which is the work the HTTP handler does. It
allocates six objects and twice the payload per round trip, at every
size, because ObjectBuilder.SetBytes heap-copies the payload before
Finish copies it into the message.
The byte echo stays as `floor`, named for what it is: a lower bound on
transport cost that no number may be attributed to. A fasthttp arm joins
it, because fasthttp's object model is cheaper than net/http's on any
wire and a ZAP-vs-net/http ratio otherwise moves for two reasons at once.
Also here:
- zaphttp.NewTransport is gone; cmd/zapbench dials with Dial("tcp", addr)
- the handler moved net/http -> fasthttp and MarshalRequest changed shape
- zap-proto/http comes from v0.3.9 instead of ../http, so one clone and
./run.sh is the whole reproduction; zap-proto/go moves 1.1.0 -> 1.8.3
- TestWireBytes names the direction of its comparison. It used to print
a ZAP frame 86% larger than HTTP's as "0.54x (-86.0% smaller)"
- run.sh regenerates bench-results.txt, records the machine and its load
before and after, and repeats the rate-bearing tests three times
Co-authored-by: Hanzo Dev <[email protected]>
135 lines
3.0 KiB
Go
135 lines
3.0 KiB
Go
// The floor: a length-prefixed byte echo over TCP. No protocol.
|
|
//
|
|
// The server reads a frame and writes the same bytes back without
|
|
// looking at them. The client sends a body and reads the echo into a
|
|
// caller-owned buffer. There is no message format, no schema, no
|
|
// headers, no status, and no dispatch — the payload is opaque to both
|
|
// ends.
|
|
//
|
|
// This arm exists to bound the others from below. Whatever a real
|
|
// protocol costs, it costs at least this, and the gap between this arm
|
|
// and the native-ZAP arm is the price of having a message format at
|
|
// all. It is NOT a measurement of ZAP, and a number taken from here
|
|
// must never be reported as one: an earlier revision of this harness
|
|
// called this arm "native ZAP", and the paper built on it claimed for
|
|
// the protocol what is really just the cost of copying bytes.
|
|
|
|
package bench_test
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
)
|
|
|
|
type floorServer struct {
|
|
ln net.Listener
|
|
wg sync.WaitGroup
|
|
stop chan struct{}
|
|
}
|
|
|
|
func startFloorServer() (*floorServer, error) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s := &floorServer{ln: ln, stop: make(chan struct{})}
|
|
s.wg.Add(1)
|
|
go s.accept()
|
|
return s, nil
|
|
}
|
|
|
|
func (s *floorServer) Addr() string { return s.ln.Addr().String() }
|
|
|
|
func (s *floorServer) Close() error {
|
|
close(s.stop)
|
|
err := s.ln.Close()
|
|
s.wg.Wait()
|
|
return err
|
|
}
|
|
|
|
func (s *floorServer) accept() {
|
|
defer s.wg.Done()
|
|
for {
|
|
conn, err := s.ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
s.wg.Add(1)
|
|
go s.serve(conn)
|
|
}
|
|
}
|
|
|
|
func (s *floorServer) serve(conn net.Conn) {
|
|
defer s.wg.Done()
|
|
defer conn.Close()
|
|
tune(conn)
|
|
br := bufio.NewReaderSize(conn, connBuf)
|
|
bw := bufio.NewWriterSize(conn, connBuf)
|
|
var hdr [4]byte
|
|
buf := make([]byte, 0, connBuf)
|
|
for {
|
|
select {
|
|
case <-s.stop:
|
|
return
|
|
default:
|
|
}
|
|
frame, err := readFrame(br, &hdr, buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
buf = frame[:0]
|
|
if err := writeFrame(bw, &hdr, frame); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// floorClient holds one persistent connection. Sequential calls only.
|
|
type floorClient struct {
|
|
conn net.Conn
|
|
bw *bufio.Writer
|
|
br *bufio.Reader
|
|
hdr [4]byte
|
|
}
|
|
|
|
func newFloorClient(addr string) (*floorClient, error) {
|
|
c, err := net.Dial("tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tune(c)
|
|
return &floorClient{
|
|
conn: c,
|
|
bw: bufio.NewWriterSize(c, connBuf),
|
|
br: bufio.NewReaderSize(c, connBuf),
|
|
}, nil
|
|
}
|
|
|
|
func (c *floorClient) Close() error { return c.conn.Close() }
|
|
|
|
// Echo sends body and reads the echoed bytes into out, which the caller
|
|
// sizes to len(body). Steady state allocates nothing.
|
|
func (c *floorClient) Echo(body, out []byte) (int, error) {
|
|
if err := writeFrame(c.bw, &c.hdr, body); err != nil {
|
|
return 0, err
|
|
}
|
|
got, err := readFrame(c.br, &c.hdr, out)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(got) != len(body) {
|
|
return 0, fmt.Errorf("floor: echoed %d bytes, want %d", len(got), len(body))
|
|
}
|
|
return len(got), nil
|
|
}
|
|
|
|
const connBuf = 64 << 10
|
|
|
|
func tune(c net.Conn) {
|
|
if tc, ok := c.(*net.TCPConn); ok {
|
|
_ = tc.SetNoDelay(true)
|
|
}
|
|
}
|