Skip to content

Commit f1a8d99

Browse files
mauri870rdner
andauthored
Add optimized ReadAll function (#229)
* Add optimized ReadAll function This adds an optimized io.ReadAll that uses a bytes.Buffer + io.Copy internally, improving the buffer growth ratio over the runtime append and taking advange of io.WriterTo when available. The only scenario where this optimizes version is slower than io.ReadAll is when reading < 512 bytes of a reader that does not implement io.WriterTo. For now I only updated tests to use the new function as updating the code requires more careful consideration. While at it, migrate from the deprecated ioutil.ReadAll to io.ReadAll. goos: linux goarch: amd64 pkg: github.com/elastic/elastic-agent-libs/iobuf cpu: Intel(R) Xeon(R) CPU E5-2697A v4 @ 2.60GHz BenchmarkReadAll/size_32B/io.ReadAll/WriterTo-32 7479658 162.4 ns/op 512 B/op 1 allocs/op BenchmarkReadAll/size_32B/io.ReadAll/Reader-32 7000012 169.3 ns/op 512 B/op 1 allocs/op BenchmarkReadAll/size_32B/ReadAll/WriterTo-32 10630838 112.3 ns/op 112 B/op 2 allocs/op BenchmarkReadAll/size_32B/ReadAll/Reader-32 2111246 567.3 ns/op 1584 B/op 3 allocs/op BenchmarkReadAll/size_64B/io.ReadAll/WriterTo-32 7154652 163.2 ns/op 512 B/op 1 allocs/op BenchmarkReadAll/size_64B/io.ReadAll/Reader-32 7223264 166.1 ns/op 512 B/op 1 allocs/op BenchmarkReadAll/size_64B/ReadAll/WriterTo-32 10400562 113.5 ns/op 112 B/op 2 allocs/op BenchmarkReadAll/size_64B/ReadAll/Reader-32 2129949 558.7 ns/op 1584 B/op 3 allocs/op BenchmarkReadAll/size_512B/io.ReadAll/WriterTo-32 2843871 419.1 ns/op 1408 B/op 2 allocs/op BenchmarkReadAll/size_512B/io.ReadAll/Reader-32 2871580 413.1 ns/op 1408 B/op 2 allocs/op BenchmarkReadAll/size_512B/ReadAll/WriterTo-32 4976233 241.5 ns/op 560 B/op 2 allocs/op BenchmarkReadAll/size_512B/ReadAll/Reader-32 2183186 552.9 ns/op 1584 B/op 3 allocs/op BenchmarkReadAll/size_10KB/io.ReadAll/WriterTo-32 142633 8235 ns/op 46080 B/op 10 allocs/op BenchmarkReadAll/size_10KB/io.ReadAll/Reader-32 148326 8229 ns/op 46080 B/op 10 allocs/op BenchmarkReadAll/size_10KB/ReadAll/WriterTo-32 574903 2210 ns/op 10288 B/op 2 allocs/op BenchmarkReadAll/size_10KB/ReadAll/Reader-32 147210 7995 ns/op 32304 B/op 7 allocs/op BenchmarkReadAll/size_100KB/io.ReadAll/WriterTo-32 13171 90853 ns/op 514304 B/op 18 allocs/op BenchmarkReadAll/size_100KB/io.ReadAll/Reader-32 12892 91787 ns/op 514304 B/op 18 allocs/op BenchmarkReadAll/size_100KB/ReadAll/WriterTo-32 51472 22420 ns/op 106544 B/op 2 allocs/op BenchmarkReadAll/size_100KB/ReadAll/Reader-32 21568 55070 ns/op 261680 B/op 10 allocs/op BenchmarkReadAll/size_1MB/io.ReadAll/WriterTo-32 1220 983276 ns/op 5241098 B/op 27 allocs/op BenchmarkReadAll/size_1MB/io.ReadAll/Reader-32 1089 990818 ns/op 5241100 B/op 27 allocs/op BenchmarkReadAll/size_1MB/ReadAll/WriterTo-32 4153 294507 ns/op 1048627 B/op 2 allocs/op BenchmarkReadAll/size_1MB/ReadAll/Reader-32 1195 944781 ns/op 4193852 B/op 14 allocs/op * check error returned by Seek * update ReadAll doc * fix linter error in doc * nolint for http.Serve * fix license headers * fix code duplication * use t.TempDir instead of os.MkdirTemp * remove os.CreateTemp --------- Co-authored-by: Denis <[email protected]>
1 parent eb7e16b commit f1a8d99

File tree

16 files changed

+232
-117
lines changed

16 files changed

+232
-117
lines changed

api/npipe/listener_windows_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ package npipe
2121

2222
import (
2323
"fmt"
24-
"io/ioutil"
2524
"net/http"
2625
"testing"
2726

27+
"github.com/elastic/elastic-agent-libs/transport/httpcommon"
2828
"github.com/stretchr/testify/assert"
2929
"github.com/stretchr/testify/require"
3030
)
@@ -43,7 +43,8 @@ func TestHTTPOverNamedPipe(t *testing.T) {
4343
})
4444

