Skip to content

Commit 38fad5b

Browse files
authored
Optimize CombinePaths (microsoft#251)
1 parent 1228ada commit 38fad5b

File tree

2 files changed

+50
-11
lines changed

2 files changed

+50
-11
lines changed

internal/tspath/path.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,27 +82,44 @@ func HasTrailingDirectorySeparator(path string) bool {
8282
func CombinePaths(firstPath string, paths ...string) string {
8383
// TODO (drosen): There is potential for a fast path here.
8484
// In the case where we find the last absolute path and just path.Join from there.
85-
result := NormalizeSlashes(firstPath)
85+
firstPath = NormalizeSlashes(firstPath)
86+
87+
var b strings.Builder
88+
size := len(firstPath) + len(paths)
89+
for _, p := range paths {
90+
size += len(p)
91+
}
92+
b.Grow(size)
93+
94+
b.WriteString(firstPath)
95+
96+
// To provide a way to "set" the path, keep track of the start and then slice.
97+
// This will waste some memory each time we do it, but saving memory is more common.
98+
start := 0
99+
result := func() string {
100+
return b.String()[start:]
101+
}
102+
setResult := func(value string) {
103+
start = b.Len()
104+
b.WriteString(value)
105+
}
86106

87107
for _, trailingPath := range paths {
88108
if trailingPath == "" {
89109
continue
90110
}
91111
trailingPath = NormalizeSlashes(trailingPath)
92-
if result == "" || GetRootLength(trailingPath) != 0 {
112+
if result() == "" || GetRootLength(trailingPath) != 0 {
93113
// `trailingPath` is absolute.
94-
result = trailingPath
114+
setResult(trailingPath)
95115
} else {
96-
// Could use
97-
// result = path.Join(result, trailingPath)
98-
// but that collapses `..` and prior segments,
99-
// which is not necessarily compatible with how combinePaths
100-
// was originally implemented.
101-
102-
result = EnsureTrailingDirectorySeparator(result) + trailingPath
116+
if !HasTrailingDirectorySeparator(result()) {
117+
b.WriteByte(DirectorySeparator)
118+
}
119+
b.WriteString(trailingPath)
103120
}
104121
}
105-
return result
122+
return result()
106123
}
107124

108125
func GetPathComponents(path string, currentDirectory string) []string {

internal/tspath/path_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,28 @@ func TestCombinePaths(t *testing.T) {
240240
assert.Equal(t, CombinePaths("/a/..", "/b"), "/b")
241241
}
242242

243+
func BenchmarkCombinePaths(b *testing.B) {
244+
tests := [][]string{
245+
{"path", "to", "file.ext"},
246+
{"path", "dir", "..", "to", "file.ext"},
247+
{"/path", "to", "file.ext"},
248+
{"/path", "/to", "file.ext"},
249+
{"c:/path", "to", "file.ext"},
250+
{"file:///path", "to", "file.ext"},
251+
}
252+
253+
for _, test := range tests {
254+
name := shortenName(strings.Join(test, "/"))
255+
b.Run(name, func(b *testing.B) {
256+
first, rest := test[0], test[1:]
257+
b.ReportAllocs()
258+
for range b.N {
259+
CombinePaths(first, rest...)
260+
}
261+
})
262+
}
263+
}
264+
243265
func TestResolvePath(t *testing.T) {
244266
t.Parallel()
245267
assert.Equal(t, ResolvePath(""), "")

0 commit comments

Comments
 (0)