diff options
author | Přemysl Eric Janouch <p@janouch.name> | 2022-09-15 22:45:14 +0200 |
---|---|---|
committer | Přemysl Eric Janouch <p@janouch.name> | 2022-09-16 00:51:11 +0200 |
commit | 6f39aa66156f53b27e3b9cfe8457fc2f64129e56 (patch) | |
tree | 0119fd13384bdcdffacfb9d05e10616e6342ca15 /xP/xP.go | |
parent | e87cc90b5e20ba90d4f5c9ea349d9cf41b5ae58c (diff) | |
download | xK-6f39aa66156f53b27e3b9cfe8457fc2f64129e56.tar.gz xK-6f39aa66156f53b27e3b9cfe8457fc2f64129e56.tar.xz xK-6f39aa66156f53b27e3b9cfe8457fc2f64129e56.zip |
xP: use the binary protocol for incoming events
And batch event messages together as much as possible.
JSON has proven itself to be really slow
(for example, encoding/json.Marshaler is a slow interface),
and browsers have significant overhead per WS message.
Commands are still sent as JSON, sending them in binary
would be a laborious rewrite without measurable merits.
The xP server now only prints debug output when requested,
because that was another source of major slowdowns.
Diffstat (limited to 'xP/xP.go')
-rw-r--r-- | xP/xP.go | 122 |
1 files changed, 82 insertions, 40 deletions
@@ -8,6 +8,7 @@ import ( "context" "encoding/binary" "encoding/json" + "flag" "fmt" "html/template" "io" @@ -21,6 +22,8 @@ import ( ) var ( + debug = flag.Bool("debug", false, "enable debug output") + addressBind string addressConnect string addressWS string @@ -28,7 +31,7 @@ var ( // ----------------------------------------------------------------------------- -func relayReadJSON(r io.Reader) []byte { +func relayReadFrame(r io.Reader) []byte { var length uint32 if err := binary.Read(r, binary.BigEndian, &length); err != nil { log.Println("Event receive failed: " + err.Error()) @@ -40,32 +43,38 @@ func relayReadJSON(r io.Reader) []byte { return nil } - log.Printf("<? %v\n", b) + if *debug { + log.Printf("<? %v\n", b) - var m RelayEventMessage - if after, ok := m.ConsumeFrom(b); !ok { - log.Println("Event deserialization failed") - return nil - } else if len(after) != 0 { - log.Println("Event deserialization failed: trailing data") - return nil - } + var m RelayEventMessage + if after, ok := m.ConsumeFrom(b); !ok { + log.Println("Event deserialization failed") + return nil + } else if len(after) != 0 { + log.Println("Event deserialization failed: trailing data") + return nil + } - j, err := m.MarshalJSON() - if err != nil { - log.Println("Event marshalling failed: " + err.Error()) - return nil + j, err := m.MarshalJSON() + if err != nil { + log.Println("Event marshalling failed: " + err.Error()) + return nil + } + + log.Printf("<- %s\n", j) } - return j + return b } func relayMakeReceiver(ctx context.Context, conn net.Conn) <-chan []byte { - p := make(chan []byte, 1) - r := bufio.NewReader(conn) + // The usual event message rarely gets above 1 kilobyte, + // thus this is set to buffer up at most 1 megabyte or so. + p := make(chan []byte, 1000) + r := bufio.NewReaderSize(conn, 65536) go func() { defer close(p) for { - j := relayReadJSON(r) + j := relayReadFrame(r) if j == nil { return } @@ -97,7 +106,9 @@ func relayWriteJSON(conn net.Conn, j []byte) bool { return false } - log.Printf("-> %v\n", b) + if *debug { + log.Printf("-> %v\n", b) + } return true } @@ -114,21 +125,23 @@ func clientReadJSON(ctx context.Context, ws *websocket.Conn) []byte { "Command receive failed: " + "binary messages are not supported") return nil } - log.Printf("?> %s\n", j) + + if *debug { + log.Printf("?> %s\n", j) + } return j } -func clientWriteJSON(ctx context.Context, ws *websocket.Conn, j []byte) bool { - if err := ws.Write(ctx, websocket.MessageText, j); err != nil { +func clientWriteBinary(ctx context.Context, ws *websocket.Conn, b []byte) bool { + if err := ws.Write(ctx, websocket.MessageBinary, b); err != nil { log.Println("Event send failed: " + err.Error()) return false } - log.Printf("<- %s\n", j) return true } func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool { - j, err := (&RelayEventMessage{ + b, ok := (&RelayEventMessage{ EventSeq: 0, Data: RelayEventData{ Interface: RelayEventDataError{ @@ -137,12 +150,12 @@ func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool { Error: err.Error(), }, }, - }).MarshalJSON() - if err != nil { - log.Println("Event marshalling failed: " + err.Error()) + }).AppendTo(nil) + if ok { + log.Println("Event serialization failed") return false } - return clientWriteJSON(ctx, ws, j) + return clientWriteBinary(ctx, ws, b) } func handleWS(w http.ResponseWriter, r *http.Request) { @@ -164,15 +177,36 @@ func handleWS(w http.ResponseWriter, r *http.Request) { conn, err := net.Dial("tcp", addressConnect) if err != nil { + log.Println("Connection failed: " + err.Error()) clientWriteError(ctx, ws, err) return } defer conn.Close() + // To decrease latencies, events are received and decoded in parallel + // to their sending, and we try to batch them together. + relayFrames := relayMakeReceiver(ctx, conn) + batchFrames := func() []byte { + batch, ok := <-relayFrames + if !ok { + return nil + } + Batch: + for { + select { + case b, ok := <-relayFrames: + if !ok { + break Batch + } + batch = append(batch, b...) + default: + break Batch + } + } + return batch + } + // We don't need to intervene, so it's just two separate pipes so far. - // However, to decrease latencies, events are received and decoded - // in parallel to their sending. - relayJSON := relayMakeReceiver(ctx, conn) go func() { defer cancel() for { @@ -186,11 +220,11 @@ func handleWS(w http.ResponseWriter, r *http.Request) { go func() { defer cancel() for { - j, ok := <-relayJSON - if !ok { + b := batchFrames() + if b == nil { return } - clientWriteJSON(ctx, ws, j) + clientWriteBinary(ctx, ws, b) } }() <-ctx.Done() @@ -214,7 +248,7 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html> <script> let proxy = '{{ . }}' </script> - <script src="xP.js"> + <script type="module" src="xP.js"> </script> </body> </html>`)) @@ -235,13 +269,21 @@ func handleDefault(w http.ResponseWriter, r *http.Request) { } func main() { - if len(os.Args) < 3 || len(os.Args) > 4 { - log.Fatalf("usage: %s BIND CONNECT [WSURI]\n", os.Args[0]) + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), + "Usage: %s [OPTION...] BIND CONNECT [WSURI]\n\n", os.Args[0]) + flag.PrintDefaults() + } + + flag.Parse() + if flag.NArg() < 2 || flag.NArg() > 3 { + flag.Usage() + os.Exit(1) } - addressBind, addressConnect = os.Args[1], os.Args[2] - if len(os.Args) > 3 { - addressWS = os.Args[3] + addressBind, addressConnect = flag.Arg(0), flag.Arg(1) + if flag.NArg() > 2 { + addressWS = flag.Arg(2) } http.Handle("/ws", http.HandlerFunc(handleWS)) |