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:
wxiaoguang 2023-04-30 20:22:23 +08:00 committed by GitHub
parent 8f4dafcd4e
commit e3750370df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 37 additions and 34 deletions

View file

@ -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

View file

@ -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{

View file

@ -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

View file

@ -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) {

View file

@ -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() {

View file

@ -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,

View file

@ -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()...)

View file

@ -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) {

View file

@ -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)

View file

@ -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() {

View file

@ -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),