diff options
Diffstat (limited to 'src/encoding')
| -rw-r--r-- | src/encoding/json/encode.go | 18 | ||||
| -rw-r--r-- | src/encoding/json/indent.go | 104 | ||||
| -rw-r--r-- | src/encoding/json/stream.go | 10 |
3 files changed, 69 insertions, 63 deletions
diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index 9d59b0ff2b..d2f752a4f8 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -175,12 +175,12 @@ func MarshalIndent(v any, prefix, indent string) ([]byte, error) { if err != nil { return nil, err } - var buf bytes.Buffer - err = Indent(&buf, b, prefix, indent) + b2 := make([]byte, 0, indentGrowthFactor*len(b)) + b2, err = appendIndent(b2, b, prefix, indent) if err != nil { return nil, err } - return buf.Bytes(), nil + return b2, nil } // HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 @@ -476,8 +476,10 @@ func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { } b, err := m.MarshalJSON() if err == nil { - // copy JSON into buffer, checking validity. - err = compact(&e.Buffer, b, opts.escapeHTML) + e.Grow(len(b)) + out := availableBuffer(&e.Buffer) + out, err = appendCompact(out, b, opts.escapeHTML) + e.Buffer.Write(out) } if err != nil { e.error(&MarshalerError{v.Type(), err, "MarshalJSON"}) @@ -493,8 +495,10 @@ func addrMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { m := va.Interface().(Marshaler) b, err := m.MarshalJSON() if err == nil { - // copy JSON into buffer, checking validity. - err = compact(&e.Buffer, b, opts.escapeHTML) + e.Grow(len(b)) + out := availableBuffer(&e.Buffer) + out, err = appendCompact(out, b, opts.escapeHTML) + e.Buffer.Write(out) } if err != nil { e.error(&MarshalerError{v.Type(), err, "MarshalJSON"}) diff --git a/src/encoding/json/indent.go b/src/encoding/json/indent.go index 2924d3b49b..375f71605a 100644 --- a/src/encoding/json/indent.go +++ b/src/encoding/json/indent.go @@ -4,69 +4,73 @@ package json -import ( - "bytes" -) +import "bytes" + +// TODO(https://go.dev/issue/53685): Use bytes.Buffer.AvailableBuffer instead. +func availableBuffer(b *bytes.Buffer) []byte { + return b.Bytes()[b.Len():] +} // Compact appends to dst the JSON-encoded src with // insignificant space characters elided. func Compact(dst *bytes.Buffer, src []byte) error { - return compact(dst, src, false) + dst.Grow(len(src)) + b := availableBuffer(dst) + b, err := appendCompact(b, src, false) + dst.Write(b) + return err } -func compact(dst *bytes.Buffer, src []byte, escape bool) error { - origLen := dst.Len() +func appendCompact(dst, src []byte, escape bool) ([]byte, error) { + origLen := len(dst) scan := newScanner() defer freeScanner(scan) start := 0 for i, c := range src { if escape && (c == '<' || c == '>' || c == '&') { - if start < i { - dst.Write(src[start:i]) - } - dst.WriteString(`\u00`) - dst.WriteByte(hex[c>>4]) - dst.WriteByte(hex[c&0xF]) + dst = append(dst, src[start:i]...) + dst = append(dst, '\\', 'u', '0', '0', hex[c>>4], hex[c&0xF]) start = i + 1 } // Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9). if escape && c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 { - if start < i { - dst.Write(src[start:i]) - } - dst.WriteString(`\u202`) - dst.WriteByte(hex[src[i+2]&0xF]) - start = i + 3 + dst = append(dst, src[start:i]...) + dst = append(dst, '\\', 'u', '2', '0', '2', hex[src[i+2]&0xF]) + start = i + len("\u2029") } v := scan.step(scan, c) if v >= scanSkipSpace { if v == scanError { break } - if start < i { - dst.Write(src[start:i]) - } + dst = append(dst, src[start:i]...) start = i + 1 } } if scan.eof() == scanError { - dst.Truncate(origLen) - return scan.err - } - if start < len(src) { - dst.Write(src[start:]) + return dst[:origLen], scan.err } - return nil + dst = append(dst, src[start:]...) + return dst, nil } -func newline(dst *bytes.Buffer, prefix, indent string, depth int) { - dst.WriteByte('\n') - dst.WriteString(prefix) +func appendNewline(dst []byte, prefix, indent string, depth int) []byte { + dst = append(dst, '\n') + dst = append(dst, prefix...) for i := 0; i < depth; i++ { - dst.WriteString(indent) + dst = append(dst, indent...) } + return dst } +// indentGrowthFactor specifies the growth factor of indenting JSON input. +// Empirically, the growth factor was measured to be between 1.4x to 1.8x +// for some set of compacted JSON with the indent being a single tab. +// Specify a growth factor slightly larger than what is observed +// to reduce probability of allocation in appendIndent. +// A factor no higher than 2 ensures that wasted space never exceeds 50%. +const indentGrowthFactor = 2 + // Indent appends to dst an indented form of the JSON-encoded src. // Each element in a JSON object or array begins on a new, // indented line beginning with prefix followed by one or more @@ -79,7 +83,15 @@ func newline(dst *bytes.Buffer, prefix, indent string, depth int) { // For example, if src has no trailing spaces, neither will dst; // if src ends in a trailing newline, so will dst. func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { - origLen := dst.Len() + dst.Grow(indentGrowthFactor * len(src)) + b := availableBuffer(dst) + b, err := appendIndent(b, src, prefix, indent) + dst.Write(b) + return err +} + +func appendIndent(dst, src []byte, prefix, indent string) ([]byte, error) { + origLen := len(dst) scan := newScanner() defer freeScanner(scan) needIndent := false @@ -96,13 +108,13 @@ func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { if needIndent && v != scanEndObject && v != scanEndArray { needIndent = false depth++ - newline(dst, prefix, indent, depth) + dst = appendNewline(dst, prefix, indent, depth) } // Emit semantically uninteresting bytes // (in particular, punctuation in strings) unmodified. if v == scanContinue { - dst.WriteByte(c) + dst = append(dst, c) continue } @@ -111,33 +123,27 @@ func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { case '{', '[': // delay indent so that empty object and array are formatted as {} and []. needIndent = true - dst.WriteByte(c) - + dst = append(dst, c) case ',': - dst.WriteByte(c) - newline(dst, prefix, indent, depth) - + dst = append(dst, c) + dst = appendNewline(dst, prefix, indent, depth) case ':': - dst.WriteByte(c) - dst.WriteByte(' ') - + dst = append(dst, c, ' ') case '}', ']': if needIndent { // suppress indent in empty object/array needIndent = false } else { depth-- - newline(dst, prefix, indent, depth) + dst = appendNewline(dst, prefix, indent, depth) } - dst.WriteByte(c) - + dst = append(dst, c) default: - dst.WriteByte(c) + dst = append(dst, c) } } if scan.eof() == scanError { - dst.Truncate(origLen) - return scan.err + return dst[:origLen], scan.err } - return nil + return dst, nil } diff --git a/src/encoding/json/stream.go b/src/encoding/json/stream.go index 1442ef29ef..b4146a359e 100644 --- a/src/encoding/json/stream.go +++ b/src/encoding/json/stream.go @@ -183,7 +183,7 @@ type Encoder struct { err error escapeHTML bool - indentBuf *bytes.Buffer + indentBuf []byte indentPrefix string indentValue string } @@ -221,15 +221,11 @@ func (enc *Encoder) Encode(v any) error { b := e.Bytes() if enc.indentPrefix != "" || enc.indentValue != "" { - if enc.indentBuf == nil { - enc.indentBuf = new(bytes.Buffer) - } - enc.indentBuf.Reset() - err = Indent(enc.indentBuf, b, enc.indentPrefix, enc.indentValue) + enc.indentBuf, err = appendIndent(enc.indentBuf[:0], b, enc.indentPrefix, enc.indentValue) if err != nil { return err } - b = enc.indentBuf.Bytes() + b = enc.indentBuf } if _, err = enc.w.Write(b); err != nil { enc.err = err |
