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 }