Repository: fxamacker/cbor
Pull request #533 (2024-05-08 16:52:33)

Improve speed & memory use for Diagnose() and DiagnoseFirst()
<!--
Thank you for your interest in contributing to fxamacker/cbor!
-->

### Description

<!-- For code contributions, please complete all the items below this line. -->
<!-- For documentation-only contributions, please delete everything below this line. -->

This PR improves efficiency of functions in `diagnose.go` used by `Diagnose()` and `DiagnoseFirst()`, especially related to use of `encoding/hex`, `encoding/base32`, and `encoding/base64`.

The "Encoder" types for the various byte string diagnostic encodings (encoding/hex, encoding/base32,
and encoding/base64) include sizable internal buffers (1KiB at time of writing), and a new encoder
is created to produce the diagnostic encoding of every byte string in the input. This results in a
lot of extra allocations, especially for inputs containing many small byte strings.

```
                                        │  before.txt  │              after.txt              │
                                        │    sec/op    │   sec/op     vs base                │
Diagnose/byte_string_base16_encoding      315.90n ± 0%   95.03n ± 1%  -69.92% (p=0.000 n=10)
Diagnose/byte_string_base32_encoding      325.55n ± 0%   99.27n ± 1%  -69.51% (p=0.000 n=10)
Diagnose/byte_string_base32hex_encoding    325.4n ± 0%   100.2n ± 1%  -69.21% (p=0.000 n=10)
Diagnose/byte_string_base64url_encoding    336.5n ± 0%   100.7n ± 2%  -70.07% (p=0.000 n=10)
geomean                                    325.8n        98.78n       -69.68%

                                        │  before.txt  │             after.txt              │
                                        │     B/op     │    B/op     vs base                │
Diagnose/byte_string_base16_encoding      1344.00 ± 0%   80.00 ± 0%  -94.05% (p=0.000 n=10)
Diagnose/byte_string_base32_encoding      1344.00 ± 0%   80.00 ± 0%  -94.05% (p=0.000 n=10)
Diagnose/byte_string_base32hex_encoding   1344.00 ± 0%   80.00 ± 0%  -94.05% (p=0.000 n=10)
Diagnose/byte_string_base64url_encoding   1344.00 ± 0%   80.00 ± 0%  -94.05% (p=0.000 n=10)
geomean                                   1.313Ki        80.00       -94.05%

                                        │ before.txt │             after.txt              │
                                        │ allocs/op  │ allocs/op   vs base                │
Diagnose/byte_string_base16_encoding      5.000 ± 0%   2.000 ± 0%  -60.00% (p=0.000 n=10)
Diagnose/byte_string_base32_encoding      5.000 ± 0%   2.000 ± 0%  -60.00% (p=0.000 n=10)
Diagnose/byte_string_base32hex_encoding   5.000 ± 0%   2.000 ± 0%  -60.00% (p=0.000 n=10)
Diagnose/byte_string_base64url_encoding   5.000 ± 0%   2.000 ± 0%  -60.00% (p=0.000 n=10)
geomean                                   5.000        2.000       -60.00%
```

Producing the \uxxxx escape sequences also made a few more allocations than necessary.

```
                                          │ before.txt  │              after.txt              │
                                          │   sec/op    │   sec/op     vs base                │
Diagnose/escaped_character_in_text_string   180.3n ± 0%   109.2n ± 2%  -39.41% (p=0.000 n=10)

                                          │ before.txt  │             after.txt              │
                                          │    B/op     │    B/op     vs base                │
Diagnose/escaped_character_in_text_string   192.00 ± 0%   72.00 ± 0%  -62.50% (p=0.000 n=10)

                                          │ before.txt │             after.txt              │
                                          │ allocs/op  │ allocs/op   vs base                │
Diagnose/escaped_character_in_text_string   5.000 ± 0%   2.000 ± 0%  -60.00% (p=0.000 n=10)
```

#### PR Was Proposed and Welcomed in Currently Open Issue

- [ ] This PR was proposed and welcomed by maintainer(s) in issue #___
- [ ] Closes or Updates Issue #___

#### Checklist (for code PR only, ignore for docs PR)