4545
go func() {
46-
_ = http.Serve(l, mux)
46+
_ = http.Serve(l, mux) //nolint:gosec // Serve does not support setting timeouts, it is fine for tests.
47+
4748
}()
4849

4950
c := http.Client{
@@ -52,10 +53,10 @@ func TestHTTPOverNamedPipe(t *testing.T) {
5253
},
5354
}
5455

55-
// nolint:noctx // for testing purposes
56+
//nolint:noctx // for testing purposes
5657
r, err := c.Get("http://npipe/echo-hello")
5758
require.NoError(t, err)
58-
body, err := ioutil.ReadAll(r.Body)
59+
body, err := httpcommon.ReadAll(r)
5960
require.NoError(t, err)
6061
defer r.Body.Close()
6162

api/server_test.go

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ package api
2020
import (
2121
"context"
2222
"fmt"
23-
"io"
24-
"io/ioutil"
2523
"net"
2624
"net/http"
2725
"os"
@@ -32,6 +30,7 @@ import (
3230
"github.com/stretchr/testify/require"
3331

3432
"github.com/elastic/elastic-agent-libs/config"
33+
"github.com/elastic/elastic-agent-libs/transport/httpcommon"
3534
)
3635

3736
const (
@@ -71,10 +70,7 @@ func TestSocket(t *testing.T) {
7170
}
7271

7372
t.Run("socket doesn't exist before", func(t *testing.T) {
74-
tmpDir, err := ioutil.TempDir("", "testsocket")
75-
require.NoError(t, err)
76-
defer os.RemoveAll(tmpDir)
77-
73+
tmpDir := t.TempDir()
7874
sockFile := tmpDir + "/test.sock"
7975

8076
cfg := config.MustNewConfigFrom(map[string]interface{}{
@@ -102,11 +98,7 @@ func TestSocket(t *testing.T) {
10298
})
10399

104100
t.Run("starting beat and recover a dangling socket file", func(t *testing.T) {
105-
tmpDir, err := ioutil.TempDir("", "testsocket")
106-
require.NoError(t, err)
107-
defer os.RemoveAll(tmpDir)
108-
109-
sockFile := tmpDir + "/test.sock"
101+
sockFile := t.TempDir() + "/test.sock"
110102

111103
// Create the socket before the server.
112104
f, err := os.Create(sockFile)
@@ -161,7 +153,7 @@ func getResponse(t *testing.T, sockFile, url string) string {
161153
require.NoError(t, err)
162154
defer r.Body.Close()
163155

164-
body, err := ioutil.ReadAll(r.Body)
156+
body, err := httpcommon.ReadAll(r)
165157
require.NoError(t, err)
166158
return string(body)
167159
}
@@ -188,7 +180,7 @@ func TestHTTP(t *testing.T) {
188180
require.NoError(t, err)
189181
}()
190182

191-
body, err := ioutil.ReadAll(r.Body)
183+
body, err := httpcommon.ReadAll(r)
192184
require.NoError(t, err)
193185

194186
assert.Equal(t, "ehlo!", string(body))
@@ -229,7 +221,7 @@ func TestAttachHandler(t *testing.T) {
229221
require.NoError(t, err)
230222
}()
231223

232-
body, err := io.ReadAll(r.Body)
224+
body, err := httpcommon.ReadAll(r)
233225
require.NoError(t, err)
234226

235227
assert.Equal(t, "test!", string(body))

api/server_windows_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
package api
2121

2222
import (
23-
"io/ioutil"
2423
"net/http"
2524
"testing"
2625

@@ -29,6 +28,7 @@ import (
2928

3029
"github.com/elastic/elastic-agent-libs/api/npipe"
3130
"github.com/elastic/elastic-agent-libs/config"
31+
"github.com/elastic/elastic-agent-libs/transport/httpcommon"
3232
)
3333

3434
func TestNamedPipe(t *testing.T) {
@@ -52,12 +52,12 @@ func TestNamedPipe(t *testing.T) {
5252
},
5353
}
5454

55-
// nolint:noctx // for testing purposes
55+
//nolint:noctx // for testing purposes
5656
r, err := c.Get("http://npipe/echo-hello")
5757
require.NoError(t, err)
5858
defer r.Body.Close()
5959

60-
body, err := ioutil.ReadAll(r.Body)
60+
body, err := httpcommon.ReadAll(r)
6161
require.NoError(t, err)
6262

6363
assert.Equal(t, "ehlo!", string(body))

file/fileinfo_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
package file_test
2525

2626
import (
27-
"io/ioutil"
2827
"os"
2928
"path/filepath"
3029
"testing"
@@ -35,13 +34,14 @@ import (
3534
)
3635

3736
func TestStat(t *testing.T) {
38-
f, err := ioutil.TempFile("", "teststat")
37+
tmpDir := t.TempDir()
38+
f, err := os.Create(filepath.Join(tmpDir, "teststat"))
3939
if err != nil {
4040
t.Fatal(err)
4141
}
42-
defer os.Remove(f.Name())
42+
defer f.Close()
4343

44-
link := filepath.Join(os.TempDir(), "teststat-link")
44+
link := filepath.Join(tmpDir, "teststat-link")
4545
if err := os.Symlink(f.Name(), link); err != nil {
4646
t.Fatal(err)
4747
}

file/helper_test.go

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
package file
2121

2222
import (
23-
"io/ioutil"
23+
"fmt"
2424
"os"
2525
"path/filepath"
2626
"testing"
@@ -29,19 +29,15 @@ import (
2929
)
3030

3131
func TestSafeFileRotateExistingFile(t *testing.T) {
32-
tempdir, err := ioutil.TempDir("", "")
33-
assert.NoError(t, err)
34-
defer func() {
35-
assert.NoError(t, os.RemoveAll(tempdir))
36-
}()
32+
tempdir := t.TempDir()
3733

3834
// create an existing registry file
39-
err = ioutil.WriteFile(filepath.Join(tempdir, "registry"),
35+
err := os.WriteFile(filepath.Join(tempdir, "registry"),
4036
[]byte("existing filebeat"), 0x777)
4137
assert.NoError(t, err)
4238

4339
// create a new registry.new file
44-
err = ioutil.WriteFile(filepath.Join(tempdir, "registry.new"),
40+
err = os.WriteFile(filepath.Join(tempdir, "registry.new"),
4541
[]byte("new filebeat"), 0x777)
4642
assert.NoError(t, err)
4743

@@ -50,35 +46,23 @@ func TestSafeFileRotateExistingFile(t *testing.T) {
5046
filepath.Join(tempdir, "registry.new"))
5147
assert.NoError(t, err)
5248

53-
contents, err := ioutil.ReadFile(filepath.Join(tempdir, "registry"))
49+
contents, err := os.ReadFile(filepath.Join(tempdir, "registry"))
5450
assert.NoError(t, err)
5551
assert.Equal(t, []byte("new filebeat"), contents)
5652

57-
// do it again to make sure we deal with deleting the old file
58-
59-
err = ioutil.WriteFile(filepath.Join(tempdir, "registry.new"),
60-
[]byte("new filebeat 1"), 0x777)
61-
assert.NoError(t, err)
62-
63-
err = SafeFileRotate(filepath.Join(tempdir, "registry"),
64-
filepath.Join(tempdir, "registry.new"))
65-
assert.NoError(t, err)
66-
67-
contents, err = ioutil.ReadFile(filepath.Join(tempdir, "registry"))
68-
assert.NoError(t, err)
69-
assert.Equal(t, []byte("new filebeat 1"), contents)
70-
71-
// and again for good measure
72-
73-
err = ioutil.WriteFile(filepath.Join(tempdir, "registry.new"),
74-
[]byte("new filebeat 2"), 0x777)
75-
assert.NoError(t, err)
76-
77-
err = SafeFileRotate(filepath.Join(tempdir, "registry"),
78-
filepath.Join(tempdir, "registry.new"))
79-
assert.NoError(t, err)
80-
81-
contents, err = ioutil.ReadFile(filepath.Join(tempdir, "registry"))
82-
assert.NoError(t, err)
83-
assert.Equal(t, []byte("new filebeat 2"), contents)
53+
// do it twice to make sure we deal with deleting the old file
54+
for i := 0; i < 2; i++ {
55+
expectedContents := []byte(fmt.Sprintf("new filebeat %d", i))
56+
err = os.WriteFile(filepath.Join(tempdir, "registry.new"),
57+
expectedContents, 0x777)
58+
assert.NoError(t, err)
59+
60+
err = SafeFileRotate(filepath.Join(tempdir, "registry"),
61+
filepath.Join(tempdir, "registry.new"))
62+
assert.NoError(t, err)
63+
64+
contents, err = os.ReadFile(filepath.Join(tempdir, "registry"))
65+
assert.NoError(t, err)
66+
assert.Equal(t, expectedContents, contents)
67+
}
8468
}

iobuf/iobuf.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package iobuf
19+
20+
import (
21+
"bytes"
22+
"io"
23+
)
24+
25+
// ReadAll reads all data from r and returns it as a byte slice.
26+
// A successful call returns err == nil, not err == EOF. It does not
27+
// treat an EOF as an error to be reported.
28+
//
29+
// This function is similar to io.ReadAll, but uses a bytes.Buffer to
30+
// accumulate the data, which has a more efficient growing algorithm and
31+
// uses io.WriterTo if r implements it.
32+
func ReadAll(r io.Reader) ([]byte, error) {
33+
var buf bytes.Buffer
34+
_, err := io.Copy(&buf, r)
35+
return buf.Bytes(), err
36+
}

0 commit comments

Comments
 (0)