Use globally shared HTMLRender (#24436)
The old `HTMLRender` is not ideal. 1. It shouldn't be initialized multiple times, it consumes a lot of memory and is slow. 2. It shouldn't depend on short-lived requests, the `WatchLocalChanges` needs a long-running context. 3. It doesn't make sense to use FuncsMap slice. HTMLRender was designed to only work for GItea's specialized 400+ templates, so it's good to make it a global shared instance.
This commit is contained in:
parent
8f4dafcd4e
commit
e3750370df
11 changed files with 37 additions and 34 deletions
|
@ -677,7 +677,7 @@ func getCsrfOpts() CsrfOptions {
|
||||||
|
|
||||||
// Contexter initializes a classic context for a request.
|
// Contexter initializes a classic context for a request.
|
||||||
func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
|
func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
|
||||||
_, rnd := templates.HTMLRenderer(ctx)
|
rnd := templates.HTMLRenderer()
|
||||||
csrfOpts := getCsrfOpts()
|
csrfOpts := getCsrfOpts()
|
||||||
if !setting.IsProd {
|
if !setting.IsProd {
|
||||||
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
|
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
|
||||||
|
|
|
@ -131,7 +131,7 @@ func determineAccessMode(ctx *Context) (perm.AccessMode, error) {
|
||||||
|
|
||||||
// PackageContexter initializes a package context for a request.
|
// PackageContexter initializes a package context for a request.
|
||||||
func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handler {
|
func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handler {
|
||||||
_, rnd := templates.HTMLRenderer(ctx)
|
rnd := templates.HTMLRenderer()
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||||
ctx := Context{
|
ctx := Context{
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -26,12 +25,9 @@ import (
|
||||||
"code.gitea.io/gitea/services/gitdiff"
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Used from static.go && dynamic.go
|
|
||||||
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
|
|
||||||
|
|
||||||
// NewFuncMap returns functions for injecting to templates
|
// NewFuncMap returns functions for injecting to templates
|
||||||
func NewFuncMap() []template.FuncMap {
|
func NewFuncMap() template.FuncMap {
|
||||||
return []template.FuncMap{map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"DumpVar": dumpVar,
|
"DumpVar": dumpVar,
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
|
@ -192,7 +188,7 @@ func NewFuncMap() []template.FuncMap {
|
||||||
|
|
||||||
"FilenameIsImage": FilenameIsImage,
|
"FilenameIsImage": FilenameIsImage,
|
||||||
"TabSizeClass": TabSizeClass,
|
"TabSizeClass": TabSizeClass,
|
||||||
}}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safe render raw as HTML
|
// Safe render raw as HTML
|
||||||
|
|
|
@ -6,7 +6,6 @@ package templates
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -15,24 +14,29 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
texttemplate "text/template"
|
texttemplate "text/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/assetfs"
|
"code.gitea.io/gitea/modules/assetfs"
|
||||||
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates/scopedtmpl"
|
"code.gitea.io/gitea/modules/templates/scopedtmpl"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rendererKey interface{} = "templatesHtmlRenderer"
|
|
||||||
|
|
||||||
type TemplateExecutor scopedtmpl.TemplateExecutor
|
type TemplateExecutor scopedtmpl.TemplateExecutor
|
||||||
|
|
||||||
type HTMLRender struct {
|
type HTMLRender struct {
|
||||||
templates atomic.Pointer[scopedtmpl.ScopedTemplate]
|
templates atomic.Pointer[scopedtmpl.ScopedTemplate]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
htmlRender *HTMLRender
|
||||||
|
htmlRenderOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
|
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
|
||||||
|
|
||||||
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error {
|
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error {
|
||||||
|
@ -55,14 +59,14 @@ func (h *HTMLRender) TemplateLookup(name string) (TemplateExecutor, error) {
|
||||||
return nil, ErrTemplateNotInitialized
|
return nil, ErrTemplateNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
return tmpls.Executor(name, NewFuncMap()[0])
|
return tmpls.Executor(name, NewFuncMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTMLRender) CompileTemplates() error {
|
func (h *HTMLRender) CompileTemplates() error {
|
||||||
assets := AssetFS()
|
assets := AssetFS()
|
||||||
extSuffix := ".tmpl"
|
extSuffix := ".tmpl"
|
||||||
tmpls := scopedtmpl.NewScopedTemplate()
|
tmpls := scopedtmpl.NewScopedTemplate()
|
||||||
tmpls.Funcs(NewFuncMap()[0])
|
tmpls.Funcs(NewFuncMap())
|
||||||
files, err := ListWebTemplateAssetNames(assets)
|
files, err := ListWebTemplateAssetNames(assets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -86,20 +90,21 @@ func (h *HTMLRender) CompileTemplates() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use
|
// HTMLRenderer init once and returns the globally shared html renderer
|
||||||
func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
|
func HTMLRenderer() *HTMLRender {
|
||||||
if renderer, ok := ctx.Value(rendererKey).(*HTMLRender); ok {
|
htmlRenderOnce.Do(initHTMLRenderer)
|
||||||
return ctx, renderer
|
return htmlRender
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initHTMLRenderer() {
|
||||||
rendererType := "static"
|
rendererType := "static"
|
||||||
if !setting.IsProd {
|
if !setting.IsProd {
|
||||||
rendererType = "auto-reloading"
|
rendererType = "auto-reloading"
|
||||||
}
|
}
|
||||||
log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer")
|
log.Debug("Creating %s HTML Renderer", rendererType)
|
||||||
|
|
||||||
renderer := &HTMLRender{}
|
htmlRender = &HTMLRender{}
|
||||||
if err := renderer.CompileTemplates(); err != nil {
|
if err := htmlRender.CompileTemplates(); err != nil {
|
||||||
p := &templateErrorPrettier{assets: AssetFS()}
|
p := &templateErrorPrettier{assets: AssetFS()}
|
||||||
wrapFatal(p.handleFuncNotDefinedError(err))
|
wrapFatal(p.handleFuncNotDefinedError(err))
|
||||||
wrapFatal(p.handleUnexpectedOperandError(err))
|
wrapFatal(p.handleUnexpectedOperandError(err))
|
||||||
|
@ -107,14 +112,14 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
|
||||||
wrapFatal(p.handleGenericTemplateError(err))
|
wrapFatal(p.handleGenericTemplateError(err))
|
||||||
log.Fatal("HTMLRenderer CompileTemplates error: %v", err)
|
log.Fatal("HTMLRenderer CompileTemplates error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !setting.IsProd {
|
if !setting.IsProd {
|
||||||
go AssetFS().WatchLocalChanges(ctx, func() {
|
go AssetFS().WatchLocalChanges(graceful.GetManager().ShutdownContext(), func() {
|
||||||
if err := renderer.CompileTemplates(); err != nil {
|
if err := htmlRender.CompileTemplates(); err != nil {
|
||||||
log.Error("Template error: %v\n%s", err, log.Stack(2))
|
log.Error("Template error: %v\n%s", err, log.Stack(2))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return context.WithValue(ctx, rendererKey, renderer), renderer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapFatal(msg string) {
|
func wrapFatal(msg string) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ package templates
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
texttmpl "text/template"
|
texttmpl "text/template"
|
||||||
|
|
||||||
|
@ -14,6 +15,8 @@ import (
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
||||||
|
|
||||||
// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
|
// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
|
||||||
func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
||||||
return texttmpl.FuncMap{
|
return texttmpl.FuncMap{
|
||||||
|
@ -55,9 +58,7 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||||
bodyTemplates := template.New("")
|
bodyTemplates := template.New("")
|
||||||
|
|
||||||
subjectTemplates.Funcs(mailSubjectTextFuncMap())
|
subjectTemplates.Funcs(mailSubjectTextFuncMap())
|
||||||
for _, funcs := range NewFuncMap() {
|
bodyTemplates.Funcs(NewFuncMap())
|
||||||
bodyTemplates.Funcs(funcs)
|
|
||||||
}
|
|
||||||
|
|
||||||
assetFS := AssetFS()
|
assetFS := AssetFS()
|
||||||
refreshTemplates := func() {
|
refreshTemplates := func() {
|
||||||
|
|
|
@ -30,7 +30,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecorder) {
|
func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecorder) {
|
||||||
_, rnd := templates.HTMLRenderer(req.Context())
|
rnd := templates.HTMLRenderer()
|
||||||
resp := httptest.NewRecorder()
|
resp := httptest.NewRecorder()
|
||||||
c := &context.Context{
|
c := &context.Context{
|
||||||
Req: req,
|
Req: req,
|
||||||
|
|
|
@ -175,7 +175,7 @@ func GlobalInitInstalled(ctx context.Context) {
|
||||||
|
|
||||||
// NormalRoutes represents non install routes
|
// NormalRoutes represents non install routes
|
||||||
func NormalRoutes(ctx context.Context) *web.Route {
|
func NormalRoutes(ctx context.Context) *web.Route {
|
||||||
ctx, _ = templates.HTMLRenderer(ctx)
|
_ = templates.HTMLRenderer()
|
||||||
r := web.NewRoute()
|
r := web.NewRoute()
|
||||||
r.Use(common.ProtocolMiddlewares()...)
|
r.Use(common.ProtocolMiddlewares()...)
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
|
||||||
|
|
||||||
// Init prepare for rendering installation page
|
// Init prepare for rendering installation page
|
||||||
func Init(ctx goctx.Context) func(next http.Handler) http.Handler {
|
func Init(ctx goctx.Context) func(next http.Handler) http.Handler {
|
||||||
_, rnd := templates.HTMLRenderer(ctx)
|
rnd := templates.HTMLRenderer()
|
||||||
dbTypeNames := getSupportedDbTypeNames()
|
dbTypeNames := getSupportedDbTypeNames()
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
|
|
@ -66,7 +66,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
|
||||||
if !setting.IsProd {
|
if !setting.IsProd {
|
||||||
store["ErrorMsg"] = combinedErr
|
store["ErrorMsg"] = combinedErr
|
||||||
}
|
}
|
||||||
_, rnd := templates.HTMLRenderer(ctx)
|
rnd := templates.HTMLRenderer()
|
||||||
err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store))
|
err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("%v", err)
|
log.Error("%v", err)
|
||||||
|
|
|
@ -120,7 +120,7 @@ func (d *dataStore) GetData() map[string]interface{} {
|
||||||
// RecoveryWith500Page returns a middleware that recovers from any panics and writes a 500 and a log if so.
|
// RecoveryWith500Page returns a middleware that recovers from any panics and writes a 500 and a log if so.
|
||||||
// This error will be created with the gitea 500 page.
|
// This error will be created with the gitea 500 page.
|
||||||
func RecoveryWith500Page(ctx goctx.Context) func(next http.Handler) http.Handler {
|
func RecoveryWith500Page(ctx goctx.Context) func(next http.Handler) http.Handler {
|
||||||
_, rnd := templates.HTMLRenderer(ctx)
|
rnd := templates.HTMLRenderer()
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
@ -114,7 +114,8 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||||
routes.RouteMethods("/apple-touch-icon.png", "GET, HEAD", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
|
routes.RouteMethods("/apple-touch-icon.png", "GET, HEAD", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
|
||||||
routes.RouteMethods("/favicon.ico", "GET, HEAD", misc.StaticRedirect("/assets/img/favicon.png"))
|
routes.RouteMethods("/favicon.ico", "GET, HEAD", misc.StaticRedirect("/assets/img/favicon.png"))
|
||||||
|
|
||||||
ctx, _ = templates.HTMLRenderer(ctx)
|
_ = templates.HTMLRenderer()
|
||||||
|
|
||||||
common := []any{
|
common := []any{
|
||||||
common.Sessioner(),
|
common.Sessioner(),
|
||||||
RecoveryWith500Page(ctx),
|
RecoveryWith500Page(ctx),
|
||||||
|
|
Loading…
Reference in a new issue