diff --git a/internal/repo/paths.go b/internal/repo/paths.go index 7dc7947475..237a8d0467 100644 --- a/internal/repo/paths.go +++ b/internal/repo/paths.go @@ -46,6 +46,10 @@ var typeScriptSubmoduleExists = sync.OnceValue(func() bool { return true }) +func TypeScriptSubmoduleExists() bool { + return typeScriptSubmoduleExists() +} + type skippable interface { Helper() Skipf(format string, args ...any) diff --git a/internal/vfs/internal/internal.go b/internal/vfs/internal/internal.go index d6e638954c..c761e20388 100644 --- a/internal/vfs/internal/internal.go +++ b/internal/vfs/internal/internal.go @@ -1,11 +1,12 @@ package internal import ( - "bytes" "encoding/binary" "fmt" "io/fs" + "strings" "unicode/utf16" + "unsafe" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" @@ -146,30 +147,42 @@ func (vfs *Common) ReadFile(path string) (contents string, ok bool) { return "", false } - return decodeBytes(b) + // An invariant of any underlying filesystem is that the bytes returned + // are immutable, otherwise anyone using the filesystem would end up + // with data races. + // + // This means that we can safely convert the bytes to a string directly, + // saving a copy. + if len(b) == 0 { + return "", true + } + + s := unsafe.String(&b[0], len(b)) + + return decodeBytes(s) } -func decodeBytes(b []byte) (contents string, ok bool) { +func decodeBytes(s string) (contents string, ok bool) { var bom [2]byte - if len(b) >= 2 { - bom = [2]byte{b[0], b[1]} + if len(s) >= 2 { + bom = [2]byte{s[0], s[1]} switch bom { case [2]byte{0xFF, 0xFE}: - return decodeUtf16(b[2:], binary.LittleEndian), true + return decodeUtf16(s[2:], binary.LittleEndian), true case [2]byte{0xFE, 0xFF}: - return decodeUtf16(b[2:], binary.BigEndian), true + return decodeUtf16(s[2:], binary.BigEndian), true } } - if len(b) >= 3 && b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF { - b = b[3:] + if len(s) >= 3 && s[0] == 0xEF && s[1] == 0xBB && s[2] == 0xBF { + s = s[3:] } - return string(b), true + return s, true } -func decodeUtf16(b []byte, order binary.ByteOrder) string { - ints := make([]uint16, len(b)/2) - if err := binary.Read(bytes.NewReader(b), order, &ints); err != nil { +func decodeUtf16(s string, order binary.ByteOrder) string { + ints := make([]uint16, len(s)/2) + if err := binary.Read(strings.NewReader(s), order, &ints); err != nil { return "" } return string(utf16.Decode(ints)) diff --git a/internal/vfs/vfs_test.go b/internal/vfs/vfs_test.go new file mode 100644 index 0000000000..81de1b5178 --- /dev/null +++ b/internal/vfs/vfs_test.go @@ -0,0 +1,65 @@ +package vfs_test + +import ( + "testing" + "testing/fstest" + + "github.com/microsoft/typescript-go/internal/repo" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/vfstest" + "gotest.tools/v3/assert" +) + +func BenchmarkReadFile(b *testing.B) { + type bench struct { + name string + fs vfs.FS + path string + } + + osFS := osvfs.FS() + + const smallData = "hello, world" + tmpdir := tspath.NormalizeSlashes(b.TempDir()) + osSmallDataPath := tspath.CombinePaths(tmpdir, "foo.ts") + err := osFS.WriteFile(osSmallDataPath, smallData, false) + assert.NilError(b, err) + + tests := []bench{ + {"MapFS small", vfstest.FromMap(fstest.MapFS{ + "/foo.ts": &fstest.MapFile{ + Data: []byte(smallData), + }, + }, true), "/foo.ts"}, + {"OS small", osFS, osSmallDataPath}, + } + + if repo.TypeScriptSubmoduleExists() { + checkerPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler", "checker.ts") + + checkerContents, ok := osFS.ReadFile(checkerPath) + assert.Assert(b, ok) + + tests = append(tests, bench{ + "MapFS checker.ts", + vfstest.FromMap(fstest.MapFS{ + "/checker.ts": &fstest.MapFile{ + Data: []byte(checkerContents), + }, + }, true), + "/checker.ts", + }) + tests = append(tests, bench{"OS checker.ts", osFS, checkerPath}) + } + + for _, tt := range tests { + b.Run(tt.name, func(b *testing.B) { + b.ReportAllocs() + for range b.N { + _, _ = tt.fs.ReadFile(tt.path) + } + }) + } +}