225 lines
7.0 KiB
Go
225 lines
7.0 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package fileserver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/internal/filesystems"
|
|
)
|
|
|
|
// makeBenchDir populates dir with n regular files and a handful of
|
|
// symlinks (both to files and to a subdirectory), mirroring what a
|
|
// real, large, browsable directory tends to contain.
|
|
func makeBenchDir(tb testing.TB, dir string, n int) {
|
|
tb.Helper()
|
|
|
|
subdir := filepath.Join(dir, "subdir")
|
|
if err := os.Mkdir(subdir, 0o755); err != nil {
|
|
tb.Fatal(err)
|
|
}
|
|
|
|
for i := range n {
|
|
name := filepath.Join(dir, fmt.Sprintf("file-%d.txt", i))
|
|
if err := os.WriteFile(name, []byte("x"), 0o600); err != nil {
|
|
tb.Fatal(err)
|
|
}
|
|
}
|
|
|
|
target := filepath.Join(dir, "file-0.txt")
|
|
if err := os.Symlink(target, filepath.Join(dir, "symlink-to-file")); err != nil {
|
|
tb.Skipf("symlink not supported on this platform: %v", err)
|
|
}
|
|
if err := os.Symlink(subdir, filepath.Join(dir, "symlink-to-dir")); err != nil {
|
|
tb.Skipf("symlink not supported on this platform: %v", err)
|
|
}
|
|
}
|
|
|
|
// readBenchDirEntries reads all entries of dir once, up front, so that
|
|
// benchmark iterations measure only the cost of directoryListing itself
|
|
// (allocations, stat calls per entry) rather than the readdir syscall.
|
|
//
|
|
// It deliberately opens the directory and calls the ReadDir method on the
|
|
// resulting file (like loadDirectoryContents does in production), not
|
|
// fs.ReadDir/os.ReadDir - the latter sort entries by filename before
|
|
// returning them, which would silently hand the sort benchmarks
|
|
// already-sorted input and let sort.Sort's adaptive algorithm breeze
|
|
// through with far fewer comparisons than a real, filesystem-ordered
|
|
// directory listing requires.
|
|
func readBenchDirEntries(tb testing.TB, dir string) (fs.FS, []fs.DirEntry) {
|
|
tb.Helper()
|
|
|
|
fileSystem := filesystems.OsFS{}
|
|
f, err := fileSystem.Open(dir)
|
|
if err != nil {
|
|
tb.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
|
|
entries, err := f.(fs.ReadDirFile).ReadDir(-1)
|
|
if err != nil {
|
|
tb.Fatal(err)
|
|
}
|
|
return fileSystem, entries
|
|
}
|
|
|
|
func benchFileServer() *FileServer {
|
|
return &FileServer{
|
|
Browse: &Browse{},
|
|
logger: zap.NewNop(),
|
|
}
|
|
}
|
|
|
|
func benchmarkDirectoryListing(b *testing.B, n int, listFn func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext) {
|
|
dir := b.TempDir()
|
|
makeBenchDir(b, dir, n)
|
|
fileSystem, entries := readBenchDirEntries(b, dir)
|
|
fsrv := benchFileServer()
|
|
ctx := context.Background()
|
|
|
|
b.ReportAllocs()
|
|
|
|
for b.Loop() {
|
|
listFn(fsrv, ctx, fileSystem, entries, dir)
|
|
}
|
|
}
|
|
|
|
func BenchmarkDirectoryListing(b *testing.B) {
|
|
for _, n := range []int{100, 1_000, 10_000, 50_000} {
|
|
b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
|
|
benchmarkDirectoryListing(b, n, func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext {
|
|
return fsrv.directoryListing(ctx, fileSystem, time.Time{}, entries, true, root, "/", caddy.NewReplacer())
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// makeBenchItems builds n synthetic fileInfo items (no disk I/O), mixing
|
|
// upper/lower case names and marking roughly 1 in 10 as directories, to
|
|
// exercise the sort comparators - including bySize's directory name
|
|
// tie-break - the way a real, large, mixed listing would.
|
|
func makeBenchItems(n int) []fileInfo {
|
|
items := make([]fileInfo, n)
|
|
for i := range n {
|
|
name := fmt.Sprintf("file_%d.txt", i)
|
|
if i%2 == 0 {
|
|
name = fmt.Sprintf("FILE_%d.TXT", i)
|
|
}
|
|
items[i] = fileInfo{
|
|
Name: name,
|
|
Size: int64(i),
|
|
IsDir: i%10 == 0,
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func fillNameLower(items []fileInfo) {
|
|
for i := range items {
|
|
items[i].nameLower = strings.ToLower(items[i].Name)
|
|
}
|
|
}
|
|
|
|
// benchmarkSort times sortFn against a fresh, unsorted copy of a synthetic
|
|
// item set on every iteration. The copy, and the optional prepare step
|
|
// (e.g. filling the nameLower cache), happen outside the timed portion so
|
|
// the benchmark isolates the cost of the sort itself.
|
|
func benchmarkSort(b *testing.B, n int, prepare func(items []fileInfo), sortFn func(items []fileInfo)) {
|
|
pristine := makeBenchItems(n)
|
|
items := make([]fileInfo, n)
|
|
|
|
for b.Loop() {
|
|
b.StopTimer()
|
|
copy(items, pristine)
|
|
if prepare != nil {
|
|
prepare(items)
|
|
}
|
|
b.StartTimer()
|
|
sortFn(items)
|
|
}
|
|
}
|
|
|
|
func BenchmarkSortByName(b *testing.B) {
|
|
for _, n := range []int{100, 1_000, 10_000, 50_000} {
|
|
b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
|
|
benchmarkSort(b, n, fillNameLower, func(items []fileInfo) {
|
|
sort.Sort(byName(browseTemplateContext{Items: items}))
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkSortByNameDirFirst(b *testing.B) {
|
|
for _, n := range []int{100, 1_000, 10_000, 50_000} {
|
|
b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
|
|
benchmarkSort(b, n, fillNameLower, func(items []fileInfo) {
|
|
sort.Sort(byNameDirFirst(browseTemplateContext{Items: items}))
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkSortBySize(b *testing.B) {
|
|
for _, n := range []int{100, 1_000, 10_000, 50_000} {
|
|
b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
|
|
benchmarkSort(b, n, fillNameLower, func(items []fileInfo) {
|
|
sort.Sort(bySize(browseTemplateContext{Items: items}))
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Overall benchmark: the whole real-world browse-request path - reading
|
|
// directory entries into a listing and then sorting it.
|
|
// ---------------------------------------------------------------------
|
|
|
|
func benchmarkDirectoryListingAndSort(b *testing.B, n int, run func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext) {
|
|
dir := b.TempDir()
|
|
makeBenchDir(b, dir, n)
|
|
fileSystem, entries := readBenchDirEntries(b, dir)
|
|
fsrv := benchFileServer()
|
|
ctx := context.Background()
|
|
|
|
b.ReportAllocs()
|
|
|
|
for b.Loop() {
|
|
run(fsrv, ctx, fileSystem, entries, dir)
|
|
}
|
|
}
|
|
|
|
func BenchmarkDirectoryListingAndSort(b *testing.B) {
|
|
for _, n := range []int{100, 1_000, 10_000, 50_000} {
|
|
b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
|
|
benchmarkDirectoryListingAndSort(b, n, func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext {
|
|
listing := fsrv.directoryListing(ctx, fileSystem, time.Time{}, entries, true, root, "/", caddy.NewReplacer())
|
|
listing.applySortAndLimit(sortByNameDirFirst, sortOrderAsc, "", "")
|
|
return listing
|
|
})
|
|
})
|
|
}
|
|
}
|