Skip to content

Commit bddaeb0

Browse files
authored
Convert read file content directly to strings (#1180)
1 parent 06b2c40 commit bddaeb0

File tree

3 files changed

+95
-13
lines changed

3 files changed

+95
-13
lines changed

internal/repo/paths.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ var typeScriptSubmoduleExists = sync.OnceValue(func() bool {
4646
return true
4747
})
4848

49+
func TypeScriptSubmoduleExists() bool {
50+
return typeScriptSubmoduleExists()
51+
}
52+
4953
type skippable interface {
5054
Helper()
5155
Skipf(format string, args ...any)

internal/vfs/internal/internal.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package internal
22

33
import (
4-
"bytes"
54
"encoding/binary"
65
"fmt"
76
"io/fs"
7+
"strings"
88
"unicode/utf16"
9+
"unsafe"
910

1011
"github.com/microsoft/typescript-go/internal/tspath"
1112
"github.com/microsoft/typescript-go/internal/vfs"
@@ -146,30 +147,42 @@ func (vfs *Common) ReadFile(path string) (contents string, ok bool) {
146147
return "", false
147148
}
148149

149-
return decodeBytes(b)
150+
// An invariant of any underlying filesystem is that the bytes returned
151+
// are immutable, otherwise anyone using the filesystem would end up
152+
// with data races.
153+
//
154+
// This means that we can safely convert the bytes to a string directly,
155+
// saving a copy.
156+
if len(b) == 0 {
157+
return "", true
158+
}
159+
160+
s := unsafe.String(&b[0], len(b))
161+
162+
return decodeBytes(s)
150163
}
151164

152-
func decodeBytes(b []byte) (contents string, ok bool) {
165+
func decodeBytes(s string) (contents string, ok bool) {
153166
var bom [2]byte
154-
if len(b) >= 2 {
155-
bom = [2]byte{b[0], b[1]}
167+
if len(s) >= 2 {
168+
bom = [2]byte{s[0], s[1]}
156169
switch bom {
157170
case [2]byte{0xFF, 0xFE}:
158-
return decodeUtf16(b[2:], binary.LittleEndian), true
171+
return decodeUtf16(s[2:], binary.LittleEndian), true
159172
case [2]byte{0xFE, 0xFF}:
160-
return decodeUtf16(b[2:], binary.BigEndian), true
173+
return decodeUtf16(s[2:], binary.BigEndian), true
161174
}
162175
}
163-
if len(b) >= 3 && b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF {
164-
b = b[3:]
176+
if len(s) >= 3 && s[0] == 0xEF && s[1] == 0xBB && s[2] == 0xBF {
177+
s = s[3:]
165178
}
166179

167-
return string(b), true
180+
return s, true
168181
}
169182

170-
func decodeUtf16(b []byte, order binary.ByteOrder) string {
171-
ints := make([]uint16, len(b)/2)
172-
if err := binary.Read(bytes.NewReader(b), order, &ints); err != nil {
183+
func decodeUtf16(s string, order binary.ByteOrder) string {
184+
ints := make([]uint16, len(s)/2)
185+
if err := binary.Read(strings.NewReader(s), order, &ints); err != nil {
173186
return ""
174187
}
175188
return string(utf16.Decode(ints))

internal/vfs/vfs_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package vfs_test
2+
3+
import (
4+
"testing"
5+
"testing/fstest"
6+
7+
"github.com/microsoft/typescript-go/internal/repo"
8+
"github.com/microsoft/typescript-go/internal/tspath"
9+
"github.com/microsoft/typescript-go/internal/vfs"
10+
"github.com/microsoft/typescript-go/internal/vfs/osvfs"
11+
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
12+
"gotest.tools/v3/assert"
13+
)
14+
15+
func BenchmarkReadFile(b *testing.B) {
16+
type bench struct {
17+
name string
18+
fs vfs.FS
19+
path string
20+
}
21+
22+
osFS := osvfs.FS()
23+
24+
const smallData = "hello, world"
25+
tmpdir := tspath.NormalizeSlashes(b.TempDir())
26+
osSmallDataPath := tspath.CombinePaths(tmpdir, "foo.ts")
27+
err := osFS.WriteFile(osSmallDataPath, smallData, false)
28+
assert.NilError(b, err)
29+
30+
tests := []bench{
31+
{"MapFS small", vfstest.FromMap(fstest.MapFS{
32+
"/foo.ts": &fstest.MapFile{
33+
Data: []byte(smallData),
34+
},
35+
}, true), "/foo.ts"},
36+
{"OS small", osFS, osSmallDataPath},
37+
}
38+
39+
if repo.TypeScriptSubmoduleExists() {
40+
checkerPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler", "checker.ts")
41+
42+
checkerContents, ok := osFS.ReadFile(checkerPath)
43+
assert.Assert(b, ok)
44+
45+
tests = append(tests, bench{
46+
"MapFS checker.ts",
47+
vfstest.FromMap(fstest.MapFS{
48+
"/checker.ts": &fstest.MapFile{
49+
Data: []byte(checkerContents),
50+
},
51+
}, true),
52+
"/checker.ts",
53+
})
54+
tests = append(tests, bench{"OS checker.ts", osFS, checkerPath})
55+
}
56+
57+
for _, tt := range tests {
58+
b.Run(tt.name, func(b *testing.B) {
59+
b.ReportAllocs()
60+
for range b.N {
61+
_, _ = tt.fs.ReadFile(tt.path)
62+
}
63+
})
64+
}
65+
}

0 commit comments

Comments
 (0)