- [x] Include unit tests that cover the new code
- [x] Pass all unit tests 
- [x] Pass all lint checks in CI (goimports, gosec, staticcheck, etc.)
- [x] Sign each commit with your real name and email.  
      Last line of each commit message should be in this format:  
      Signed-off-by: Firstname Lastname <firstname.lastname@example.com>
- [x] Certify the Developer's Certificate of Origin 1.1
      (see next section).

#### Certify the Developer's Certificate of Origin 1.1

- [x] By marking this item as completed, I certify 
      the Developer Certificate of Origin 1.1.

```
Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
```




Patch:
diff --git a/diagnose.go b/diagnose.go
index 7b4a467..ef406e0 100644
--- a/diagnose.go
+++ b/diagnose.go
@@ -9,6 +9,7 @@ import (
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
+	"fmt"
 	"io"
 	"math"
 	"math/big"
@@ -198,9 +199,7 @@ func (di *diagnose) diag(cborSequence bool) (string, error) {
 		switch err := di.wellformed(cborSequence); err {
 		case nil:
 			if !firstItem {
-				if err = di.writeString(", "); err != nil {
-					return di.w.String(), err
-				}
+				di.w.WriteString(", ")
 			}
 			firstItem = false
 			if err = di.item(); err != nil {
@@ -250,23 +249,21 @@ func (di *diagnose) item() error { //nolint:gocyclo
 			switch initialByte {
 			case 0x5f:
 				// indefinite-length bytes with no chunks.
-				return di.writeString(`''_`)
+				di.w.WriteString(`''_`)
+				return nil
 			case 0x7f:
 				// indefinite-length text with no chunks.
-				return di.writeString(`""_`)
+				di.w.WriteString(`""_`)
+				return nil
 			}
 		}
 
-		if err := di.writeString("(_ "); err != nil {
-			return err
-		}
+		di.w.WriteString("(_ ")
 
 		i := 0
 		for !di.d.foundBreak() {
 			if i > 0 {
-				if err := di.writeString(", "); err != nil {
-					return err
-				}
+				di.w.WriteString(", ")
 			}
 
 			i++
@@ -276,20 +273,17 @@ func (di *diagnose) item() error { //nolint:gocyclo
 			}
 		}
 
-		return di.writeByte(')')
+		di.w.WriteByte(')')
+		return nil
 
 	case 0x9f: // indefinite-length array
 		di.d.off++
-		if err := di.writeString("[_ "); err != nil {
-			return err
-		}
+		di.w.WriteString("[_ ")
 
 		i := 0
 		for !di.d.foundBreak() {
 			if i > 0 {
-				if err := di.writeString(", "); err != nil {
-					return err
-				}
+				di.w.WriteString(", ")
 			}
 
 			i++
@@ -298,20 +292,17 @@ func (di *diagnose) item() error { //nolint:gocyclo
 			}
 		}
 
-		return di.writeByte(']')
+		di.w.WriteByte(']')
+		return nil
 
 	case 0xbf: // indefinite-length map
 		di.d.off++
-		if err := di.writeString("{_ "); err != nil {
-			return err
-		}
+		di.w.WriteString("{_ ")
 
 		i := 0
 		for !di.d.foundBreak() {
 			if i > 0 {
-				if err := di.writeString(", "); err != nil {
-					return err
-				}
+				di.w.WriteString(", ")
 			}
 
 			i++
@@ -320,9 +311,7 @@ func (di *diagnose) item() error { //nolint:gocyclo
 				return err
 			}
 
-			if err := di.writeString(": "); err != nil {
-				return err
-			}
+			di.w.WriteString(": ")
 
 			// value
 			if err := di.item(); err != nil {
@@ -330,14 +319,16 @@ func (di *diagnose) item() error { //nolint:gocyclo
 			}
 		}
 
-		return di.writeByte('}')
+		di.w.WriteByte('}')
+		return nil
 	}
 
 	t := di.d.nextCBORType()
 	switch t {
 	case cborTypePositiveInt:
 		_, _, val := di.d.getHead()
-		return di.writeString(strconv.FormatUint(val, 10))
+		di.w.WriteString(strconv.FormatUint(val, 10))
+		return nil
 
 	case cborTypeNegativeInt:
 		_, _, val := di.d.getHead()
@@ -347,11 +338,13 @@ func (di *diagnose) item() error { //nolint:gocyclo
 			bi.SetUint64(val)
 			bi.Add(bi, big.NewInt(1))
 			bi.Neg(bi)
-			return di.writeString(bi.String())
+			di.w.WriteString(bi.String())
+			return nil
 		}
 
 		nValue := int64(-1) ^ int64(val)
-		return di.writeString(strconv.FormatInt(nValue, 10))
+		di.w.WriteString(strconv.FormatInt(nValue, 10))
+		return nil
 
 	case cborTypeByteString:
 		b, _ := di.d.parseByteString()
@@ -367,48 +360,40 @@ func (di *diagnose) item() error { //nolint:gocyclo
 	case cborTypeArray:
 		_, _, val := di.d.getHead()
 		count := int(val)
-		if err := di.writeByte('['); err != nil {
-			return err
-		}
+		di.w.WriteByte('[')
 
 		for i := 0; i < count; i++ {
 			if i > 0 {
-				if err := di.writeString(", "); err != nil {
-					return err
-				}
+				di.w.WriteString(", ")
 			}
 			if err := di.item(); err != nil {
 				return err
 			}
 		}
-		return di.writeByte(']')
+		di.w.WriteByte(']')
+		return nil
 
 	case cborTypeMap:
 		_, _, val := di.d.getHead()
 		count := int(val)
-		if err := di.writeByte('{'); err != nil {
-			return err
-		}
+		di.w.WriteByte('{')
 
 		for i := 0; i < count; i++ {
 			if i > 0 {
-				if err := di.writeString(", "); err != nil {
-					return err
-				}
+				di.w.WriteString(", ")
 			}
 			// key
 			if err := di.item(); err != nil {
 				return err
 			}
-			if err := di.writeString(": "); err != nil {
-				return err
-			}
+			di.w.WriteString(": ")
 			// value
 			if err := di.item(); err != nil {
 				return err
 			}
 		}
-		return di.writeByte('}')
+		di.w.WriteByte('}')
+		return nil
 
 	case cborTypeTag:
 		_, _, tagNum := di.d.getHead()
@@ -420,7 +405,8 @@ func (di *diagnose) item() error { //nolint:gocyclo
 
 			b, _ := di.d.parseByteString()
 			bi := new(big.Int).SetBytes(b)
-			return di.writeString(bi.String())
+			di.w.WriteString(bi.String())
+			return nil
 
 		case 3:
 			if nt := di.d.nextCBORType(); nt != cborTypeByteString {
@@ -431,71 +417,63 @@ func (di *diagnose) item() error { //nolint:gocyclo
 			bi := new(big.Int).SetBytes(b)
 			bi.Add(bi, big.NewInt(1))
 			bi.Neg(bi)
-			return di.writeString(bi.String())
+			di.w.WriteString(bi.String())
+			return nil
 
 		default:
-			if err := di.writeString(strconv.FormatUint(tagNum, 10)); err != nil {
-				return err
-			}
-			if err := di.writeByte('('); err != nil {
-				return err
-			}
+			di.w.WriteString(strconv.FormatUint(tagNum, 10))
+			di.w.WriteByte('(')
 			if err := di.item(); err != nil {
 				return err
 			}
-			return di.writeByte(')')
+			di.w.WriteByte(')')
+			return nil
 		}
 
 	case cborTypePrimitives:
 		_, ai, val := di.d.getHead()
 		switch ai {
 		case 20:
-			return di.writeString("false")
+			di.w.WriteString("false")
+			return nil
 
 		case 21:
-			return di.writeString("true")
+			di.w.WriteString("true")
+			return nil
 
 		case 22:
-			return di.writeString("null")
+			di.w.WriteString("null")
+			return nil
 
 		case 23:
-			return di.writeString("undefined")
+			di.w.WriteString("undefined")
+			return nil
 
 		case 25, 26, 27:
 			return di.encodeFloat(ai, val)
 
 		default:
-			if err := di.writeString("simple("); err != nil {
-				return err
-			}
-			if err := di.writeString(strconv.FormatUint(val, 10)); err != nil {
-				return err
-			}
-			return di.writeByte(')')
+			di.w.WriteString("simple(")
+			di.w.WriteString(strconv.FormatUint(val, 10))
+			di.w.WriteByte(')')
+			return nil
 		}
 	}
 
 	return nil
 }
 
-func (di *diagnose) writeByte(val byte) error {
-	return di.w.WriteByte(val)
-}
-
-func (di *diagnose) writeString(val string) error {
-	_, err := di.w.WriteString(val)
-	return err
-}
-
 // writeU16 format a rune as "\uxxxx"
-func (di *diagnose) writeU16(val rune) error {
-	if err := di.writeString("\\u"); err != nil {
-		return err
-	}
-	b := make([]byte, 2)
-	b[0] = byte(val >> 8)
-	b[1] = byte(val)
-	return di.writeString(hex.EncodeToString(b))
+func (di *diagnose) writeU16(val rune) {
+	di.w.WriteString("\\u")
+	var in [2]byte
+	in[0] = byte(val >> 8)
+	in[1] = byte(val)
+	sz := hex.EncodedLen(len(in))
+	di.w.Grow(sz)
+	dst := di.w.Bytes()[di.w.Len() : di.w.Len()+sz]
+	hex.Encode(dst, in[:])
+	di.w.Write(dst)
 }
 
 var rawBase32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
@@ -511,77 +489,77 @@ func (di *diagnose) encodeByteString(val []byte) error {
 			di2 := newDiagnose(val, di.dm.decMode, di.dm)
 			// should always notating embedded CBOR sequence.
 			if str, err := di2.diag(true); err == nil {
-				if err := di.writeString("<<"); err != nil {
-					return err
-				}
-				if err := di.writeString(str); err != nil {
-					return err
-				}
-				return di.writeString(">>")
+				di.w.WriteString("<<")
+				di.w.WriteString(str)
+				di.w.WriteString(">>")
+				return nil
 			}
 		}
 	}
 
 	switch di.dm.byteStringEncoding {
 	case ByteStringBase16Encoding:
-		if err := di.writeString("h'"); err != nil {
-			return err
-		}
-
-		encoder := hex.NewEncoder(di.w)
+		di.w.WriteString("h'")
 		if di.dm.byteStringHexWhitespace {
-			for i, b := range val {
+			sz := hex.EncodedLen(len(val))
+			if len(val) > 0 {
+				sz += len(val) - 1
+			}
+			di.w.Grow(sz)
+
+			dst := di.w.Bytes()[di.w.Len():]
+			for i := range val {
 				if i > 0 {
-					if err := di.writeByte(' '); err != nil {
-						return err
-					}
-				}
-				if _, err := encoder.Write([]byte{b}); err != nil {
-					return err
+					dst = append(dst, ' ')
 				}
+				hex.Encode(dst[len(dst):len(dst)+2], val[i:i+1])
+				dst = dst[:len(dst)+2]
 			}
+			di.w.Write(dst)
 		} else {
-			if _, err := encoder.Write(val); err != nil {
-				return err
-			}
+			sz := hex.EncodedLen(len(val))
+			di.w.Grow(sz)
+			dst := di.w.Bytes()[di.w.Len() : di.w.Len()+sz]
+			hex.Encode(dst, val)
+			di.w.Write(dst)
 		}
-		return di.writeByte('\'')
+		di.w.WriteByte('\'')
+		return nil
 
 	case ByteStringBase32Encoding:
-		if err := di.writeString("b32'"); err != nil {
-			return err
-		}
-		encoder := base32.NewEncoder(rawBase32Encoding, di.w)
-		if _, err := encoder.Write(val); err != nil {
-			return err
-		}
-		encoder.Close()
-		return di.writeByte('\'')
+		di.w.WriteString("b32'")
+		sz := rawBase32Encoding.EncodedLen(len(val))
+		di.w.Grow(sz)
+		dst := di.w.Bytes()[di.w.Len() : di.w.Len()+sz]
+		rawBase32Encoding.Encode(dst, val)
+		di.w.Write(dst)
+		di.w.WriteByte('\'')
+		return nil
 
 	case ByteStringBase32HexEncoding:
-		if err := di.writeString("h32'"); err != nil {
-			return err
-		}
-		encoder := base32.NewEncoder(rawBase32HexEncoding, di.w)
-		if _, err := encoder.Write(val); err != nil {
-			return err
-		}
-		encoder.Close()
-		return di.writeByte('\'')
+		di.w.WriteString("h32'")
+		sz := rawBase32HexEncoding.EncodedLen(len(val))
+		di.w.Grow(sz)
+		dst := di.w.Bytes()[di.w.Len() : di.w.Len()+sz]
+		rawBase32HexEncoding.Encode(dst, val)
+		di.w.Write(dst)
+		di.w.WriteByte('\'')
+		return nil
 
 	case ByteStringBase64Encoding:
-		if err := di.writeString("b64'"); err != nil {
-			return err
-		}
-		encoder := base64.NewEncoder(base64.RawURLEncoding, di.w)
-		if _, err := encoder.Write(val); err != nil {
-			return err
-		}
-		encoder.Close()
-		return di.writeByte('\'')
+		di.w.WriteString("b64'")
+		sz := base64.RawURLEncoding.EncodedLen(len(val))
+		di.w.Grow(sz)
+		dst := di.w.Bytes()[di.w.Len() : di.w.Len()+sz]
+		base64.RawURLEncoding.Encode(dst, val)
+		di.w.Write(dst)
+		di.w.WriteByte('\'')
+		return nil
 
 	default:
-		return di.dm.byteStringEncoding.valid()
+		// It should not be possible for users to construct a *diagMode with an invalid byte
+		// string encoding.
+		panic(fmt.Sprintf("diagmode has invalid ByteStringEncoding %v", di.dm.byteStringEncoding))
 	}
 }
 
@@ -589,17 +567,13 @@ var utf16SurrSelf = rune(0x10000)
 
 // quote should be either `'` or `"`
 func (di *diagnose) encodeTextString(val string, quote byte) error {
-	if err := di.writeByte(quote); err != nil {
-		return err
-	}
+	di.w.WriteByte(quote)
 
 	for i := 0; i < len(val); {
 		if b := val[i]; b < utf8.RuneSelf {
 			switch {
 			case b == '\t', b == '\n', b == '\r', b == '\\', b == quote:
-				if err := di.writeByte('\\'); err != nil {
-					return err
-				}
+				di.w.WriteByte('\\')
 
 				switch b {
 				case '\t':
@@ -609,19 +583,13 @@ func (di *diagnose) encodeTextString(val string, quote byte) error {
 				case '\r':
 					b = 'r'
 				}
-				if err := di.writeByte(b); err != nil {
-					return err
-				}
+				di.w.WriteByte(b)
 
 			case b >= ' ' && b <= '~':
-				if err := di.writeByte(b); err != nil {
-					return err
-				}
+				di.w.WriteByte(b)
 
 			default:
-				if err := di.writeU16(rune(b)); err != nil {
-					return err
-				}
+				di.writeU16(rune(b))
 			}
 
 			i++
@@ -631,30 +599,22 @@ func (di *diagnose) encodeTextString(val string, quote byte) error {
 		c, size := utf8.DecodeRuneInString(val[i:])
 		switch {
 		case c == utf8.RuneError:
-			// if err := di.writeU16(rune(val[i])); err != nil {
-			// 	return err
-			// }
 			return &SemanticError{"cbor: invalid UTF-8 string"}
 
 		case c < utf16SurrSelf:
-			if err := di.writeU16(c); err != nil {
-				return err
-			}
+			di.writeU16(c)
 
 		default:
 			c1, c2 := utf16.EncodeRune(c)
-			if err := di.writeU16(c1); err != nil {
-				return err
-			}
-			if err := di.writeU16(c2); err != nil {
-				return err
-			}
+			di.writeU16(c1)
+			di.writeU16(c2)
 		}
 
 		i += size
 	}
 
-	return di.writeByte(quote)
+	di.w.WriteByte(quote)
+	return nil
 }
 
 func (di *diagnose) encodeFloat(ai byte, val uint64) error {
@@ -664,11 +624,14 @@ func (di *diagnose) encodeFloat(ai byte, val uint64) error {
 		f16 := float16.Frombits(uint16(val))
 		switch {
 		case f16.IsNaN():
-			return di.writeString("NaN")
+			di.w.WriteString("NaN")
+			return nil
 		case f16.IsInf(1):
-			return di.writeString("Infinity")
+			di.w.WriteString("Infinity")
+			return nil
 		case f16.IsInf(-1):
-			return di.writeString("-Infinity")
+			di.w.WriteString("-Infinity")
+			return nil
 		default:
 			f64 = float64(f16.Float32())
 		}
@@ -677,11 +640,14 @@ func (di *diagnose) encodeFloat(ai byte, val uint64) error {
 		f32 := math.Float32frombits(uint32(val))
 		switch {
 		case f32 != f32:
-			return di.writeString("NaN")
+			di.w.WriteString("NaN")
+			return nil
 		case f32 > math.MaxFloat32:
-			return di.writeString("Infinity")
+			di.w.WriteString("Infinity")
+			return nil
 		case f32 < -math.MaxFloat32:
-			return di.writeString("-Infinity")
+			di.w.WriteString("-Infinity")
+			return nil
 		default:
 			f64 = float64(f32)
 		}
@@ -690,11 +656,14 @@ func (di *diagnose) encodeFloat(ai byte, val uint64) error {
 		f64 = math.Float64frombits(val)
 		switch {
 		case f64 != f64:
-			return di.writeString("NaN")
+			di.w.WriteString("NaN")
+			return nil
 		case f64 > math.MaxFloat64:
-			return di.writeString("Infinity")
+			di.w.WriteString("Infinity")
+			return nil
 		case f64 < -math.MaxFloat64:
-			return di.writeString("-Infinity")
+			di.w.WriteString("-Infinity")
+			return nil
 		}
 	}
 	// Use ES6 number to string conversion which should match most JSON generators.
@@ -722,18 +691,19 @@ func (di *diagnose) encodeFloat(ai byte, val uint64) error {
 		}
 	}
 
-	if err := di.writeString(string(b)); err != nil {
-		return err
-	}
+	di.w.WriteString(string(b))
 
 	if di.dm.floatPrecisionIndicator {
 		switch ai {
 		case 25:
-			return di.writeString("_1")
+			di.w.WriteString("_1")
+			return nil
 		case 26:
-			return di.writeString("_2")
+			di.w.WriteString("_2")
+			return nil
 		case 27:
-			return di.writeString("_3")
+			di.w.WriteString("_3")
+			return nil
 		}
 	}
 


Test patch:
diff --git a/diagnose_test.go b/diagnose_test.go
index 8abf6a6..9835ef5 100644
--- a/diagnose_test.go
+++ b/diagnose_test.go
@@ -1102,3 +1102,49 @@ func TestDiagnoseEmptyData(t *testing.T) {
 		})
 	}
 }
+
+func BenchmarkDiagnose(b *testing.B) {
+	for _, tc := range []struct {
+		name  string
+		opts  DiagOptions
+		input []byte
+	}{
+		{
+			name:  "escaped character in text string",
+			opts:  DiagOptions{},
+			input: hexDecode("62c3bc"), // "\u00fc"
+		},
+		{
+			name:  "byte string base16 encoding",
+			opts:  DiagOptions{ByteStringEncoding: ByteStringBase16Encoding},
+			input: []byte("\x45hello"),
+		},
+		{
+			name:  "byte string base32 encoding",
+			opts:  DiagOptions{ByteStringEncoding: ByteStringBase32Encoding},
+			input: []byte("\x45hello"),
+		},
+		{
+			name:  "byte string base32hex encoding",
+			opts:  DiagOptions{ByteStringEncoding: ByteStringBase32HexEncoding},
+			input: []byte("\x45hello"),
+		},
+		{
+			name:  "byte string base64url encoding",
+			opts:  DiagOptions{ByteStringEncoding: ByteStringBase64Encoding},
+			input: []byte("\x45hello"),
+		},
+	} {
+		b.Run(tc.name, func(b *testing.B) {
+			dm, err := tc.opts.DiagMode()
+			if err != nil {
+				b.Fatal(err)
+			}
+
+			b.ResetTimer()
+			for i := 0; i < b.N; i++ {
+				_, _ = dm.Diagnose(tc.input)
+			}
+		})
+	}
+}