Skip to content

Commit

Permalink
Less mem (grpc#1987)
Browse files Browse the repository at this point in the history
* Export changes to OSS.

* First commit.

* Cherry-pick.

* Documentation.

* Post review updates.
  • Loading branch information
MakMukhi authored Apr 30, 2018
1 parent fc37cf1 commit 7a8c989
Show file tree
Hide file tree
Showing 15 changed files with 1,080 additions and 1,023 deletions.
11 changes: 5 additions & 6 deletions call_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,16 @@ type testStreamHandler struct {
}

func (h *testStreamHandler) handleStream(t *testing.T, s *transport.Stream) {
p := &parser{r: s}
for {
pf, req, err := p.recvMsg(math.MaxInt32)
isCompressed, req, err := recvMsg(s, math.MaxInt32)
if err == io.EOF {
break
}
if err != nil {
return
}
if pf != compressionNone {
t.Errorf("Received the mistaken message format %d, want %d", pf, compressionNone)
if isCompressed {
t.Errorf("Received compressed message want non-compressed message")
return
}
var v string
Expand Down Expand Up @@ -105,12 +104,12 @@ func (h *testStreamHandler) handleStream(t *testing.T, s *transport.Stream) {
}
}
// send a response back to end the stream.
hdr, data, err := encode(testCodec{}, &expectedResponse, nil, nil, nil)
data, err := encode(testCodec{}, &expectedResponse, nil, nil, nil)
if err != nil {
t.Errorf("Failed to encode the response: %v", err)
return
}
h.t.Write(s, hdr, data, &transport.Options{})
h.t.Write(s, data, &transport.Options{})
h.t.WriteStatus(s, status.New(codes.OK, ""))
}

Expand Down
203 changes: 203 additions & 0 deletions internal/msgdecoder/msgdecoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* Copyright 2018 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

// Package msgdecoder contains the logic to deconstruct a gRPC-message.
package msgdecoder

import (
"encoding/binary"
)

// RecvMsg is a message constructed from the incoming
// bytes on the transport for a stream.
// An instance of RecvMsg will contain only one of the
// following: message header related fields, data slice
// or error.
type RecvMsg struct {
// Following three are message header related
// fields.
// true if the message was compressed by the other
// side.
IsCompressed bool
// Length of the message.
Length int
// Overhead is the length of message header(5 bytes)
// plus padding.
Overhead int

// Data payload of the message.
Data []byte

// Err occurred while reading.
// nil: received some data
// io.EOF: stream is completed. data is nil.
// other non-nil error: transport failure. data is nil.
Err error

Next *RecvMsg
}

// RecvMsgList is a linked-list of RecvMsg.
type RecvMsgList struct {
head *RecvMsg
tail *RecvMsg
}

// IsEmpty returns true when l is empty.
func (l *RecvMsgList) IsEmpty() bool {
if l.tail == nil {
return true
}
return false
}

// Enqueue adds r to l at the back.
func (l *RecvMsgList) Enqueue(r *RecvMsg) {
if l.IsEmpty() {
l.head, l.tail = r, r
return
}
t := l.tail
l.tail = r
t.Next = r
}

// Dequeue removes a RcvMsg from the end of l.
func (l *RecvMsgList) Dequeue() *RecvMsg {
if l.head == nil {
// Note to developer: Instead of calling isEmpty() which
// checks the same condition on l.tail, we check it directly
// on l.head so that in non-nil cases, there aren't cache misses.
return nil
}
r := l.head
l.head = l.head.Next
if l.head == nil {
l.tail = nil
}
return r
}

// MessageDecoder decodes bytes from HTTP2 data frames
// and constructs a gRPC message which is then put in a
// buffer that application(RPCs) read from.
// gRPC Messages:
// First 5 bytes is the message header:
// First byte: Payload format.
// Next 4 bytes: Length of the message.
// Rest of the bytes is the message payload.
//
// TODO(mmukhi): Write unit tests.
type MessageDecoder struct {
// current message being read by the transport.
current *RecvMsg
dataOfst int
padding int
// hdr stores the message header as it is beind received by the transport.
hdr []byte
hdrOfst int
// Callback used to send decoded messages.
dispatch func(*RecvMsg)
}

// NewMessageDecoder creates an instance of MessageDecoder. It takes a callback
// which is called to dispatch finished headers and messages to the application.
func NewMessageDecoder(dispatch func(*RecvMsg)) *MessageDecoder {
return &MessageDecoder{
hdr: make([]byte, 5),
dispatch: dispatch,
}
}

// Decode consumes bytes from a HTTP2 data frame to create gRPC messages.
func (m *MessageDecoder) Decode(b []byte, padding int) {
m.padding += padding
for len(b) > 0 {
// Case 1: A complete message hdr was received earlier.
if m.current != nil {
n := copy(m.current.Data[m.dataOfst:], b)
m.dataOfst += n
b = b[n:]
if m.dataOfst == len(m.current.Data) { // Message is complete.
m.dispatch(m.current)
m.current = nil
m.dataOfst = 0
}
continue
}
// Case 2a: No message header has been received yet.
if m.hdrOfst == 0 {
// case 2a.1: b has the whole header
if len(b) >= 5 {
m.parseHeader(b[:5])
b = b[5:]
continue
}
// case 2a.2: b has partial header
n := copy(m.hdr, b)
m.hdrOfst = n
b = b[n:]
continue
}
// Case 2b: Partial message header was received earlier.
n := copy(m.hdr[m.hdrOfst:], b)
m.hdrOfst += n
b = b[n:]
if m.hdrOfst == 5 { // hdr is complete.
m.hdrOfst = 0
m.parseHeader(m.hdr)
}
}
}

func (m *MessageDecoder) parseHeader(b []byte) {
length := int(binary.BigEndian.Uint32(b[1:5]))
hdr := &RecvMsg{
IsCompressed: int(b[0]) == 1,
Length: length,
Overhead: m.padding + 5,
}
m.padding = 0
// Dispatch the information retreived from message header so
// that the RPC goroutine can send a proactive window update as we
// wait for the rest of it.
m.dispatch(hdr)
if length == 0 {
m.dispatch(&RecvMsg{})
return
}
m.current = &RecvMsg{
Data: getMem(length),
}
}

func getMem(l int) []byte {
// TODO(mmukhi): Reuse this memory.
return make([]byte, l)
}

// CreateMessageHeader creates a gRPC-specific message header.
func CreateMessageHeader(l int, isCompressed bool) []byte {
// TODO(mmukhi): Investigate if this memory is worth
// reusing.
hdr := make([]byte, 5)
if isCompressed {
hdr[0] = byte(1)
}
binary.BigEndian.PutUint32(hdr[1:], uint32(l))
return hdr
}
81 changes: 81 additions & 0 deletions internal/msgdecoder/msgdecoder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2018 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package msgdecoder

import (
"encoding/binary"
"reflect"
"testing"
)

func TestMessageDecoder(t *testing.T) {
for _, test := range []struct {
numFrames int
data []string
}{
{1, []string{"abc"}}, // One message per frame.
{1, []string{"abc", "def", "ghi"}}, // Multiple messages per frame.
{3, []string{"a", "bcdef", "ghif"}}, // Multiple messages over multiple frames.
} {
var want []*RecvMsg
for _, d := range test.data {
want = append(want, &RecvMsg{Length: len(d), Overhead: 5})
want = append(want, &RecvMsg{Data: []byte(d)})
}
var got []*RecvMsg
dcdr := NewMessageDecoder(func(r *RecvMsg) { got = append(got, r) })
for _, fr := range createFrames(test.numFrames, test.data) {
dcdr.Decode(fr, 0)
}
if !match(got, want) {
t.Fatalf("got: %v, want: %v", got, want)
}
}
}

func match(got, want []*RecvMsg) bool {
for i, v := range got {
if !reflect.DeepEqual(v, want[i]) {
return false
}
}
return true
}

func createFrames(n int, msgs []string) [][]byte {
var b []byte
for _, m := range msgs {
payload := []byte(m)
hdr := make([]byte, 5)
binary.BigEndian.PutUint32(hdr[1:], uint32(len(payload)))
b = append(b, hdr...)
b = append(b, payload...)
}
// break b into n parts.
var result [][]byte
batch := len(b) / n
for len(b) != 0 {
sz := batch
if len(b) < sz {
sz = len(b)
}
result = append(result, b[:sz])
b = b[sz:]
}
return result
}
Loading

0 comments on commit 7a8c989

Please sign in to comment.