diff --git a/encoding/protojson/encode.go b/encoding/protojson/encode.go index d09d22e1..66b95870 100644 --- a/encoding/protojson/encode.go +++ b/encoding/protojson/encode.go @@ -106,13 +106,19 @@ func (o MarshalOptions) Format(m proto.Message) string { // MarshalOptions. Do not depend on the output being stable. It may change over // time across different versions of the program. func (o MarshalOptions) Marshal(m proto.Message) ([]byte, error) { - return o.marshal(m) + return o.marshal(nil, m) +} + +// MarshalAppend appends the JSON format encoding of m to b, +// returning the result. +func (o MarshalOptions) MarshalAppend(b []byte, m proto.Message) ([]byte, error) { + return o.marshal(b, m) } // marshal is a centralized function that all marshal operations go through. // For profiling purposes, avoid changing the name of this function or // introducing other code paths for marshal that do not go through this. -func (o MarshalOptions) marshal(m proto.Message) ([]byte, error) { +func (o MarshalOptions) marshal(b []byte, m proto.Message) ([]byte, error) { if o.Multiline && o.Indent == "" { o.Indent = defaultIndent } @@ -120,7 +126,7 @@ func (o MarshalOptions) marshal(m proto.Message) ([]byte, error) { o.Resolver = protoregistry.GlobalTypes } - internalEnc, err := json.NewEncoder(o.Indent) + internalEnc, err := json.NewEncoder(b, o.Indent) if err != nil { return nil, err } @@ -128,7 +134,7 @@ func (o MarshalOptions) marshal(m proto.Message) ([]byte, error) { // Treat nil message interface as an empty message, // in which case the output in an empty JSON object. if m == nil { - return []byte("{}"), nil + return append(b, '{', '}'), nil } enc := encoder{internalEnc, o} diff --git a/encoding/protojson/encode_test.go b/encoding/protojson/encode_test.go index e8db20b0..adda0762 100644 --- a/encoding/protojson/encode_test.go +++ b/encoding/protojson/encode_test.go @@ -2310,3 +2310,44 @@ func TestMarshal(t *testing.T) { }) } } + +func TestEncodeAppend(t *testing.T) { + want := []byte("prefix") + got := append([]byte(nil), want...) + got, err := protojson.MarshalOptions{}.MarshalAppend(got, &pb3.Scalars{ + SString: "value", + }) + if err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(got, want) { + t.Fatalf("MarshalAppend modified prefix: got %v, want prefix %v", got, want) + } +} + +func TestMarshalAppendAllocations(t *testing.T) { + m := &pb3.Scalars{SInt32: 1} + const count = 1000 + size := 12 + b := make([]byte, size) + // AllocsPerRun returns an integral value. + marshalAllocs := testing.AllocsPerRun(count, func() { + _, err := protojson.MarshalOptions{}.MarshalAppend(b[:0], m) + if err != nil { + t.Fatal(err) + } + }) + b = nil + marshalAppendAllocs := testing.AllocsPerRun(count, func() { + var err error + b, err = protojson.MarshalOptions{}.MarshalAppend(b, m) + if err != nil { + t.Fatal(err) + } + }) + if marshalAllocs != marshalAppendAllocs { + t.Errorf("%v allocs/op when writing to a preallocated buffer", marshalAllocs) + t.Errorf("%v allocs/op when repeatedly appending to a slice", marshalAppendAllocs) + t.Errorf("expect amortized allocs/op to be identical") + } +} diff --git a/encoding/prototext/encode.go b/encoding/prototext/encode.go index ebf6c652..722a7b41 100644 --- a/encoding/prototext/encode.go +++ b/encoding/prototext/encode.go @@ -101,13 +101,19 @@ func (o MarshalOptions) Format(m proto.Message) string { // MarshalOptions object. Do not depend on the output being stable. It may // change over time across different versions of the program. func (o MarshalOptions) Marshal(m proto.Message) ([]byte, error) { - return o.marshal(m) + return o.marshal(nil, m) +} + +// MarshalAppend appends the textproto format encoding of m to b, +// returning the result. +func (o MarshalOptions) MarshalAppend(b []byte, m proto.Message) ([]byte, error) { + return o.marshal(b, m) } // marshal is a centralized function that all marshal operations go through. // For profiling purposes, avoid changing the name of this function or // introducing other code paths for marshal that do not go through this. -func (o MarshalOptions) marshal(m proto.Message) ([]byte, error) { +func (o MarshalOptions) marshal(b []byte, m proto.Message) ([]byte, error) { var delims = [2]byte{'{', '}'} if o.Multiline && o.Indent == "" { @@ -117,7 +123,7 @@ func (o MarshalOptions) marshal(m proto.Message) ([]byte, error) { o.Resolver = protoregistry.GlobalTypes } - internalEnc, err := text.NewEncoder(o.Indent, delims, o.EmitASCII) + internalEnc, err := text.NewEncoder(b, o.Indent, delims, o.EmitASCII) if err != nil { return nil, err } @@ -125,7 +131,7 @@ func (o MarshalOptions) marshal(m proto.Message) ([]byte, error) { // Treat nil message interface as an empty message, // in which case there is nothing to output. if m == nil { - return []byte{}, nil + return b, nil } enc := encoder{internalEnc, o} diff --git a/encoding/prototext/encode_test.go b/encoding/prototext/encode_test.go index 96510bce..65b93cb4 100644 --- a/encoding/prototext/encode_test.go +++ b/encoding/prototext/encode_test.go @@ -5,6 +5,7 @@ package prototext_test import ( + "bytes" "math" "testing" @@ -1435,3 +1436,44 @@ value: "\x80" }) } } + +func TestEncodeAppend(t *testing.T) { + want := []byte("prefix") + got := append([]byte(nil), want...) + got, err := prototext.MarshalOptions{}.MarshalAppend(got, &pb3.Scalars{ + SString: "value", + }) + if err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(got, want) { + t.Fatalf("MarshalAppend modified prefix: got %v, want prefix %v", got, want) + } +} + +func TestMarshalAppendAllocations(t *testing.T) { + m := &pb3.Scalars{SInt32: 1} + const count = 1000 + size := 9 + b := make([]byte, size) + // AllocsPerRun returns an integral value. + marshalAllocs := testing.AllocsPerRun(count, func() { + _, err := prototext.MarshalOptions{}.MarshalAppend(b[:0], m) + if err != nil { + t.Fatal(err) + } + }) + b = nil + marshalAppendAllocs := testing.AllocsPerRun(count, func() { + var err error + b, err = prototext.MarshalOptions{}.MarshalAppend(b, m) + if err != nil { + t.Fatal(err) + } + }) + if marshalAllocs != marshalAppendAllocs { + t.Errorf("%v allocs/op when writing to a preallocated buffer", marshalAllocs) + t.Errorf("%v allocs/op when repeatedly appending to a slice", marshalAppendAllocs) + t.Errorf("expect amortized allocs/op to be identical") + } +} diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go index fbdf3487..934f2dcb 100644 --- a/internal/encoding/json/encode.go +++ b/internal/encoding/json/encode.go @@ -41,8 +41,10 @@ type Encoder struct { // // If indent is a non-empty string, it causes every entry for an Array or Object // to be preceded by the indent and trailed by a newline. -func NewEncoder(indent string) (*Encoder, error) { - e := &Encoder{} +func NewEncoder(buf []byte, indent string) (*Encoder, error) { + e := &Encoder{ + out: buf, + } if len(indent) > 0 { if strings.Trim(indent, " \t") != "" { return nil, errors.New("indent may only be composed of space or tab characters") @@ -176,13 +178,13 @@ func appendFloat(out []byte, n float64, bitSize int) []byte { // WriteInt writes out the given signed integer in JSON number value. func (e *Encoder) WriteInt(n int64) { e.prepareNext(scalar) - e.out = append(e.out, strconv.FormatInt(n, 10)...) + e.out = strconv.AppendInt(e.out, n, 10) } // WriteUint writes out the given unsigned integer in JSON number value. func (e *Encoder) WriteUint(n uint64) { e.prepareNext(scalar) - e.out = append(e.out, strconv.FormatUint(n, 10)...) + e.out = strconv.AppendUint(e.out, n, 10) } // StartObject writes out the '{' symbol. diff --git a/internal/encoding/json/encode_test.go b/internal/encoding/json/encode_test.go index 5370f8b8..c844b553 100644 --- a/internal/encoding/json/encode_test.go +++ b/internal/encoding/json/encode_test.go @@ -356,7 +356,7 @@ func TestEncoder(t *testing.T) { for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { if tc.wantOut != "" { - enc, err := json.NewEncoder("") + enc, err := json.NewEncoder(nil, "") if err != nil { t.Fatalf("NewEncoder() returned error: %v", err) } @@ -367,7 +367,7 @@ func TestEncoder(t *testing.T) { } } if tc.wantOutIndent != "" { - enc, err := json.NewEncoder("\t") + enc, err := json.NewEncoder(nil, "\t") if err != nil { t.Fatalf("NewEncoder() returned error: %v", err) } @@ -387,7 +387,7 @@ func TestWriteStringError(t *testing.T) { for _, in := range tests { t.Run(in, func(t *testing.T) { - enc, err := json.NewEncoder("") + enc, err := json.NewEncoder(nil, "") if err != nil { t.Fatalf("NewEncoder() returned error: %v", err) } diff --git a/internal/encoding/text/encode.go b/internal/encoding/text/encode.go index da289ccc..cf7aed77 100644 --- a/internal/encoding/text/encode.go +++ b/internal/encoding/text/encode.go @@ -53,8 +53,10 @@ type encoderState struct { // If outputASCII is true, strings will be serialized in such a way that // multi-byte UTF-8 sequences are escaped. This property ensures that the // overall output is ASCII (as opposed to UTF-8). -func NewEncoder(indent string, delims [2]byte, outputASCII bool) (*Encoder, error) { - e := &Encoder{} +func NewEncoder(buf []byte, indent string, delims [2]byte, outputASCII bool) (*Encoder, error) { + e := &Encoder{ + encoderState: encoderState{out: buf}, + } if len(indent) > 0 { if strings.Trim(indent, " \t") != "" { return nil, errors.New("indent may only be composed of space and tab characters") @@ -195,13 +197,13 @@ func appendFloat(out []byte, n float64, bitSize int) []byte { // WriteInt writes out the given signed integer value. func (e *Encoder) WriteInt(n int64) { e.prepareNext(scalar) - e.out = append(e.out, strconv.FormatInt(n, 10)...) + e.out = strconv.AppendInt(e.out, n, 10) } // WriteUint writes out the given unsigned integer value. func (e *Encoder) WriteUint(n uint64) { e.prepareNext(scalar) - e.out = append(e.out, strconv.FormatUint(n, 10)...) + e.out = strconv.AppendUint(e.out, n, 10) } // WriteLiteral writes out the given string as a literal value without quotes. diff --git a/internal/encoding/text/encode_test.go b/internal/encoding/text/encode_test.go index af74690f..d9c50098 100644 --- a/internal/encoding/text/encode_test.go +++ b/internal/encoding/text/encode_test.go @@ -341,7 +341,7 @@ func runEncoderTest(t *testing.T, tc encoderTestCase, delims [2]byte) { t.Helper() if tc.wantOut != "" { - enc, err := text.NewEncoder("", delims, false) + enc, err := text.NewEncoder(nil, "", delims, false) if err != nil { t.Fatalf("NewEncoder returned error: %v", err) } @@ -352,7 +352,7 @@ func runEncoderTest(t *testing.T, tc encoderTestCase, delims [2]byte) { } } if tc.wantOutIndent != "" { - enc, err := text.NewEncoder("\t", delims, false) + enc, err := text.NewEncoder(nil, "\t", delims, false) if err != nil { t.Fatalf("NewEncoder returned error: %v", err) } @@ -520,7 +520,7 @@ func runEncodeStringsTest(t *testing.T, in string, want string, outputASCII bool charType = "ASCII" } - enc, err := text.NewEncoder("", [2]byte{}, outputASCII) + enc, err := text.NewEncoder(nil, "", [2]byte{}, outputASCII) if err != nil { t.Fatalf("[%s] NewEncoder returned error: %v", charType, err) } @@ -532,7 +532,7 @@ func runEncodeStringsTest(t *testing.T, in string, want string, outputASCII bool } func TestReset(t *testing.T) { - enc, err := text.NewEncoder("\t", [2]byte{}, false) + enc, err := text.NewEncoder(nil, "\t", [2]byte{}, false) if err != nil { t.Fatalf("NewEncoder returned error: %v", err) }