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