Repository: ogen-go/ogen
Pull request #626 (2022-10-15 15:59:06)

perf(gen): use `ht.ParseForm` instead of `ParseForm` method
```
name                time/op
ParseForm/Custom-4     289µs ± 3%
ParseForm/Std-4        562µs ± 3%

name                speed
ParseForm/Custom-4  42.5MB/s ± 3%
ParseForm/Std-4     21.9MB/s ± 3%

name                alloc/op
ParseForm/Custom-4     231kB ± 0%
ParseForm/Std-4        496kB ± 0%

name                allocs/op
ParseForm/Custom-4     1.06k ± 0%
ParseForm/Std-4        2.12k ± 0%
```

codecov[bot]: # [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/626?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) Report
Base: **73.71**% // Head: **73.71**% // Decreases project coverage by **`-0.00%`** :warning:
> Coverage data is based on head [(`bbaeee5`)](https://codecov.io/gh/ogen-go/ogen/pull/626?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) compared to base [(`73d0bba`)](https://codecov.io/gh/ogen-go/ogen/commit/73d0bba48aeb4587b699e8d7b071d8e9afa7b37a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #626      +/-   ##
==========================================
- Coverage   73.71%   73.71%   -0.01%     
==========================================
  Files         166      167       +1     
  Lines       11459    11466       +7     
==========================================
+ Hits         8447     8452       +5     
- Misses       2578     2579       +1     
- Partials      434      435       +1     
```


| [Impacted Files](https://codecov.io/gh/ogen-go/ogen/pull/626?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) | Coverage Δ | |
|---|---|---|
| [http/http.go](https://codecov.io/gh/ogen-go/ogen/pull/626/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aHR0cC9odHRwLmdv) | `58.82% <ø> (ø)` | |
| [http/multipart.go](https://codecov.io/gh/ogen-go/ogen/pull/626/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aHR0cC9tdWx0aXBhcnQuZ28=) | `84.00% <ø> (ø)` | |
| [http/multipart\_file.go](https://codecov.io/gh/ogen-go/ogen/pull/626/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aHR0cC9tdWx0aXBhcnRfZmlsZS5nbw==) | `85.71% <ø> (ø)` | |
| [http/form.go](https://codecov.io/gh/ogen-go/ogen/pull/626/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aHR0cC9mb3JtLmdv) | `100.00% <100.00%> (ø)` | |
| [gen/gen\_contents.go](https://codecov.io/gh/ogen-go/ogen/pull/626/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL2dlbl9jb250ZW50cy5nbw==) | `72.20% <0.00%> (-0.68%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/ogen-go/ogen/pull/626?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index f1068cbd..5d3fa1bc 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -119,7 +119,7 @@ issues:
       text: G114
 
     # We are using quoting algorithm from mime/multipart package. False-positive.
-    - path: http[/\\]file\.go
+    - path: http[/\\]multipart_file\.go
       linters: [ gocritic ]
       text: sprintfQuotedString
 
diff --git a/gen/_template/request_decode.tmpl b/gen/_template/request_decode.tmpl
index 7ed0ea5b..65082fba 100644
--- a/gen/_template/request_decode.tmpl
+++ b/gen/_template/request_decode.tmpl
@@ -101,10 +101,10 @@ func (s *Server) decode{{ $op.Name }}Request(r *http.Request) (
 		}
 
 		{{- if $e.FormURLEncoded }}
-		if err := r.ParseForm(); err != nil {
+		form, err := ht.ParseForm(r)
+		if err != nil {
 			return req, close, errors.Wrap(err, "parse form")
 		}
-		form := r.PostForm
 		{{- else }}
 		if err := r.ParseMultipartForm(s.cfg.MaxMultipartMemory); err != nil {
 			return req, close, errors.Wrap(err, "parse multipart form")
diff --git a/http/client.go b/http/client.go
deleted file mode 100644
index 405148f2..00000000
--- a/http/client.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package http
-
-import (
-	"net/http"
-)
-
-// Client represents http client.
-type Client interface {
-	Do(r *http.Request) (*http.Response, error)
-}
diff --git a/http/form.go b/http/form.go
new file mode 100644
index 00000000..56416760
--- /dev/null
+++ b/http/form.go
@@ -0,0 +1,25 @@
+package http
+
+import (
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+// ParseForm is optimized version of http.Request.ParseForm.
+//
+// Difference from http.Request.ParseForm:
+//   - This function does not modify any fields of http.Request. The only copy of the form values is returned.
+//   - This function does not check Content-Type header.
+func ParseForm(r *http.Request) (url.Values, error) {
+	if f := r.PostForm; f != nil {
+		return f, nil
+	}
+	// TODO(tdakkota): implement streaming parser?
+	var sb strings.Builder
+	if _, err := io.Copy(&sb, r.Body); err != nil {
+		return nil, err
+	}
+	return url.ParseQuery(sb.String())
+}
diff --git a/http/http.go b/http/http.go
index 0e40b063..b296d92e 100644
--- a/http/http.go
+++ b/http/http.go
@@ -11,6 +11,11 @@ import (
 	"strings"
 )
 
+// Client represents http client.
+type Client interface {
+	Do(r *http.Request) (*http.Response, error)
+}
+
 // NewRequest creates a new http.Request.
 func NewRequest(ctx context.Context, method string, u *url.URL, body io.Reader) (*http.Request, error) {
 	return http.NewRequestWithContext(ctx, method, u.String(), body)
diff --git a/http/writer.go b/http/multipart.go
similarity index 100%
rename from http/writer.go
rename to http/multipart.go
diff --git a/http/file.go b/http/multipart_file.go
similarity index 95%
rename from http/file.go
rename to http/multipart_file.go
index 2052b602..be79ac3b 100644
--- a/http/file.go
+++ b/http/multipart_file.go
@@ -6,6 +6,8 @@ import (
 	"mime/multipart"
 	"net/textproto"
 	"strings"
+
+	"golang.org/x/exp/slices"
 )
 
 // MultipartFile is multipart form file.
@@ -26,7 +28,7 @@ func escapeQuotes(s string) string {
 func (m MultipartFile) headers(fieldName string) (h textproto.MIMEHeader) {
 	h = make(textproto.MIMEHeader, len(m.Header)+2)
 	for k, v := range m.Header {
-		h[k] = v
+		h[k] = slices.Clone(v)
 	}
 
 	disposition := fmt.Sprintf(`form-data; name="%s"; filename="%s"`,


Test patch:
diff --git a/http/form_test.go b/http/form_test.go
new file mode 100644
index 00000000..54837041
--- /dev/null
+++ b/http/form_test.go
@@ -0,0 +1,107 @@
+package http
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+	"testing/iotest"
+
+	"github.com/go-faster/errors"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseForm(t *testing.T) {
+	tests := []struct {
+		input   string
+		want    url.Values
+		wantErr bool
+	}{
+		{"", url.Values{}, false},
+		{"a=b", url.Values{"a": {"b"}}, false},
+		{"a=b&c=d", url.Values{"a": {"b"}, "c": {"d"}}, false},
+		{"a=b&a=c&d=e", url.Values{"a": {"b", "c"}, "d": {"e"}}, false},
+		// Invalid form data.
+		{"%", url.Values{}, true},
+	}
+	for i, tt := range tests {
+		tt := tt
+		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
+			a := require.New(t)
+			got, err := ParseForm(&http.Request{
+				Body: io.NopCloser(strings.NewReader(tt.input)),
+			})
+			if tt.wantErr {
+				a.Error(err)
+				return
+			}
+			a.NoError(err)
+			a.Equal(tt.want, got)
+		})
+	}
+	t.Run("Error", func(t *testing.T) {
+		testErr := errors.New("test error")
+		_, err := ParseForm(&http.Request{
+			Body: io.NopCloser(iotest.ErrReader(testErr)),
+		})
+		require.ErrorIs(t, err, testErr)
+	})
+	t.Run("PostFormIsSet", func(t *testing.T) {
+		a := require.New(t)
+		got, err := ParseForm(&http.Request{
+			PostForm: url.Values{"a": {"b"}},
+		})
+		a.NoError(err)
+		a.Equal(url.Values{"a": {"b"}}, got)
+	})
+}
+
+func BenchmarkParseForm(b *testing.B) {
+	bench := func(body string, parse func(r *http.Request) error) func(*testing.B) {
+		return func(b *testing.B) {
+			sr := strings.NewReader(body)
+			r := &http.Request{
+				Method: http.MethodPost,
+				Body:   io.NopCloser(sr),
+				Header: http.Header{},
+			}
+			r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+			b.SetBytes(int64(len(body)))
+			b.ReportAllocs()
+			b.ResetTimer()
+
+			var sink error
+			for i := 0; i < b.N; i++ {
+				r.Form = nil
+				r.PostForm = nil
+				sr.Reset(body)
+
+				sink = parse(r)
+			}
+			if sink != nil {
+				b.Fatal(sink)
+			}
+		}
+	}
+
+	// ~12KB of form data.
+	body := func() string {
+		var sb strings.Builder
+		for i := 0; i < 1024; i++ {
+			if i > 0 {
+				sb.WriteString("&")
+			}
+			_, _ = fmt.Fprintf(&sb, "a%04d=b%04d", i, i)
+		}
+		return sb.String()
+	}()
+
+	b.Run("Custom", bench(body, func(r *http.Request) error {
+		_, err := ParseForm(r)
+		return err
+	}))
+	b.Run("Std", bench(body, (*http.Request).ParseForm))
+}
diff --git a/internal/test_form/oas_request_decoders_gen.go b/internal/test_form/oas_request_decoders_gen.go
index 0ca52698..91710a04 100644
--- a/internal/test_form/oas_request_decoders_gen.go
+++ b/internal/test_form/oas_request_decoders_gen.go
@@ -48,10 +48,10 @@ func (s *Server) decodeTestFormURLEncodedRequest(r *http.Request) (
 		if r.ContentLength == 0 {
 			return req, close, validate.ErrBodyRequired
 		}
-		if err := r.ParseForm(); err != nil {
+		form, err := ht.ParseForm(r)
+		if err != nil {
 			return req, close, errors.Wrap(err, "parse form")
 		}
-		form := r.PostForm
 
 		var request TestForm
 		q := uri.NewQueryDecoder(form)
diff --git a/internal/test_http_requests/oas_request_decoders_gen.go b/internal/test_http_requests/oas_request_decoders_gen.go
index 660f23fa..409ce0e3 100644
--- a/internal/test_http_requests/oas_request_decoders_gen.go
+++ b/internal/test_http_requests/oas_request_decoders_gen.go
@@ -78,10 +78,10 @@ func (s *Server) decodeAllRequestBodiesRequest(r *http.Request) (
 		if r.ContentLength == 0 {
 			return req, close, validate.ErrBodyRequired
 		}
-		if err := r.ParseForm(); err != nil {
+		form, err := ht.ParseForm(r)
+		if err != nil {
 			return req, close, errors.Wrap(err, "parse form")
 		}
-		form := r.PostForm
 
 		var request AllRequestBodiesApplicationXWwwFormUrlencoded
 		{
@@ -301,10 +301,10 @@ func (s *Server) decodeAllRequestBodiesOptionalRequest(r *http.Request) (
 		if r.ContentLength == 0 {
 			return req, close, nil
 		}
-		if err := r.ParseForm(); err != nil {
+		form, err := ht.ParseForm(r)
+		if err != nil {
 			return req, close, errors.Wrap(err, "parse form")
 		}
-		form := r.PostForm
 
 		var request AllRequestBodiesOptionalApplicationXWwwFormUrlencoded
 		{