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