site_benchmark_new_test.go (14563B)
1 // Copyright 2019 The Hugo Authors. All rights reserved. 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 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package hugolib 15 16 import ( 17 "fmt" 18 "math/rand" 19 "path" 20 "path/filepath" 21 "strconv" 22 "strings" 23 "testing" 24 25 "github.com/gohugoio/hugo/resources/page" 26 27 qt "github.com/frankban/quicktest" 28 ) 29 30 type siteBenchmarkTestcase struct { 31 name string 32 create func(t testing.TB) *sitesBuilder 33 check func(s *sitesBuilder) 34 } 35 36 func getBenchmarkSiteDeepContent(b testing.TB) *sitesBuilder { 37 pageContent := func(size int) string { 38 return getBenchmarkTestDataPageContentForMarkdown(size, false, "", benchmarkMarkdownSnippets) 39 } 40 41 sb := newTestSitesBuilder(b).WithConfigFile("toml", ` 42 baseURL = "https://example.com" 43 44 [languages] 45 [languages.en] 46 weight=1 47 contentDir="content/en" 48 [languages.fr] 49 weight=2 50 contentDir="content/fr" 51 [languages.no] 52 weight=3 53 contentDir="content/no" 54 [languages.sv] 55 weight=4 56 contentDir="content/sv" 57 58 `) 59 60 createContent := func(dir, name string) { 61 sb.WithContent(filepath.Join("content", dir, name), pageContent(1)) 62 } 63 64 createBundledFiles := func(dir string) { 65 sb.WithContent(filepath.Join("content", dir, "data.json"), `{ "hello": "world" }`) 66 for i := 1; i <= 3; i++ { 67 sb.WithContent(filepath.Join("content", dir, fmt.Sprintf("page%d.md", i)), pageContent(1)) 68 } 69 } 70 71 for _, lang := range []string{"en", "fr", "no", "sv"} { 72 for level := 1; level <= 5; level++ { 73 sectionDir := path.Join(lang, strings.Repeat("section/", level)) 74 createContent(sectionDir, "_index.md") 75 createBundledFiles(sectionDir) 76 for i := 1; i <= 3; i++ { 77 leafBundleDir := path.Join(sectionDir, fmt.Sprintf("bundle%d", i)) 78 createContent(leafBundleDir, "index.md") 79 createBundledFiles(path.Join(leafBundleDir, "assets1")) 80 createBundledFiles(path.Join(leafBundleDir, "assets1", "assets2")) 81 } 82 } 83 } 84 85 return sb 86 } 87 88 func getBenchmarkTestDataPageContentForMarkdown(size int, toml bool, category, markdown string) string { 89 base := `--- 90 title: "My Page" 91 %s 92 --- 93 94 My page content. 95 ` 96 if toml { 97 base = `+++ 98 title="My Page" 99 %s 100 +++ 101 102 My page content. 103 ` 104 105 } 106 107 var categoryKey string 108 if category != "" { 109 categoryKey = fmt.Sprintf("categories: [%s]", category) 110 if toml { 111 categoryKey = fmt.Sprintf("categories=[%s]", category) 112 } 113 } 114 base = fmt.Sprintf(base, categoryKey) 115 116 return base + strings.Repeat(markdown, size) 117 } 118 119 const benchmarkMarkdownSnippets = ` 120 121 ## Links 122 123 124 This is [an example](http://example.com/ "Title") inline link. 125 126 [This link](http://example.net/) has no title attribute. 127 128 This is [Relative](/all-is-relative). 129 130 See my [About](/about/) page for details. 131 ` 132 133 func getBenchmarkSiteNewTestCases() []siteBenchmarkTestcase { 134 pageContentWithCategory := func(size int, category string) string { 135 return getBenchmarkTestDataPageContentForMarkdown(size, false, category, benchmarkMarkdownSnippets) 136 } 137 138 pageContent := func(size int) string { 139 return getBenchmarkTestDataPageContentForMarkdown(size, false, "", benchmarkMarkdownSnippets) 140 } 141 142 config := ` 143 baseURL = "https://example.com" 144 ` 145 146 benchmarks := []siteBenchmarkTestcase{ 147 { 148 "Bundle with image", func(b testing.TB) *sitesBuilder { 149 sb := newTestSitesBuilder(b).WithConfigFile("toml", config) 150 sb.WithContent("content/blog/mybundle/index.md", pageContent(1)) 151 sb.WithSunset("content/blog/mybundle/sunset1.jpg") 152 153 return sb 154 }, 155 func(s *sitesBuilder) { 156 s.AssertFileContent("public/blog/mybundle/index.html", "/blog/mybundle/sunset1.jpg") 157 s.CheckExists("public/blog/mybundle/sunset1.jpg") 158 }, 159 }, 160 { 161 "Bundle with JSON file", func(b testing.TB) *sitesBuilder { 162 sb := newTestSitesBuilder(b).WithConfigFile("toml", config) 163 sb.WithContent("content/blog/mybundle/index.md", pageContent(1)) 164 sb.WithContent("content/blog/mybundle/mydata.json", `{ "hello": "world" }`) 165 166 return sb 167 }, 168 func(s *sitesBuilder) { 169 s.AssertFileContent("public/blog/mybundle/index.html", "Resources: application/json: /blog/mybundle/mydata.json") 170 s.CheckExists("public/blog/mybundle/mydata.json") 171 }, 172 }, 173 { 174 "Tags and categories", func(b testing.TB) *sitesBuilder { 175 sb := newTestSitesBuilder(b).WithConfigFile("toml", ` 176 title = "Tags and Cats" 177 baseURL = "https://example.com" 178 179 `) 180 181 const pageTemplate = ` 182 --- 183 title: "Some tags and cats" 184 categories: ["caGR", "cbGR"] 185 tags: ["taGR", "tbGR"] 186 --- 187 188 Some content. 189 190 ` 191 for i := 1; i <= 100; i++ { 192 content := strings.Replace(pageTemplate, "GR", strconv.Itoa(i/3), -1) 193 sb.WithContent(fmt.Sprintf("content/page%d.md", i), content) 194 } 195 196 return sb 197 }, 198 func(s *sitesBuilder) { 199 s.AssertFileContent("public/page3/index.html", "/page3/|Permalink: https://example.com/page3/") 200 s.AssertFileContent("public/tags/ta3/index.html", "|ta3|") 201 }, 202 }, 203 { 204 "Canonify URLs", func(b testing.TB) *sitesBuilder { 205 sb := newTestSitesBuilder(b).WithConfigFile("toml", ` 206 title = "Canon" 207 baseURL = "https://example.com" 208 canonifyURLs = true 209 210 `) 211 for i := 1; i <= 100; i++ { 212 sb.WithContent(fmt.Sprintf("content/page%d.md", i), pageContent(i)) 213 } 214 215 return sb 216 }, 217 func(s *sitesBuilder) { 218 s.AssertFileContent("public/page8/index.html", "https://example.com/about/") 219 }, 220 }, 221 222 { 223 "Deep content tree", func(b testing.TB) *sitesBuilder { 224 return getBenchmarkSiteDeepContent(b) 225 }, 226 func(s *sitesBuilder) { 227 s.CheckExists("public/blog/mybundle/index.html") 228 s.Assert(len(s.H.Sites), qt.Equals, 4) 229 s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, len(s.H.Sites[1].RegularPages())) 230 s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, 30) 231 }, 232 }, 233 { 234 "TOML front matter", func(b testing.TB) *sitesBuilder { 235 sb := newTestSitesBuilder(b).WithConfigFile("toml", config) 236 for i := 1; i <= 200; i++ { 237 content := getBenchmarkTestDataPageContentForMarkdown(1, true, "\"a\", \"b\", \"c\"", benchmarkMarkdownSnippets) 238 sb.WithContent(fmt.Sprintf("content/p%d.md", i), content) 239 } 240 241 return sb 242 }, 243 func(s *sitesBuilder) { 244 245 }, 246 }, 247 { 248 "Many HTML templates", func(b testing.TB) *sitesBuilder { 249 pageTemplateTemplate := ` 250 <!DOCTYPE html> 251 <html> 252 <head> 253 <meta charset="utf-8"> 254 <title>{{ if not .IsPage }}{{ .Title }}{{ else }}{{ printf "Site: %s" site.Title }}{{ end }}</title> 255 <style> 256 body { 257 margin: 3rem; 258 } 259 </style> 260 </head> 261 <body> 262 <div class="page">{{ .Content }}</div> 263 <ul> 264 {{ with .Pages }} 265 {{ range . }} 266 <li><a href="{{ .RelPermalink }}">{{ .LinkTitle }} {{ if not .IsNode }} (Page){{ end }}</a></li> 267 {{ end }} 268 {{ end }} 269 </ul> 270 </body> 271 </html> 272 ` 273 274 sb := newTestSitesBuilder(b).WithConfigFile("toml", ` 275 baseURL = "https://example.com" 276 277 [languages] 278 [languages.en] 279 weight=1 280 contentDir="content/en" 281 [languages.fr] 282 weight=2 283 contentDir="content/fr" 284 [languages.no] 285 weight=3 286 contentDir="content/no" 287 [languages.sv] 288 weight=4 289 contentDir="content/sv" 290 291 `) 292 293 createContent := func(dir, name string) { 294 sb.WithContent(filepath.Join("content", dir, name), pageContent(1)) 295 } 296 297 for _, lang := range []string{"en", "fr", "no", "sv"} { 298 sb.WithTemplatesAdded(fmt.Sprintf("_default/single.%s.html", lang), pageTemplateTemplate) 299 sb.WithTemplatesAdded(fmt.Sprintf("_default/list.%s.html", lang), pageTemplateTemplate) 300 301 for level := 1; level <= 5; level++ { 302 sectionDir := path.Join(lang, strings.Repeat("section/", level)) 303 createContent(sectionDir, "_index.md") 304 for i := 1; i <= 3; i++ { 305 leafBundleDir := path.Join(sectionDir, fmt.Sprintf("bundle%d", i)) 306 createContent(leafBundleDir, "index.md") 307 } 308 } 309 } 310 311 return sb 312 }, 313 func(s *sitesBuilder) { 314 s.CheckExists("public/blog/mybundle/index.html") 315 s.Assert(len(s.H.Sites), qt.Equals, 4) 316 s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, len(s.H.Sites[1].RegularPages())) 317 s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, 15) 318 }, 319 }, 320 { 321 "Page collections", func(b testing.TB) *sitesBuilder { 322 pageTemplateTemplate := ` 323 {{ if .IsNode }} 324 {{ len .Paginator.Pages }} 325 {{ end }} 326 {{ len .Sections }} 327 {{ len .Pages }} 328 {{ len .RegularPages }} 329 {{ len .Resources }} 330 {{ len site.RegularPages }} 331 {{ len site.Pages }} 332 {{ with .NextInSection }}Next in section: {{ .RelPermalink }}{{ end }} 333 {{ with .PrevInSection }}Prev in section: {{ .RelPermalink }}{{ end }} 334 {{ with .Next }}Next: {{ .RelPermalink }}{{ end }} 335 {{ with .Prev }}Prev: {{ .RelPermalink }}{{ end }} 336 ` 337 338 sb := newTestSitesBuilder(b).WithConfigFile("toml", ` 339 baseURL = "https://example.com" 340 341 [languages] 342 [languages.en] 343 weight=1 344 contentDir="content/en" 345 [languages.fr] 346 weight=2 347 contentDir="content/fr" 348 [languages.no] 349 weight=3 350 contentDir="content/no" 351 [languages.sv] 352 weight=4 353 contentDir="content/sv" 354 355 `) 356 357 sb.WithTemplates("index.html", pageTemplateTemplate) 358 sb.WithTemplates("_default/single.html", pageTemplateTemplate) 359 sb.WithTemplates("_default/list.html", pageTemplateTemplate) 360 361 r := rand.New(rand.NewSource(99)) 362 363 createContent := func(dir, name string) { 364 var content string 365 if strings.Contains(name, "_index") { 366 content = pageContent(1) 367 } else { 368 content = pageContentWithCategory(1, fmt.Sprintf("category%d", r.Intn(5)+1)) 369 } 370 371 sb.WithContent(filepath.Join("content", dir, name), content) 372 } 373 374 createBundledFiles := func(dir string) { 375 sb.WithContent(filepath.Join("content", dir, "data.json"), `{ "hello": "world" }`) 376 for i := 1; i <= 3; i++ { 377 sb.WithContent(filepath.Join("content", dir, fmt.Sprintf("page%d.md", i)), pageContent(1)) 378 } 379 } 380 381 for _, lang := range []string{"en", "fr", "no", "sv"} { 382 for level := 1; level <= r.Intn(5)+1; level++ { 383 sectionDir := path.Join(lang, strings.Repeat("section/", level)) 384 createContent(sectionDir, "_index.md") 385 createBundledFiles(sectionDir) 386 for i := 1; i <= r.Intn(20)+1; i++ { 387 leafBundleDir := path.Join(sectionDir, fmt.Sprintf("bundle%d", i)) 388 createContent(leafBundleDir, "index.md") 389 createBundledFiles(path.Join(leafBundleDir, "assets1")) 390 createBundledFiles(path.Join(leafBundleDir, "assets1", "assets2")) 391 } 392 } 393 } 394 395 return sb 396 }, 397 func(s *sitesBuilder) { 398 s.CheckExists("public/blog/mybundle/index.html") 399 s.Assert(len(s.H.Sites), qt.Equals, 4) 400 s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, 26) 401 }, 402 }, 403 { 404 "List terms", func(b testing.TB) *sitesBuilder { 405 pageTemplateTemplate := ` 406 <ul> 407 {{ range (.GetTerms "categories") }} 408 <li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li> 409 {{ end }} 410 </ul> 411 ` 412 413 sb := newTestSitesBuilder(b).WithConfigFile("toml", ` 414 baseURL = "https://example.com" 415 `) 416 417 sb.WithTemplates("_default/single.html", pageTemplateTemplate) 418 419 r := rand.New(rand.NewSource(99)) 420 421 createContent := func(dir, name string) { 422 var content string 423 if strings.Contains(name, "_index") { 424 content = pageContent(1) 425 } else { 426 content = pageContentWithCategory(1, fmt.Sprintf("category%d", r.Intn(5)+1)) 427 sb.WithContent(filepath.Join("content", dir, name), content) 428 } 429 } 430 431 for level := 1; level <= r.Intn(5)+1; level++ { 432 sectionDir := path.Join(strings.Repeat("section/", level)) 433 createContent(sectionDir, "_index.md") 434 for i := 1; i <= r.Intn(33); i++ { 435 leafBundleDir := path.Join(sectionDir, fmt.Sprintf("bundle%d", i)) 436 createContent(leafBundleDir, "index.md") 437 } 438 } 439 440 return sb 441 }, 442 func(s *sitesBuilder) { 443 s.AssertFileContent("public/section/bundle8/index.html", ` <li><a href="https://example.com/categories/category1/">category1</a></li>`) 444 s.Assert(len(s.H.Sites), qt.Equals, 1) 445 s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, 35) 446 }, 447 }, 448 } 449 450 return benchmarks 451 } 452 453 // Run the benchmarks below as tests. Mostly useful when adding new benchmark 454 // variants. 455 func TestBenchmarkSiteNew(b *testing.T) { 456 benchmarks := getBenchmarkSiteNewTestCases() 457 for _, bm := range benchmarks { 458 b.Run(bm.name, func(b *testing.T) { 459 s := bm.create(b) 460 461 err := s.BuildE(BuildCfg{}) 462 if err != nil { 463 b.Fatal(err) 464 } 465 bm.check(s) 466 }) 467 } 468 } 469 470 func TestBenchmarkSiteDeepContentEdit(t *testing.T) { 471 b := getBenchmarkSiteDeepContent(t).Running() 472 b.Build(BuildCfg{}) 473 474 p := b.H.Sites[0].RegularPages()[12] 475 476 b.EditFiles(p.File().Filename(), fmt.Sprintf(`--- 477 title: %s 478 --- 479 480 Edited!!`, p.Title())) 481 482 counters := &testCounters{} 483 484 b.Build(BuildCfg{testCounters: counters}) 485 486 // We currently rebuild all the language versions of the same content file. 487 // We could probably optimize that case, but it's not trivial. 488 b.Assert(int(counters.contentRenderCounter), qt.Equals, 4) 489 b.AssertFileContent("public"+p.RelPermalink()+"index.html", "Edited!!") 490 } 491 492 func BenchmarkSiteNew(b *testing.B) { 493 rnd := rand.New(rand.NewSource(32)) 494 benchmarks := getBenchmarkSiteNewTestCases() 495 for _, edit := range []bool{true, false} { 496 for _, bm := range benchmarks { 497 name := bm.name 498 if edit { 499 name = "Edit_" + name 500 } else { 501 name = "Regular_" + name 502 } 503 b.Run(name, func(b *testing.B) { 504 sites := make([]*sitesBuilder, b.N) 505 for i := 0; i < b.N; i++ { 506 sites[i] = bm.create(b) 507 if edit { 508 sites[i].Running() 509 } 510 } 511 512 b.ResetTimer() 513 for i := 0; i < b.N; i++ { 514 if edit { 515 b.StopTimer() 516 } 517 s := sites[i] 518 err := s.BuildE(BuildCfg{}) 519 if err != nil { 520 b.Fatal(err) 521 } 522 bm.check(s) 523 524 if edit { 525 if edit { 526 b.StartTimer() 527 } 528 // Edit a random page in a random language. 529 pages := s.H.Sites[rnd.Intn(len(s.H.Sites))].Pages() 530 var p page.Page 531 count := 0 532 for { 533 count++ 534 if count > 100 { 535 panic("infinite loop") 536 } 537 p = pages[rnd.Intn(len(pages))] 538 if !p.File().IsZero() { 539 break 540 } 541 } 542 543 s.EditFiles(p.File().Filename(), fmt.Sprintf(`--- 544 title: %s 545 --- 546 547 Edited!!`, p.Title())) 548 549 err := s.BuildE(BuildCfg{}) 550 if err != nil { 551 b.Fatal(err) 552 } 553 } 554 } 555 }) 556 } 557 } 558 }