|
33 | 33 | package metadata |
34 | 34 |
|
35 | 35 | import ( |
36 | | - "io" |
| 36 | + "compress/gzip" |
| 37 | + "fmt" |
| 38 | + _ "net/http/pprof" |
37 | 39 | "os" |
38 | 40 | "testing" |
| 41 | + "time" |
39 | 42 |
|
| 43 | + "github.com/awslabs/soci-snapshotter/util/testutil" |
40 | 44 | "github.com/awslabs/soci-snapshotter/ztoc" |
41 | | - bolt "go.etcd.io/bbolt" |
| 45 | + "golang.org/x/sync/errgroup" |
42 | 46 | ) |
43 | 47 |
|
44 | | -func TestMetadataReader(t *testing.T) { |
45 | | - testReader(t, newTestableReader) |
| 48 | +var allowedPrefix = [4]string{"", "./", "/", "../"} |
| 49 | + |
| 50 | +var srcCompressions = map[string]int{ |
| 51 | + "gzip-nocompression": gzip.NoCompression, |
| 52 | + "gzip-bestspeed": gzip.BestSpeed, |
| 53 | + "gzip-bestcompression": gzip.BestCompression, |
| 54 | + "gzip-defaultcompression": gzip.DefaultCompression, |
| 55 | + "gzip-huffmanonly": gzip.HuffmanOnly, |
46 | 56 | } |
47 | 57 |
|
48 | | -func newTestableReader(sr *io.SectionReader, toc ztoc.TOC, opts ...Option) (testableReader, error) { |
49 | | - f, err := os.CreateTemp("", "readertestdb") |
50 | | - if err != nil { |
51 | | - return nil, err |
| 58 | +func TestMetadataReader(t *testing.T) { |
| 59 | + sampleTime := time.Now().Truncate(time.Second) |
| 60 | + tests := []struct { |
| 61 | + name string |
| 62 | + in []testutil.TarEntry |
| 63 | + want []check |
| 64 | + }{ |
| 65 | + { |
| 66 | + name: "files", |
| 67 | + in: []testutil.TarEntry{ |
| 68 | + testutil.File("file1", "foofoo", testutil.WithFileMode(0644|os.ModeSetuid)), |
| 69 | + testutil.Dir("dir1/"), |
| 70 | + testutil.File("dir1/file2.txt", "bazbazbaz", testutil.WithFileOwner(1000, 1000)), |
| 71 | + testutil.File("file3.txt", "xxxxx", testutil.WithFileModTime(sampleTime)), |
| 72 | + testutil.File("file4.txt", "", testutil.WithFileXattrs(map[string]string{"testkey": "testval"})), |
| 73 | + }, |
| 74 | + want: []check{ |
| 75 | + numOfNodes(6), // root dir + 1 dir + 4 files |
| 76 | + hasFile("file1", 6), |
| 77 | + hasMode("file1", 0644|os.ModeSetuid), |
| 78 | + hasFile("dir1/file2.txt", 9), |
| 79 | + hasOwner("dir1/file2.txt", 1000, 1000), |
| 80 | + hasFile("file3.txt", 5), |
| 81 | + hasModTime("file3.txt", sampleTime), |
| 82 | + hasFile("file4.txt", 0), |
| 83 | + // For details on the keys of Xattrs, see https://pkg.go.dev/archive/tar#Header |
| 84 | + hasXattrs("file4.txt", map[string]string{"testkey": "testval"}), |
| 85 | + }, |
| 86 | + }, |
| 87 | + { |
| 88 | + name: "dirs", |
| 89 | + in: []testutil.TarEntry{ |
| 90 | + testutil.Dir("dir1/", testutil.WithDirMode(os.ModeDir|0600|os.ModeSticky)), |
| 91 | + testutil.Dir("dir1/dir2/", testutil.WithDirOwner(1000, 1000)), |
| 92 | + testutil.File("dir1/dir2/file1.txt", "testtest"), |
| 93 | + testutil.File("dir1/dir2/file2", "x"), |
| 94 | + testutil.File("dir1/dir2/file3", "yyy"), |
| 95 | + testutil.Dir("dir1/dir3/", testutil.WithDirModTime(sampleTime)), |
| 96 | + testutil.Dir("dir1/dir3/dir4/", testutil.WithDirXattrs(map[string]string{"testkey": "testval"})), |
| 97 | + testutil.File("dir1/dir3/dir4/file4", "1111111111"), |
| 98 | + }, |
| 99 | + want: []check{ |
| 100 | + numOfNodes(9), // root dir + 4 dirs + 4 files |
| 101 | + hasDirChildren("dir1", "dir2", "dir3"), |
| 102 | + hasDirChildren("dir1/dir2", "file1.txt", "file2", "file3"), |
| 103 | + hasDirChildren("dir1/dir3", "dir4"), |
| 104 | + hasDirChildren("dir1/dir3/dir4", "file4"), |
| 105 | + hasMode("dir1", os.ModeDir|0600|os.ModeSticky), |
| 106 | + hasOwner("dir1/dir2", 1000, 1000), |
| 107 | + hasModTime("dir1/dir3", sampleTime), |
| 108 | + hasXattrs("dir1/dir3/dir4", map[string]string{"testkey": "testval"}), |
| 109 | + hasFile("dir1/dir2/file1.txt", 8), |
| 110 | + hasFile("dir1/dir2/file2", 1), |
| 111 | + hasFile("dir1/dir2/file3", 3), |
| 112 | + hasFile("dir1/dir3/dir4/file4", 10), |
| 113 | + }, |
| 114 | + }, |
| 115 | + { |
| 116 | + name: "hardlinks", |
| 117 | + in: []testutil.TarEntry{ |
| 118 | + testutil.File("file1", "foofoo", testutil.WithFileOwner(1000, 1000)), |
| 119 | + testutil.Dir("dir1/"), |
| 120 | + testutil.Link("dir1/link1", "file1"), |
| 121 | + testutil.Link("dir1/link2", "dir1/link1"), |
| 122 | + testutil.Dir("dir1/dir2/"), |
| 123 | + testutil.File("dir1/dir2/file2.txt", "testtest"), |
| 124 | + testutil.Link("link3", "dir1/dir2/file2.txt"), |
| 125 | + testutil.Symlink("link4", "dir1/link2"), |
| 126 | + }, |
| 127 | + want: []check{ |
| 128 | + numOfNodes(6), // root dir + 2 dirs + 1 file(linked) + 1 file(linked) + 1 symlink |
| 129 | + hasFile("file1", 6), |
| 130 | + hasOwner("file1", 1000, 1000), |
| 131 | + hasFile("dir1/link1", 6), |
| 132 | + hasOwner("dir1/link1", 1000, 1000), |
| 133 | + hasFile("dir1/link2", 6), |
| 134 | + hasOwner("dir1/link2", 1000, 1000), |
| 135 | + hasFile("dir1/dir2/file2.txt", 8), |
| 136 | + hasFile("link3", 8), |
| 137 | + hasDirChildren("dir1", "link1", "link2", "dir2"), |
| 138 | + hasDirChildren("dir1/dir2", "file2.txt"), |
| 139 | + sameNodes("file1", "dir1/link1", "dir1/link2"), |
| 140 | + sameNodes("dir1/dir2/file2.txt", "link3"), |
| 141 | + linkName("link4", "dir1/link2"), |
| 142 | + hasNumLink("file1", 3), // parent dir + 2 links |
| 143 | + hasNumLink("link3", 2), // parent dir + 1 link |
| 144 | + hasNumLink("dir1", 3), // parent + "." + child's ".." |
| 145 | + }, |
| 146 | + }, |
| 147 | + { |
| 148 | + name: "various files", |
| 149 | + in: []testutil.TarEntry{ |
| 150 | + testutil.Dir("dir1/"), |
| 151 | + testutil.File("dir1/../dir1///////////////////file1", ""), |
| 152 | + testutil.Chardev("dir1/cdev", 10, 11), |
| 153 | + testutil.Blockdev("dir1/bdev", 100, 101), |
| 154 | + testutil.Fifo("dir1/fifo"), |
| 155 | + }, |
| 156 | + want: []check{ |
| 157 | + numOfNodes(6), // root dir + 1 file + 1 dir + 1 cdev + 1 bdev + 1 fifo |
| 158 | + hasFile("dir1/file1", 0), |
| 159 | + hasChardev("dir1/cdev", 10, 11), |
| 160 | + hasBlockdev("dir1/bdev", 100, 101), |
| 161 | + hasFifo("dir1/fifo"), |
| 162 | + }, |
| 163 | + }, |
52 | 164 | } |
53 | | - defer os.Remove(f.Name()) |
54 | | - db, err := bolt.Open(f.Name(), 0600, nil) |
55 | | - if err != nil { |
56 | | - return nil, err |
| 165 | + for _, tt := range tests { |
| 166 | + tt := tt |
| 167 | + for _, prefix := range allowedPrefix { |
| 168 | + prefix := prefix |
| 169 | + for srcCompresionName, srcCompression := range srcCompressions { |
| 170 | + srcCompresionName, srcCompression := srcCompresionName, srcCompression |
| 171 | + t.Run(tt.name+"-"+srcCompresionName, func(t *testing.T) { |
| 172 | + opts := []testutil.BuildTarOption{ |
| 173 | + testutil.WithPrefix(prefix), |
| 174 | + } |
| 175 | + |
| 176 | + ztoc, sr, err := ztoc.BuildZtocReader(t, tt.in, srcCompression, 64, opts...) |
| 177 | + if err != nil { |
| 178 | + t.Fatalf("failed to build ztoc: %v", err) |
| 179 | + } |
| 180 | + telemetry, checkCalled := newCalledTelemetry() |
| 181 | + |
| 182 | + // create a metadata reader |
| 183 | + r, err := newTestableReader(sr, ztoc.TOC, WithTelemetry(telemetry)) |
| 184 | + if err != nil { |
| 185 | + t.Fatalf("failed to create new reader: %v", err) |
| 186 | + } |
| 187 | + defer r.Close() |
| 188 | + t.Logf("vvvvv Node tree vvvvv") |
| 189 | + t.Logf("[%d] ROOT", r.RootID()) |
| 190 | + dumpNodes(t, r, r.RootID(), 1) |
| 191 | + t.Logf("^^^^^^^^^^^^^^^^^^^^^") |
| 192 | + for _, want := range tt.want { |
| 193 | + want(t, r) |
| 194 | + } |
| 195 | + if err := checkCalled(); err != nil { |
| 196 | + t.Errorf("telemetry failure: %v", err) |
| 197 | + } |
| 198 | + }) |
| 199 | + } |
| 200 | + } |
57 | 201 | } |
58 | | - r, err := NewReader(db, sr, toc, opts...) |
| 202 | +} |
| 203 | + |
| 204 | +func BenchmarkMetadataReader(b *testing.B) { |
| 205 | + testCases := []struct { |
| 206 | + name string |
| 207 | + entries int |
| 208 | + }{ |
| 209 | + { |
| 210 | + name: "Create metadata.Reader with 1,000 TOC entries", |
| 211 | + entries: 1000, |
| 212 | + }, |
| 213 | + { |
| 214 | + name: "Create metadata.Reader with 10,000 TOC entries", |
| 215 | + entries: 10_000, |
| 216 | + }, |
| 217 | + { |
| 218 | + name: "Create metadata.Reader with 50,000 TOC entries", |
| 219 | + entries: 50_000, |
| 220 | + }, |
| 221 | + { |
| 222 | + name: "Create metadata.Reader with 100,000 TOC entries", |
| 223 | + entries: 100_000, |
| 224 | + }, |
| 225 | + } |
| 226 | + cwdPath, err := os.Getwd() |
59 | 227 | if err != nil { |
60 | | - return nil, err |
| 228 | + b.Fatal(err) |
61 | 229 | } |
62 | | - return &testableReadCloser{ |
63 | | - testableReader: r.(*reader), |
64 | | - closeFn: func() error { |
65 | | - db.Close() |
66 | | - return os.Remove(f.Name()) |
67 | | - }, |
68 | | - }, nil |
69 | | -} |
| 230 | + for _, tc := range testCases { |
| 231 | + toc, err := generateTOC(tc.entries) |
| 232 | + if err != nil { |
| 233 | + b.Fatalf("failed to generate TOC: %v", err) |
| 234 | + } |
| 235 | + b.ResetTimer() |
| 236 | + b.Run(tc.name, func(b *testing.B) { |
| 237 | + for i := 0; i < b.N; i++ { |
| 238 | + b.StopTimer() |
| 239 | + tempDB, clean, err := newTempDB(cwdPath) |
| 240 | + defer func() { |
| 241 | + b.StopTimer() |
| 242 | + clean() |
| 243 | + b.StartTimer() |
| 244 | + }() |
| 245 | + if err != nil { |
| 246 | + b.Fatalf("failed to initialize temp db: %v", err) |
| 247 | + } |
| 248 | + b.StartTimer() |
| 249 | + if _, err := NewReader(tempDB, nil, toc); err != nil { |
| 250 | + b.Fatalf("failed to create new reader: %v", err) |
| 251 | + } |
| 252 | + } |
70 | 253 |
|
71 | | -type testableReadCloser struct { |
72 | | - testableReader |
73 | | - closeFn func() error |
| 254 | + }) |
| 255 | + } |
74 | 256 | } |
75 | 257 |
|
76 | | -func (r *testableReadCloser) Close() error { |
77 | | - r.closeFn() |
78 | | - return r.testableReader.Close() |
| 258 | +func BenchmarkConcurrentMetadataReader(b *testing.B) { |
| 259 | + smallTOC, err := generateTOC(1000) |
| 260 | + if err != nil { |
| 261 | + b.Fatalf("failed to generate TOC: %v", err) |
| 262 | + } |
| 263 | + mediumTOC, err := generateTOC(10_000) |
| 264 | + if err != nil { |
| 265 | + b.Fatalf("failed to generate TOC: %v", err) |
| 266 | + } |
| 267 | + largeTOC, err := generateTOC(50_000) |
| 268 | + if err != nil { |
| 269 | + b.Fatalf("failed to generate TOC: %v", err) |
| 270 | + } |
| 271 | + cwdPath, err := os.Getwd() |
| 272 | + if err != nil { |
| 273 | + b.Fatal(err) |
| 274 | + } |
| 275 | + tocs := []ztoc.TOC{smallTOC, mediumTOC, largeTOC} |
| 276 | + var eg errgroup.Group |
| 277 | + b.ResetTimer() |
| 278 | + b.Run("Write small, medium and large TOC concurrently", func(b *testing.B) { |
| 279 | + for i := 0; i < b.N; i++ { |
| 280 | + b.StopTimer() |
| 281 | + tempDB, clean, err := newTempDB(cwdPath) |
| 282 | + defer func() { |
| 283 | + b.StopTimer() |
| 284 | + clean() |
| 285 | + b.StartTimer() |
| 286 | + }() |
| 287 | + if err != nil { |
| 288 | + b.Fatalf("failed to initialize temp db: %v", err) |
| 289 | + } |
| 290 | + b.StartTimer() |
| 291 | + for _, toc := range tocs { |
| 292 | + toc := toc |
| 293 | + eg.Go(func() error { |
| 294 | + if _, err := NewReader(tempDB, nil, toc); err != nil { |
| 295 | + return fmt.Errorf("failed to create new reader: %v", err) |
| 296 | + } |
| 297 | + return nil |
| 298 | + }) |
| 299 | + } |
| 300 | + if err := eg.Wait(); err != nil { |
| 301 | + b.Fatal(err) |
| 302 | + } |
| 303 | + } |
| 304 | + }) |
79 | 305 | } |
0 commit comments