diff --git a/cache.go b/cache.go index a8a930b..723a46a 100644 --- a/cache.go +++ b/cache.go @@ -22,16 +22,14 @@ const ( ) type cache struct { - g singleflight.Group - c *ristretto.Cache - cfg *configCache + g singleflight.Group + c *ristretto.Cache } func (a *goBlog) initCache() (err error) { - a.cache = &cache{ - cfg: a.cfg.Cache, - } - if a.cache.cfg != nil && !a.cache.cfg.Enable { + a.cache = &cache{} + if a.cfg.Cache != nil && !a.cfg.Cache.Enable { + // Cache disabled return nil } a.cache.c, err = ristretto.NewCache(&ristretto.Config{ @@ -50,13 +48,8 @@ func cacheLoggedIn(next http.Handler) http.Handler { func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if a.cache.c == nil { - // No cache configured - next.ServeHTTP(w, r) - return - } // Do checks - if !cacheable(r) { + if a.cache.c == nil || !cacheable(r) { next.ServeHTTP(w, r) return } @@ -79,7 +72,7 @@ func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler { }) ci := cacheInterface.(*cacheItem) // copy and set headers - a.cache.setHeaders(w, ci) + a.setCacheHeaders(w, ci) // check conditional request if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == ci.eTag { // send 304 @@ -129,7 +122,7 @@ func cacheKey(r *http.Request) string { return buf.String() } -func (c *cache) setHeaders(w http.ResponseWriter, cache *cacheItem) { +func (a *goBlog) setCacheHeaders(w http.ResponseWriter, cache *cacheItem) { // Copy headers for k, v := range cache.header.Clone() { w.Header()[k] = v @@ -141,7 +134,8 @@ func (c *cache) setHeaders(w http.ResponseWriter, cache *cacheItem) { if cache.expiration != 0 { w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,stale-while-revalidate=%d", cache.expiration, cache.expiration)) } else { - w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", c.cfg.Expiration, c.cfg.Expiration/3, c.cfg.Expiration)) + exp := a.cfg.Cache.Expiration + w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", exp, exp/3, exp)) } } } diff --git a/editor.go b/editor.go index 84d0b88..7ad01af 100644 --- a/editor.go +++ b/editor.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" "gopkg.in/yaml.v3" ws "nhooyr.io/websocket" @@ -71,9 +72,11 @@ func (a *goBlog) createMarkdownPreview(blog string, markdown []byte) (rendered [ p.RenderedTitle = a.renderMdTitle(t) } // Render post - var hb htmlBuilder - a.renderEditorPreview(&hb, a.cfg.Blogs[blog], p) - rendered = hb.Bytes() + buf := bufferpool.Get() + hb := newHtmlBuilder(buf) + a.renderEditorPreview(hb, a.cfg.Blogs[blog], p) + rendered = buf.Bytes() + bufferpool.Put(buf) return } diff --git a/go.mod b/go.mod index c207770..a02d34e 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b nhooyr.io/websocket v1.8.7 - tailscale.com v1.20.2 + tailscale.com v1.20.3 // main willnorris.com/go/microformats v1.1.2-0.20210827044458-ff2a6ae41971 ) diff --git a/go.sum b/go.sum index 22117b7..3a097d5 100644 --- a/go.sum +++ b/go.sum @@ -823,8 +823,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4= -tailscale.com v1.20.2 h1:Vk6SVGmczDFSx+PYjbKMa8gIC9Y9Rq+vT1XoZni+fkA= -tailscale.com v1.20.2/go.mod h1:kjVy3ji2OH5lZhPLIIRacoY3CN4Bo3Yyb2mtoM8nfJ4= +tailscale.com v1.20.3 h1:C3g2AgmQaOi0YT5dAal9mslugPXMxwj0EXY7YfL2QrA= +tailscale.com v1.20.3/go.mod h1:kjVy3ji2OH5lZhPLIIRacoY3CN4Bo3Yyb2mtoM8nfJ4= willnorris.com/go/microformats v1.1.2-0.20210827044458-ff2a6ae41971 h1:b4juh5znIpBA1KnzHMP0UB4Cs+3/0b0XfchkWE81FXw= willnorris.com/go/microformats v1.1.2-0.20210827044458-ff2a6ae41971/go.mod h1:kvVnWrkkEscVAIITCEoiTX66Hcyg59C7q0E49mb9TJ0= willnorris.com/go/webmention v0.0.0-20211028201829-b0044f1a24d0 h1:3/ozQ2qGZat82ON3AYMTot3gCg/vU7tgn/LYSJbkVPM= diff --git a/markdown.go b/markdown.go index b0bbff5..a9160e2 100644 --- a/markdown.go +++ b/markdown.go @@ -3,6 +3,7 @@ package main import ( "bytes" "html/template" + "io" marktag "git.jlel.se/jlelse/goldmark-mark" chromahtml "github.com/alecthomas/chroma/formatters/html" @@ -15,6 +16,7 @@ import ( "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/util" + "go.goblog.app/app/pkgs/bufferpool" ) func (a *goBlog) initMarkdown() { @@ -76,13 +78,20 @@ func (a *goBlog) initMarkdown() { } func (a *goBlog) renderMarkdown(source string, absoluteLinks bool) (rendered []byte, err error) { - var buffer bytes.Buffer + buffer := bufferpool.Get() + a.renderMarkdownToWriter(buffer, source, absoluteLinks) + rendered = buffer.Bytes() + bufferpool.Put(buffer) + return +} + +func (a *goBlog) renderMarkdownToWriter(w io.Writer, source string, absoluteLinks bool) (err error) { if absoluteLinks { - err = a.absoluteMd.Convert([]byte(source), &buffer) + err = a.absoluteMd.Convert([]byte(source), w) } else { - err = a.md.Convert([]byte(source), &buffer) + err = a.md.Convert([]byte(source), w) } - return buffer.Bytes(), err + return err } func (a *goBlog) renderMarkdownAsHTML(source string, absoluteLinks bool) (rendered template.HTML, err error) { @@ -113,8 +122,9 @@ func (a *goBlog) renderMdTitle(s string) string { if s == "" { return "" } - var buffer bytes.Buffer - err := a.titleMd.Convert([]byte(s), &buffer) + buffer := bufferpool.Get() + defer bufferpool.Put(buffer) + err := a.titleMd.Convert([]byte(s), buffer) if err != nil { return "" } diff --git a/pkgs/bufferpool/bufferPool.go b/pkgs/bufferpool/bufferPool.go new file mode 100644 index 0000000..430975f --- /dev/null +++ b/pkgs/bufferpool/bufferPool.go @@ -0,0 +1,29 @@ +package bufferpool + +import ( + "bytes" + "sync" +) + +var bufferPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +var poolMutex sync.Mutex + +func Get() *bytes.Buffer { + poolMutex.Lock() + defer poolMutex.Unlock() + return bufferPool.Get().(*bytes.Buffer) +} + +func Put(bufs ...*bytes.Buffer) { + poolMutex.Lock() + defer poolMutex.Unlock() + for _, buf := range bufs { + buf.Reset() + bufferPool.Put(buf) + } +} diff --git a/render.go b/render.go index 4182b41..48c5e72 100644 --- a/render.go +++ b/render.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "html/template" "net/http" "os" @@ -9,6 +8,7 @@ import ( "path/filepath" "strings" + "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" ) @@ -33,9 +33,12 @@ func (a *goBlog) initRendering() error { "html": wrapStringAsHTML, // Code based rendering "tor": func(rd *renderData) template.HTML { - var hb htmlBuilder - a.renderTorNotice(&hb, rd) - return hb.html() + buf := bufferpool.Get() + hb := newHtmlBuilder(buf) + a.renderTorNotice(hb, rd) + res := template.HTML(buf.String()) + bufferpool.Put(buf) + return res }, // Others "dateformat": dateFormat, @@ -103,16 +106,14 @@ func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, st // Set content type w.Header().Set(contentType, contenttype.HTMLUTF8) // Render template and write minified HTML - var templateBuffer bytes.Buffer - if err := a.templates[template].ExecuteTemplate(&templateBuffer, template, data); err != nil { + buf := bufferpool.Get() + defer bufferpool.Put(buf) + if err := a.templates[template].ExecuteTemplate(buf, template, data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(statusCode) - if err := a.min.Minify(contenttype.HTML, w, &templateBuffer); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + _ = a.min.Minify(contenttype.HTML, w, buf) } func (a *goBlog) renderNew(w http.ResponseWriter, r *http.Request, f func(*htmlBuilder, *renderData), data *renderData) { diff --git a/ui.go b/ui.go index 2e9c17c..554283a 100644 --- a/ui.go +++ b/ui.go @@ -71,7 +71,7 @@ func (a *goBlog) renderBase(hb *htmlBuilder, rd *renderData, title, main func(hb // Announcement if ann := rd.Blog.Announcement; ann != nil && ann.Text != "" { hb.writeElementOpen("div", "id", "announcement", "data-nosnippet", "") - hb.writeHtml(a.safeRenderMarkdownAsHTML(ann.Text)) + _ = a.renderMarkdownToWriter(hb, ann.Text, false) hb.writeElementClose("div") } // Header @@ -270,7 +270,7 @@ func (a *goBlog) renderSearch(hb *htmlBuilder, rd *renderData) { // Description if sc.Description != "" { titleOrDesc = true - hb.writeHtml(a.safeRenderMarkdownAsHTML(sc.Description)) + _ = a.renderMarkdownToWriter(hb, sc.Description, false) } if titleOrDesc { hb.writeElementOpen("hr") @@ -381,7 +381,7 @@ func (a *goBlog) renderIndex(hb *htmlBuilder, rd *renderData) { // Description if id.description != "" { titleOrDesc = true - hb.writeHtml(a.safeRenderMarkdownAsHTML(id.description)) + _ = a.renderMarkdownToWriter(hb, id.description, false) } if titleOrDesc { hb.writeElementOpen("hr") @@ -445,7 +445,7 @@ func (a *goBlog) renderBlogStats(hb *htmlBuilder, rd *renderData) { } // Description if bs.Description != "" { - hb.writeHtml(a.safeRenderMarkdownAsHTML(bs.Description)) + _ = a.renderMarkdownToWriter(hb, bs.Description, false) } // Table hb.writeElementOpen("p", "id", "loading", "data-table", bsd.tableUrl) @@ -663,7 +663,7 @@ func (a *goBlog) renderBlogroll(hb *htmlBuilder, rd *renderData) { // Description if bd.description != "" { hb.writeElementOpen("p") - hb.writeHtml(a.safeRenderMarkdownAsHTML(bd.description)) + _ = a.renderMarkdownToWriter(hb, bd.description, false) hb.writeElementClose("p") } // Download button @@ -745,7 +745,7 @@ func (a *goBlog) renderContact(hb *htmlBuilder, rd *renderData) { } // Description if cd.description != "" { - hb.writeHtml(a.safeRenderMarkdownAsHTML(cd.description)) + _ = a.renderMarkdownToWriter(hb, cd.description, false) } // Form hb.writeElementOpen("form", "class", "fw p", "method", "post") @@ -760,7 +760,7 @@ func (a *goBlog) renderContact(hb *htmlBuilder, rd *renderData) { hb.writeElementClose("textarea") // Send if cd.privacy != "" { - hb.writeHtml(a.safeRenderMarkdownAsHTML(cd.privacy)) + _ = a.renderMarkdownToWriter(hb, cd.privacy, false) hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "contactagreesend")) } else { hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "contactsend")) @@ -837,7 +837,7 @@ func (a *goBlog) renderTaxonomy(hb *htmlBuilder, rd *renderData) { } // Description if trd.taxonomy.Description != "" { - hb.writeHtml(a.safeRenderMarkdownAsHTML(trd.taxonomy.Description)) + _ = a.renderMarkdownToWriter(hb, trd.taxonomy.Description, false) } // List for _, valGroup := range trd.valueGroups { diff --git a/uiComponents.go b/uiComponents.go index 2cf1d7f..16a31c3 100644 --- a/uiComponents.go +++ b/uiComponents.go @@ -38,7 +38,7 @@ func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ sum photos := a.photoLinks(p) if typ == photoSummary && len(photos) > 0 { for _, photo := range photos { - hb.write(string(a.safeRenderMarkdownAsHTML(fmt.Sprintf("![](%s)", photo)))) + _ = a.renderMarkdownToWriter(hb, fmt.Sprintf("![](%s)", photo), false) } } // Post meta diff --git a/uiHtmlBuilder.go b/uiHtmlBuilder.go index d5c3ff4..61b1948 100644 --- a/uiHtmlBuilder.go +++ b/uiHtmlBuilder.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "fmt" "html/template" "io" @@ -9,8 +8,7 @@ import ( ) type htmlBuilder struct { - w io.Writer - buf bytes.Buffer + w io.Writer } func newHtmlBuilder(w io.Writer) *htmlBuilder { @@ -20,10 +18,7 @@ func newHtmlBuilder(w io.Writer) *htmlBuilder { } func (h *htmlBuilder) getWriter() io.Writer { - if h.w != nil { - return h.w - } - return &h.buf + return h.w } func (h *htmlBuilder) Write(p []byte) (int, error) { @@ -34,22 +29,6 @@ func (h *htmlBuilder) WriteString(s string) (int, error) { return io.WriteString(h.getWriter(), s) } -func (h *htmlBuilder) Read(p []byte) (int, error) { - return h.buf.Read(p) -} - -func (h *htmlBuilder) String() string { - return h.buf.String() -} - -func (h *htmlBuilder) Bytes() []byte { - return h.buf.Bytes() -} - -func (h *htmlBuilder) html() template.HTML { - return template.HTML(h.String()) -} - func (h *htmlBuilder) write(s string) { _, _ = h.WriteString(s) } diff --git a/ui_test.go b/ui_test.go index 603ce06..344f2a6 100644 --- a/ui_test.go +++ b/ui_test.go @@ -1,7 +1,7 @@ package main import ( - "html/template" + "bytes" "io" "os" "strings" @@ -14,7 +14,6 @@ import ( var _ io.Writer = &htmlBuilder{} var _ io.StringWriter = &htmlBuilder{} -var _ io.Reader = &htmlBuilder{} func Test_renderPostTax(t *testing.T) { app := &goBlog{ @@ -30,13 +29,16 @@ func Test_renderPostTax(t *testing.T) { }, } - var hb htmlBuilder - app.renderPostTax(&hb, p, app.cfg.Blogs["default"]) - res := hb.html() - _, err := goquery.NewDocumentFromReader(strings.NewReader(string(res))) + buf := &bytes.Buffer{} + hb := newHtmlBuilder(buf) + + app.renderPostTax(hb, p, app.cfg.Blogs["default"]) + res := buf.String() + + _, err := goquery.NewDocumentFromReader(strings.NewReader(res)) require.NoError(t, err) - assert.Equal(t, template.HTML("

Tags: Bar, Foo

"), res) + assert.Equal(t, "

Tags: Bar, Foo

", res) } func Test_renderOldContentWarning(t *testing.T) { @@ -51,13 +53,16 @@ func Test_renderOldContentWarning(t *testing.T) { Published: "2018-01-01", } - var hb htmlBuilder - app.renderOldContentWarning(&hb, p, app.cfg.Blogs["default"]) - res := hb.html() - _, err := goquery.NewDocumentFromReader(strings.NewReader(string(res))) + buf := &bytes.Buffer{} + hb := newHtmlBuilder(buf) + + app.renderOldContentWarning(hb, p, app.cfg.Blogs["default"]) + res := buf.String() + + _, err := goquery.NewDocumentFromReader(strings.NewReader(res)) require.NoError(t, err) - assert.Equal(t, template.HTML("⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed."), res) + assert.Equal(t, "⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed.", res) } func Test_renderInteractions(t *testing.T) { @@ -112,16 +117,19 @@ func Test_renderInteractions(t *testing.T) { err = app.db.approveWebmentionId(2) require.NoError(t, err) - var hb htmlBuilder - app.renderInteractions(&hb, app.cfg.Blogs["default"], "https://example.com/testpost1") - res := hb.html() - _, err = goquery.NewDocumentFromReader(strings.NewReader(string(res))) + buf := &bytes.Buffer{} + hb := newHtmlBuilder(buf) + + app.renderInteractions(hb, app.cfg.Blogs["default"], "https://example.com/testpost1") + res := buf.Bytes() + + _, err = goquery.NewDocumentFromReader(bytes.NewReader(res)) require.NoError(t, err) expected, err := os.ReadFile("testdata/interactionstest.html") require.NoError(t, err) - assert.Equal(t, template.HTML(expected), res) + assert.Equal(t, expected, res) } func Test_renderAuthor(t *testing.T) { @@ -134,11 +142,14 @@ func Test_renderAuthor(t *testing.T) { _ = app.initDatabase(false) app.initComponents(false) - var hb htmlBuilder - app.renderAuthor(&hb) - res := hb.html() - _, err := goquery.NewDocumentFromReader(strings.NewReader(string(res))) + buf := &bytes.Buffer{} + hb := newHtmlBuilder(buf) + + app.renderAuthor(hb) + res := buf.String() + + _, err := goquery.NewDocumentFromReader(strings.NewReader(res)) require.NoError(t, err) - assert.Equal(t, template.HTML("
John Doe
"), res) + assert.Equal(t, "
John Doe
", res) }