Repository: ogen-go/ogen
Pull request #726 (2022-12-16 01:53:18)

perf(uri): optimize parameter decoding


Patch:
diff --git a/uri/cursor.go b/uri/cursor.go
index e852d958..93ceb5da 100644
--- a/uri/cursor.go
+++ b/uri/cursor.go
@@ -1,67 +1,46 @@
 package uri
 
-import "io"
+import (
+	"io"
+	"strings"
+)
 
 type cursor struct {
-	src []rune
+	src string
 	pos int
 }
 
-func (c *cursor) readUntil(until rune) (string, error) {
-	var from, to = c.pos, c.pos
-	for {
-		r, ok := c.read()
-		if !ok {
-			return "", io.EOF
-		}
-
-		if r == until {
-			return string(c.src[from:to]), nil
-		}
-		to++
+func (c *cursor) readUntil(until byte) (string, error) {
+	from := c.pos
+	idx := strings.IndexByte(c.src[from:], until)
+	if idx < 0 {
+		c.pos = len(c.src)
+		return "", io.EOF
 	}
+	c.pos += idx + 1
+	return c.src[from : from+idx], nil
 }
 
-func (c *cursor) readValue(sep rune) (v string, hasNext bool, err error) {
-	var from, to = c.pos, c.pos
-	for {
-		r, ok := c.read()
-		if !ok {
-			if to-from == 0 {
-				return "", false, io.EOF
-			}
-			return string(c.src[from:to]), false, nil
-		}
-
-		if r == sep {
-			return string(c.src[from:to]), len(c.src) != c.pos, nil
+func (c *cursor) readValue(sep byte) (v string, hasNext bool, err error) {
+	before, _, ok := strings.Cut(c.src[c.pos:], string(sep))
+	if !ok {
+		if before == "" {
+			return "", false, io.EOF
 		}
-
-		to++
+		c.pos = len(c.src)
+		return before, false, nil
 	}
+	c.pos += len(before) + 1
+	return before, true, nil
 }
 
-func (c *cursor) read() (rune, bool) {
-	if len(c.src) == c.pos {
-		return rune(0), false
+func (c *cursor) eat(r byte) bool {
+	s := c.src[c.pos:]
+	if len(s) > 0 && s[0] == r {
+		c.pos++
+		return true
 	}
-
-	defer func() { c.pos++ }()
-	return c.src[c.pos], true
-}
-
-func (c *cursor) eat(r rune) bool {
-	rr, ok := c.read()
-	if !ok {
-		return false
-	}
-
-	if r != rr {
-		c.pos--
-		return false
-	}
-
-	return true
+	return false
 }
 
 func (c *cursor) readAll() (string, error) {
@@ -70,5 +49,5 @@ func (c *cursor) readAll() (string, error) {
 	}
 
 	defer func() { c.pos = len(c.src) }()
-	return string(c.src[c.pos:]), nil
+	return c.src[c.pos:], nil
 }
diff --git a/uri/header_param_decoder.go b/uri/header_param_decoder.go
index 12eda922..61c9bbf8 100644
--- a/uri/header_param_decoder.go
+++ b/uri/header_param_decoder.go
@@ -42,12 +42,12 @@ func (d *headerParamDecoder) DecodeFields(f func(field string, d Decoder) error)
 	}
 
 	pushField := func(field, value string) error { return f(field, constval{value}) }
-	kvSep, fieldSep := ',', ','
+	var kvSep, fieldSep byte = ',', ','
 	if d.explode {
 		kvSep = '='
 	}
 	return decodeObject(
-		(&cursor{src: []rune(val)}),
+		(&cursor{src: val}),
 		kvSep, fieldSep, pushField,
 	)
 }
diff --git a/uri/object.go b/uri/object.go
index 33018635..36e1a27f 100644
--- a/uri/object.go
+++ b/uri/object.go
@@ -34,7 +34,7 @@ func encodeObject(kvSep, fieldSep byte, fields []Field) string {
 	return sb.String()
 }
 
-func decodeObject(cur *cursor, kvSep, fieldSep rune, f func(field, value string) error) error {
+func decodeObject(cur *cursor, kvSep, fieldSep byte, f func(field, value string) error) error {
 	var (
 		fname string
 		field = true
diff --git a/uri/path_decoder.go b/uri/path_decoder.go
index a781e5b0..7ee9bc18 100644
--- a/uri/path_decoder.go
+++ b/uri/path_decoder.go
@@ -23,7 +23,7 @@ type PathDecoderConfig struct {
 
 func NewPathDecoder(cfg PathDecoderConfig) *PathDecoder {
 	return &PathDecoder{
-		cur:     &cursor{src: []rune(cfg.Value)},
+		cur:     &cursor{src: cfg.Value},
 		param:   cfg.Param,
 		style:   cfg.Style,
 		explode: cfg.Explode,
@@ -72,7 +72,7 @@ func (d *PathDecoder) DecodeArray(f func(d Decoder) error) error {
 			return errors.New(`value must begin with "."`)
 		}
 
-		delim := ','
+		delim := byte(',')
 		if d.explode {
 			delim = '.'
 		}
@@ -185,7 +185,7 @@ func (d *PathDecoder) DecodeFields(f func(name string, d Decoder) error) error {
 	}
 }
 
-func parseArray(cur *cursor, delim rune, f func(d Decoder) error) error {
+func parseArray(cur *cursor, delim byte, f func(d Decoder) error) error {
 	for {
 		value, hasNext, err := cur.readValue(delim)
 		if err != nil {
diff --git a/uri/query_param_decoder.go b/uri/query_param_decoder.go
index 02af8077..654c64b8 100644
--- a/uri/query_param_decoder.go
+++ b/uri/query_param_decoder.go
@@ -148,7 +148,7 @@ func (d *queryParamDecoder) DecodeFields(f func(name string, d Decoder) error) e
 			return errors.Errorf("query parameter %q multiple values", d.paramName)
 		}
 
-		cur := &cursor{src: []rune(values[0])}
+		cur := &cursor{src: values[0]}
 		return decodeObject(cur, ',', ',', adapter)
 
 	case QueryStyleSpaceDelimited:


Test patch:
diff --git a/uri/cursor_test.go b/uri/cursor_test.go
new file mode 100644
index 00000000..ff1b73e3
--- /dev/null
+++ b/uri/cursor_test.go
@@ -0,0 +1,118 @@
+package uri
+
+import (
+	"fmt"
+	"io"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func Test_cursor_readUntil(t *testing.T) {
+	a := require.New(t)
+	c := &cursor{
+		src: `abc,def,ghi`,
+	}
+
+	r, err := c.readUntil(',')
+	a.NoError(err)
+	a.Equal("abc", r)
+
+	r, err = c.readUntil(',')
+	a.NoError(err)
+	a.Equal("def", r)
+
+	_, err = c.readUntil(',')
+	a.ErrorIs(err, io.EOF)
+}
+
+func Test_cursor_readValue(t *testing.T) {
+	type state struct {
+		r       string
+		hasNext bool
+		err     error
+	}
+	for i, tt := range []struct {
+		input  string
+		sep    byte
+		states []state
+	}{
+		{
+			input: `abc`,
+			sep:   ',',
+			states: []state{
+				{r: "abc", hasNext: false},
+				{err: io.EOF},
+			},
+		},
+		{
+			input: `abc,def`,
+			sep:   ',',
+			states: []state{
+				{r: "abc", hasNext: true},
+				{r: "def", hasNext: false},
+				{err: io.EOF},
+			},
+		},
+		{
+			input: `abc,def,ghi`,
+			sep:   ',',
+			states: []state{
+				{r: "abc", hasNext: true},
+				{r: "def", hasNext: true},
+				{r: "ghi", hasNext: false},
+				{err: io.EOF},
+			},
+		},
+	} {
+		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
+			a := require.New(t)
+			c := &cursor{
+				src: tt.input,
+			}
+
+			for _, s := range tt.states {
+				r, hasNext, err := c.readValue(tt.sep)
+				if s.err != nil {
+					a.ErrorIs(err, s.err)
+					continue
+				}
+				a.NoError(err)
+				a.Equal(s.r, r)
+				a.Equal(s.hasNext, hasNext)
+			}
+		})
+	}
+}
+
+func BenchmarkCursor(b *testing.B) {
+	input := `abc,def,ghi`
+
+	var (
+		sinkErr error
+		sink    = make([]string, 0, 3)
+	)
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		sink = sink[:0]
+		c := &cursor{src: input}
+		sinkErr = parseArray(c, ',', func(d Decoder) error {
+			v, err := d.DecodeValue()
+			if err != nil {
+				return err
+			}
+			sink = append(sink, v)
+			return nil
+		})
+	}
+
+	if sinkErr != nil {
+		b.Fatal(sinkErr)
+	}
+	if len(sink) != 3 {
+		b.Fatalf("unexpected length: %d", len(sink))
+	}
+}