// There will be normally only one instance of deps in play
// at a given time, i.e. one per Site built.
type Deps struct {
-
// The logger to use.
Log loggers.Logger `json:"-"`
// BuildStartListeners will be notified before a build starts.
BuildStartListeners *Listeners
+ // BuildEndListeners will be notified after a build finishes.
+ BuildEndListeners *Listeners
+
// Resources that gets closed when the build is done or the server shuts down.
BuildClosers *Closers
}
return &d, nil
-
}
func (d *Deps) SetTempl(t *tpl.TemplateHandlers) {
d.BuildStartListeners = &Listeners{}
}
+ if d.BuildEndListeners == nil {
+ d.BuildEndListeners = &Listeners{}
+ }
+
if d.BuildClosers == nil {
d.BuildClosers = &Closers{}
}
"bytes"
"context"
"encoding/json"
+ "errors"
"fmt"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/output"
- "errors"
-
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/helpers"
)
defer unlock()
}
+ defer func() {
+ for _, s := range h.Sites {
+ s.Deps.BuildEndListeners.Notify()
+ }
+ }()
+
infol := h.Log.InfoCommand("build")
errCollector := h.StartErrorCollector()
h.Log.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
}
}
-
})
return nil
}
}
// Make sure it's written to the OS fs as this is used by
// editors.
- if err := afero.WriteFile(hugofs.Os, filename, b, 0666); err != nil {
+ if err := afero.WriteFile(hugofs.Os, filename, b, 0o666); err != nil {
h.Log.Warnf("Failed to write jsconfig.json: %s", err)
}
}
}
if changed {
- return afero.WriteFile(h.BaseFs.PublishFs, filename, content, 0666)
+ return afero.WriteFile(h.BaseFs.PublishFs, filename, content, 0o666)
}
return nil
}
// Make sure it's always written to the OS fs.
- if err := afero.WriteFile(hugofs.Os, filename, js, 0666); err != nil {
+ if err := afero.WriteFile(hugofs.Os, filename, js, 0o666); err != nil {
return err
}
// Write to the destination as well if it's a in-memory fs.
if !hugofs.IsOsFs(h.Fs.Source) {
- if err := afero.WriteFile(h.Fs.WorkingDirWritable, filename, js, 0666); err != nil {
+ if err := afero.WriteFile(h.Fs.WorkingDirWritable, filename, js, 0o666); err != nil {
return err
}
}
package debug
import (
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/bep/logg"
"github.com/sanity-io/litter"
"github.com/spf13/cast"
"github.com/yuin/goldmark/util"
// New returns a new instance of the debug-namespaced template functions.
func New(d *deps.Deps) *Namespace {
- return &Namespace{}
+ var timers map[string][]*timer
+ if d.Log.Level() <= logg.LevelInfo {
+ timers = make(map[string][]*timer)
+ }
+ ns := &Namespace{
+ timers: timers,
+ }
+
+ if ns.timers == nil {
+ return ns
+ }
+
+ l := d.Log.InfoCommand("timer")
+
+ d.BuildEndListeners.Add(func() {
+ type nameCountDuration struct {
+ Name string
+ Count int
+ Duration time.Duration
+ }
+
+ var timersSorted []nameCountDuration
+
+ for k, v := range timers {
+ var total time.Duration
+ for _, t := range v {
+ // Stop any running timers.
+ t.Stop()
+ total += t.elapsed
+ }
+ timersSorted = append(timersSorted, nameCountDuration{k, len(v), total})
+ }
+
+ sort.Slice(timersSorted, func(i, j int) bool {
+ // Sort it so the slowest gets printed last.
+ return timersSorted[i].Duration < timersSorted[j].Duration
+ })
+
+ for _, t := range timersSorted {
+ l.WithField("name", t.Name).WithField("count", t.Count).WithField("duration", t.Duration).Logf("")
+ }
+
+ ns.timers = make(map[string][]*timer)
+ })
+
+ return ns
}
// Namespace provides template functions for the "debug" namespace.
type Namespace struct {
+ timersMu sync.Mutex
+ timers map[string][]*timer
}
// Dump returns a object dump of val as a string.
s := cast.ToString(val)
return string(util.VisualizeSpaces([]byte(s)))
}
+
+func (ns *Namespace) Timer(name string) Timer {
+ if ns.timers == nil {
+ return nopTimer
+ }
+ ns.timersMu.Lock()
+ defer ns.timersMu.Unlock()
+ t := &timer{start: time.Now()}
+ ns.timers[name] = append(ns.timers[name], t)
+ return t
+}
+
+var nopTimer = nopTimerImpl{}
+
+type nopTimerImpl struct{}
+
+func (nopTimerImpl) Stop() string {
+ return ""
+}
+
+// Timer is a timer that can be stopped.
+type Timer interface {
+ // Stop stops the timer and returns an empty string.
+ // Stop can be called multiple times, but only the first call will stop the timer.
+ // If Stop is not called, the timer will be stopped when the build ends.
+ Stop() string
+}
+
+type timer struct {
+ start time.Time
+ elapsed time.Duration
+ stopOnce sync.Once
+}
+
+func (t *timer) Stop() string {
+ t.stopOnce.Do(func() {
+ t.elapsed = time.Since(t.start)
+ })
+ // This is used in templates, we need to return something.
+ return ""
+}
--- /dev/null
+// Copyright 2023 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless requiredF by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package debug_test
+
+import (
+ "testing"
+
+ "github.com/bep/logg"
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestTimer(t *testing.T) {
+ files := `
+-- hugo.toml --
+baseURL = "https://example.org/"
+disableKinds = ["taxonomy", "term"]
+-- layouts/index.html --
+{{ range seq 2 }}
+{{ $t := debug.Timer "foo" }}
+{{ seq 1 1000 }}
+{{ $t.Stop }}
+{{ end }}
+
+`
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ LogLevel: logg.LevelInfo,
+ },
+ ).Build()
+
+ b.AssertLogContains("imer: name \"foo\" count '\\x02' duration")
+}