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]>
52 lines
1.5 KiB
Go
52 lines
1.5 KiB
Go
// Length-prefixed framing, shared by the two non-HTTP arms.
|
|
//
|
|
// A frame is a 4-byte big-endian length followed by that many bytes.
|
|
// The native-ZAP arm carries a ZAP message in the payload; the floor
|
|
// arm carries the raw body. Both use these helpers so the framing cost
|
|
// is identical between them and the only difference measured is what
|
|
// the payload is and what each end does with it.
|
|
|
|
package bench_test
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
const frameMax = 64 << 20
|
|
|
|
// writeFrame writes payload behind a 4-byte big-endian length and flushes.
|
|
// hdr is caller-owned scratch so the write path allocates nothing.
|
|
func writeFrame(w *bufio.Writer, hdr *[4]byte, payload []byte) error {
|
|
binary.BigEndian.PutUint32(hdr[:], uint32(len(payload)))
|
|
if _, err := w.Write(hdr[:]); err != nil {
|
|
return err
|
|
}
|
|
if _, err := w.Write(payload); err != nil {
|
|
return err
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
// readFrame reads one frame into buf, growing buf only when the frame does
|
|
// not fit. The returned slice aliases buf and is valid until the next read.
|
|
func readFrame(r *bufio.Reader, hdr *[4]byte, buf []byte) ([]byte, error) {
|
|
if _, err := io.ReadFull(r, hdr[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
n := int(binary.BigEndian.Uint32(hdr[:]))
|
|
if n <= 0 || n > frameMax {
|
|
return nil, fmt.Errorf("frame: length %d out of range", n)
|
|
}
|
|
if cap(buf) < n {
|
|
buf = make([]byte, n)
|
|
}
|
|
buf = buf[:n]
|
|
if _, err := io.ReadFull(r, buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|