@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 224 lines 6.96 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package fileserver
16
17 import (
18 "context"
19 "fmt"
20 "io/fs"
21 "os"
22 "path/filepath"
23 "sort"
24 "strings"
25 "testing"
26 "time"
27
28 "go.uber.org/zap"
29
30 "github.com/caddyserver/caddy/v2"
31 "github.com/caddyserver/caddy/v2/internal/filesystems"
32 )
33
34 // makeBenchDir populates dir with n regular files and a handful of
35 // symlinks (both to files and to a subdirectory), mirroring what a
36 // real, large, browsable directory tends to contain.
37 func makeBenchDir(tb testing.TB, dir string, n int) {
38 tb.Helper()
39
40 subdir := filepath.Join(dir, "subdir")
41 if err := os.Mkdir(subdir, 0o755); err != nil {
42 tb.Fatal(err)
43 }
44
45 for i := range n {
46 name := filepath.Join(dir, fmt.Sprintf("file-%d.txt", i))
47 if err := os.WriteFile(name, []byte("x"), 0o600); err != nil {
48 tb.Fatal(err)
49 }
50 }
51
52 target := filepath.Join(dir, "file-0.txt")
53 if err := os.Symlink(target, filepath.Join(dir, "symlink-to-file")); err != nil {
54 tb.Skipf("symlink not supported on this platform: %v", err)
55 }
56 if err := os.Symlink(subdir, filepath.Join(dir, "symlink-to-dir")); err != nil {
57 tb.Skipf("symlink not supported on this platform: %v", err)
58 }
59 }
60
61 // readBenchDirEntries reads all entries of dir once, up front, so that
62 // benchmark iterations measure only the cost of directoryListing itself
63 // (allocations, stat calls per entry) rather than the readdir syscall.
64 //
65 // It deliberately opens the directory and calls the ReadDir method on the
66 // resulting file (like loadDirectoryContents does in production), not
67 // fs.ReadDir/os.ReadDir - the latter sort entries by filename before
68 // returning them, which would silently hand the sort benchmarks
69 // already-sorted input and let sort.Sort's adaptive algorithm breeze
70 // through with far fewer comparisons than a real, filesystem-ordered
71 // directory listing requires.
72 func readBenchDirEntries(tb testing.TB, dir string) (fs.FS, []fs.DirEntry) {
73 tb.Helper()
74
75 fileSystem := filesystems.OsFS{}
76 f, err := fileSystem.Open(dir)
77 if err != nil {
78 tb.Fatal(err)
79 }
80 defer f.Close()
81
82 entries, err := f.(fs.ReadDirFile).ReadDir(-1)
83 if err != nil {
84 tb.Fatal(err)
85 }
86 return fileSystem, entries
87 }
88
89 func benchFileServer() *FileServer {
90 return &FileServer{
91 Browse: &Browse{},
92 logger: zap.NewNop(),
93 }
94 }
95
96 func benchmarkDirectoryListing(b *testing.B, n int, listFn func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext) {
97 dir := b.TempDir()
98 makeBenchDir(b, dir, n)
99 fileSystem, entries := readBenchDirEntries(b, dir)
100 fsrv := benchFileServer()
101 ctx := context.Background()
102
103 b.ReportAllocs()
104
105 for b.Loop() {
106 listFn(fsrv, ctx, fileSystem, entries, dir)
107 }
108 }
109
110 func BenchmarkDirectoryListing(b *testing.B) {
111 for _, n := range []int{100, 1_000, 10_000, 50_000} {
112 b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
113 benchmarkDirectoryListing(b, n, func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext {
114 return fsrv.directoryListing(ctx, fileSystem, time.Time{}, entries, true, root, "/", caddy.NewReplacer())
115 })
116 })
117 }
118 }
119
120 // makeBenchItems builds n synthetic fileInfo items (no disk I/O), mixing
121 // upper/lower case names and marking roughly 1 in 10 as directories, to
122 // exercise the sort comparators - including bySize's directory name
123 // tie-break - the way a real, large, mixed listing would.
124 func makeBenchItems(n int) []fileInfo {
125 items := make([]fileInfo, n)
126 for i := range n {
127 name := fmt.Sprintf("file_%d.txt", i)
128 if i%2 == 0 {
129 name = fmt.Sprintf("FILE_%d.TXT", i)
130 }
131 items[i] = fileInfo{
132 Name: name,
133 Size: int64(i),
134 IsDir: i%10 == 0,
135 }
136 }
137 return items
138 }
139
140 func fillNameLower(items []fileInfo) {
141 for i := range items {
142 items[i].nameLower = strings.ToLower(items[i].Name)
143 }
144 }
145
146 // benchmarkSort times sortFn against a fresh, unsorted copy of a synthetic
147 // item set on every iteration. The copy, and the optional prepare step
148 // (e.g. filling the nameLower cache), happen outside the timed portion so
149 // the benchmark isolates the cost of the sort itself.
150 func benchmarkSort(b *testing.B, n int, prepare func(items []fileInfo), sortFn func(items []fileInfo)) {
151 pristine := makeBenchItems(n)
152 items := make([]fileInfo, n)
153
154 for b.Loop() {
155 b.StopTimer()
156 copy(items, pristine)
157 if prepare != nil {
158 prepare(items)
159 }
160 b.StartTimer()
161 sortFn(items)
162 }
163 }
164
165 func BenchmarkSortByName(b *testing.B) {
166 for _, n := range []int{100, 1_000, 10_000, 50_000} {
167 b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
168 benchmarkSort(b, n, fillNameLower, func(items []fileInfo) {
169 sort.Sort(byName(browseTemplateContext{Items: items}))
170 })
171 })
172 }
173 }
174
175 func BenchmarkSortByNameDirFirst(b *testing.B) {
176 for _, n := range []int{100, 1_000, 10_000, 50_000} {
177 b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
178 benchmarkSort(b, n, fillNameLower, func(items []fileInfo) {
179 sort.Sort(byNameDirFirst(browseTemplateContext{Items: items}))
180 })
181 })
182 }
183 }
184
185 func BenchmarkSortBySize(b *testing.B) {
186 for _, n := range []int{100, 1_000, 10_000, 50_000} {
187 b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
188 benchmarkSort(b, n, fillNameLower, func(items []fileInfo) {
189 sort.Sort(bySize(browseTemplateContext{Items: items}))
190 })
191 })
192 }
193 }
194
195 // ---------------------------------------------------------------------
196 // Overall benchmark: the whole real-world browse-request path - reading
197 // directory entries into a listing and then sorting it.
198 // ---------------------------------------------------------------------
199
200 func benchmarkDirectoryListingAndSort(b *testing.B, n int, run func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext) {
201 dir := b.TempDir()
202 makeBenchDir(b, dir, n)
203 fileSystem, entries := readBenchDirEntries(b, dir)
204 fsrv := benchFileServer()
205 ctx := context.Background()
206
207 b.ReportAllocs()
208
209 for b.Loop() {
210 run(fsrv, ctx, fileSystem, entries, dir)
211 }
212 }
213
214 func BenchmarkDirectoryListingAndSort(b *testing.B) {
215 for _, n := range []int{100, 1_000, 10_000, 50_000} {
216 b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
217 benchmarkDirectoryListingAndSort(b, n, func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext {
218 listing := fsrv.directoryListing(ctx, fileSystem, time.Time{}, entries, true, root, "/", caddy.NewReplacer())
219 listing.applySortAndLimit(sortByNameDirFirst, sortOrderAsc, "", "")
220 return listing
221 })
222 })
223 }
224 }