Use more strings.Builder instead of += for building queries etc. (more efficient)

This commit is contained in:
Jan-Lukas Else 2021-07-23 17:26:14 +02:00
parent b7f578cf2f
commit 6429d64b0a
9 changed files with 161 additions and 135 deletions

View File

@ -8,15 +8,12 @@ import (
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unsafe"
"github.com/araddon/dateparse" "github.com/araddon/dateparse"
"github.com/dgraph-io/ristretto" "github.com/dgraph-io/ristretto"
servertiming "github.com/mitchellh/go-server-timing"
"golang.org/x/sync/singleflight" "golang.org/x/sync/singleflight"
) )
@ -36,23 +33,9 @@ func (a *goBlog) initCache() (err error) {
return nil return nil
} }
a.cache.c, err = ristretto.NewCache(&ristretto.Config{ a.cache.c, err = ristretto.NewCache(&ristretto.Config{
NumCounters: 5000, NumCounters: 40 * 1000, // 4000 items when full with 5 KB items -> x10 = 40.000
MaxCost: 20000000, // 20 MB MaxCost: 20 * 1000 * 1000, // 20 MB
BufferItems: 16, BufferItems: 64, // recommended
Cost: func(value interface{}) (cost int64) {
if cacheItem, ok := value.(*cacheItem); ok {
cost = int64(binary.Size(cacheItem.body)) // Byte size of body
for h, hv := range cacheItem.header {
cost += int64(binary.Size([]byte(h))) // Byte size of header name
for _, hvi := range hv {
cost += int64(binary.Size([]byte(hvi))) // byte size of each header value item
}
}
} else {
cost = int64(unsafe.Sizeof(cacheItem))
}
return cost
},
}) })
return return
} }
@ -110,24 +93,21 @@ func (c *cache) cacheMiddleware(next http.Handler) http.Handler {
} }
func cacheKey(r *http.Request) string { func cacheKey(r *http.Request) string {
key := cacheURLString(r.URL) var buf strings.Builder
// Special cases // Special cases
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest { if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
key = "as-" + key buf.WriteString("as-")
} }
if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed { if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed {
key = "tor-" + key buf.WriteString("tor-")
} }
return key // Add cache URL
} _, _ = buf.WriteString(r.URL.EscapedPath())
if q := r.URL.Query(); len(q) > 0 {
func cacheURLString(u *url.URL) string {
var buf strings.Builder
_, _ = buf.WriteString(u.EscapedPath())
if q := u.Query(); len(q) > 0 {
_ = buf.WriteByte('?') _ = buf.WriteByte('?')
_, _ = buf.WriteString(q.Encode()) _, _ = buf.WriteString(q.Encode())
} }
// Return string
return buf.String() return buf.String()
} }
@ -152,13 +132,21 @@ type cacheItem struct {
body []byte body []byte
} }
// Calculate byte size of cache item using size of body and header
func (ci *cacheItem) cost() int64 {
var headerBuf strings.Builder
_ = ci.header.Write(&headerBuf)
headerSize := int64(binary.Size(headerBuf.String()))
bodySize := int64(binary.Size(ci.body))
return headerSize + bodySize
}
func (c *cache) getCache(key string, next http.Handler, r *http.Request) (item *cacheItem) { func (c *cache) getCache(key string, next http.Handler, r *http.Request) (item *cacheItem) {
if rItem, ok := c.c.Get(key); ok { if rItem, ok := c.c.Get(key); ok {
item = rItem.(*cacheItem) item = rItem.(*cacheItem)
} }
if item == nil { if item == nil {
// No cache available // No cache available
servertiming.FromContext(r.Context()).NewMetric("cm")
// Remove problematic headers // Remove problematic headers
r.Header.Del("If-Modified-Since") r.Header.Del("If-Modified-Since")
r.Header.Del("If-Unmodified-Since") r.Header.Del("If-Unmodified-Since")
@ -202,16 +190,13 @@ func (c *cache) getCache(key string, next http.Handler, r *http.Request) (item *
body: body, body: body,
} }
// Save cache // Save cache
if cch := item.header.Get("Cache-Control"); !strings.Contains(cch, "no-store") && !strings.Contains(cch, "private") && !strings.Contains(cch, "no-cache") { if cch := item.header.Get("Cache-Control"); !containsStrings(cch, "no-store", "private", "no-cache") {
if exp == 0 { if exp == 0 {
c.c.Set(key, item, 0) c.c.Set(key, item, item.cost())
} else { } else {
ttl := time.Duration(exp) * time.Second c.c.SetWithTTL(key, item, item.cost(), time.Duration(exp)*time.Second)
c.c.SetWithTTL(key, item, 0, ttl)
} }
} }
} else {
servertiming.FromContext(r.Context()).NewMetric("c")
} }
return item return item
} }

View File

@ -105,13 +105,13 @@ type commentsRequestConfig struct {
} }
func buildCommentsQuery(config *commentsRequestConfig) (query string, args []interface{}) { func buildCommentsQuery(config *commentsRequestConfig) (query string, args []interface{}) {
args = []interface{}{} var queryBuilder strings.Builder
query = "select id, target, name, website, comment from comments order by id desc" queryBuilder.WriteString("select id, target, name, website, comment from comments order by id desc")
if config.limit != 0 || config.offset != 0 { if config.limit != 0 || config.offset != 0 {
query += " limit @limit offset @offset" queryBuilder.WriteString(" limit @limit offset @offset")
args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset)) args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset))
} }
return return queryBuilder.String(), args
} }
func (db *database) getComments(config *commentsRequestConfig) ([]*comment, error) { func (db *database) getComments(config *commentsRequestConfig) ([]*comment, error) {

View File

@ -546,15 +546,18 @@ const blogContextKey contextKey = "blog"
const pathContextKey contextKey = "httpPath" const pathContextKey contextKey = "httpPath"
func (a *goBlog) refreshCSPDomains() { func (a *goBlog) refreshCSPDomains() {
a.cspDomains = "" var cspBuilder strings.Builder
if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" { if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" {
if u, err := url.Parse(mp.MediaURL); err == nil { if u, err := url.Parse(mp.MediaURL); err == nil {
a.cspDomains += " " + u.Hostname() cspBuilder.WriteByte(' ')
cspBuilder.WriteString(u.Hostname())
} }
} }
if len(a.cfg.Server.CSPDomains) > 0 { if len(a.cfg.Server.CSPDomains) > 0 {
a.cspDomains += " " + strings.Join(a.cfg.Server.CSPDomains, " ") cspBuilder.WriteByte(' ')
cspBuilder.WriteString(strings.Join(a.cfg.Server.CSPDomains, " "))
} }
a.cspDomains = cspBuilder.String()
} }
const cspHeader = "Content-Security-Policy" const cspHeader = "Content-Security-Policy"

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -54,13 +55,13 @@ type notificationsRequestConfig struct {
} }
func buildNotificationsQuery(config *notificationsRequestConfig) (query string, args []interface{}) { func buildNotificationsQuery(config *notificationsRequestConfig) (query string, args []interface{}) {
args = []interface{}{} var queryBuilder strings.Builder
query = "select id, time, text from notifications order by id desc" queryBuilder.WriteString("select id, time, text from notifications order by id desc")
if config.limit != 0 || config.offset != 0 { if config.limit != 0 || config.offset != 0 {
query += " limit @limit offset @offset" queryBuilder.WriteString(" limit @limit offset @offset")
args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset)) args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset))
} }
return return queryBuilder.String(), args
} }
func (db *database) getNotifications(config *notificationsRequestConfig) ([]*notification, error) { func (db *database) getNotifications(config *notificationsRequestConfig) ([]*notification, error) {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"reflect" "reflect"
"strconv" "strconv"
@ -30,8 +31,8 @@ type post struct {
Priority int Priority int
// Not persisted // Not persisted
Slug string Slug string
renderCache sync.Map renderCache map[bool]template.HTML
renderMutex sync.Mutex renderMutex sync.RWMutex
} }
type postStatus string type postStatus string

View File

@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"text/template" "text/template"
"time" "time"
@ -245,113 +246,125 @@ type postsRequestConfig struct {
} }
func buildPostsQuery(c *postsRequestConfig, selection string) (query string, args []interface{}) { func buildPostsQuery(c *postsRequestConfig, selection string) (query string, args []interface{}) {
args = []interface{}{} var queryBuilder strings.Builder
table := "posts" // Selection
queryBuilder.WriteString("select ")
queryBuilder.WriteString(selection)
queryBuilder.WriteString(" from ")
// Table
if c.search != "" { if c.search != "" {
table = "posts_fts(@search)" queryBuilder.WriteString("posts_fts(@search)")
args = append(args, sql.Named("search", c.search)) args = append(args, sql.Named("search", c.search))
} else {
queryBuilder.WriteString("posts")
} }
var wheres []string // Filter
queryBuilder.WriteString(" where 1")
if c.path != "" { if c.path != "" {
wheres = append(wheres, "path = @path") queryBuilder.WriteString(" and path = @path")
args = append(args, sql.Named("path", c.path)) args = append(args, sql.Named("path", c.path))
} }
if c.status != "" && c.status != statusNil { if c.status != "" && c.status != statusNil {
wheres = append(wheres, "status = @status") queryBuilder.WriteString(" and status = @status")
args = append(args, sql.Named("status", c.status)) args = append(args, sql.Named("status", c.status))
} }
if c.blog != "" { if c.blog != "" {
wheres = append(wheres, "blog = @blog") queryBuilder.WriteString(" and blog = @blog")
args = append(args, sql.Named("blog", c.blog)) args = append(args, sql.Named("blog", c.blog))
} }
if c.parameter != "" { if c.parameter != "" {
if c.parameterValue != "" { if c.parameterValue != "" {
wheres = append(wheres, "path in (select path from post_parameters where parameter = @param and value = @paramval)") queryBuilder.WriteString(" and path in (select path from post_parameters where parameter = @param and value = @paramval)")
args = append(args, sql.Named("param", c.parameter), sql.Named("paramval", c.parameterValue)) args = append(args, sql.Named("param", c.parameter), sql.Named("paramval", c.parameterValue))
} else { } else {
wheres = append(wheres, "path in (select path from post_parameters where parameter = @param and length(coalesce(value, '')) > 0)") queryBuilder.WriteString(" and path in (select path from post_parameters where parameter = @param and length(coalesce(value, '')) > 0)")
args = append(args, sql.Named("param", c.parameter)) args = append(args, sql.Named("param", c.parameter))
} }
} }
if c.taxonomy != nil && len(c.taxonomyValue) > 0 { if c.taxonomy != nil && len(c.taxonomyValue) > 0 {
wheres = append(wheres, "path in (select path from post_parameters where parameter = @taxname and lower(value) = lower(@taxval))") queryBuilder.WriteString(" and path in (select path from post_parameters where parameter = @taxname and lower(value) = lower(@taxval))")
args = append(args, sql.Named("taxname", c.taxonomy.Name), sql.Named("taxval", c.taxonomyValue)) args = append(args, sql.Named("taxname", c.taxonomy.Name), sql.Named("taxval", c.taxonomyValue))
} }
if len(c.sections) > 0 { if len(c.sections) > 0 {
ws := "section in (" queryBuilder.WriteString(" and section in (")
for i, section := range c.sections { for i, section := range c.sections {
if i > 0 { if i > 0 {
ws += ", " queryBuilder.WriteString(", ")
} }
named := fmt.Sprintf("section%v", i) named := "section" + strconv.Itoa(i)
ws += "@" + named queryBuilder.WriteByte('@')
queryBuilder.WriteString(named)
args = append(args, sql.Named(named, section)) args = append(args, sql.Named(named, section))
} }
ws += ")" queryBuilder.WriteByte(')')
wheres = append(wheres, ws)
} }
if c.publishedYear != 0 { if c.publishedYear != 0 {
wheres = append(wheres, "substr(tolocal(published), 1, 4) = @publishedyear") queryBuilder.WriteString(" and substr(tolocal(published), 1, 4) = @publishedyear")
args = append(args, sql.Named("publishedyear", fmt.Sprintf("%0004d", c.publishedYear))) args = append(args, sql.Named("publishedyear", fmt.Sprintf("%0004d", c.publishedYear)))
} }
if c.publishedMonth != 0 { if c.publishedMonth != 0 {
wheres = append(wheres, "substr(tolocal(published), 6, 2) = @publishedmonth") queryBuilder.WriteString(" and substr(tolocal(published), 6, 2) = @publishedmonth")
args = append(args, sql.Named("publishedmonth", fmt.Sprintf("%02d", c.publishedMonth))) args = append(args, sql.Named("publishedmonth", fmt.Sprintf("%02d", c.publishedMonth)))
} }
if c.publishedDay != 0 { if c.publishedDay != 0 {
wheres = append(wheres, "substr(tolocal(published), 9, 2) = @publishedday") queryBuilder.WriteString(" and substr(tolocal(published), 9, 2) = @publishedday")
args = append(args, sql.Named("publishedday", fmt.Sprintf("%02d", c.publishedDay))) args = append(args, sql.Named("publishedday", fmt.Sprintf("%02d", c.publishedDay)))
} }
if len(wheres) > 0 { // Order
table += " where " + strings.Join(wheres, " and ") queryBuilder.WriteString(" order by ")
}
sorting := " order by published desc"
if c.randomOrder { if c.randomOrder {
sorting = " order by random()" queryBuilder.WriteString("random()")
} else if c.priorityOrder { } else if c.priorityOrder {
sorting = " order by priority desc, published desc" queryBuilder.WriteString("priority desc, published desc")
} else {
queryBuilder.WriteString("published desc")
} }
table += sorting // Limit & Offset
if c.limit != 0 || c.offset != 0 { if c.limit != 0 || c.offset != 0 {
table += " limit @limit offset @offset" queryBuilder.WriteString(" limit @limit offset @offset")
args = append(args, sql.Named("limit", c.limit), sql.Named("offset", c.offset)) args = append(args, sql.Named("limit", c.limit), sql.Named("offset", c.offset))
} }
query = "select " + selection + " from " + table return queryBuilder.String(), args
return query, args
} }
func (d *database) loadPostParameters(posts []*post, parameters ...string) (err error) { func (d *database) loadPostParameters(posts []*post, parameters ...string) (err error) {
if len(posts) == 0 {
return nil
}
// Build query
var sqlArgs []interface{} var sqlArgs []interface{}
// Parameter filter var queryBuilder strings.Builder
paramFilter := "" queryBuilder.WriteString("select path, parameter, value from post_parameters where")
// Paths
queryBuilder.WriteString(" path in (")
for i, p := range posts {
if i > 0 {
queryBuilder.WriteString(", ")
}
named := "path" + strconv.Itoa(i)
queryBuilder.WriteByte('@')
queryBuilder.WriteString(named)
sqlArgs = append(sqlArgs, sql.Named(named, p.Path))
}
queryBuilder.WriteByte(')')
// Parameters
if len(parameters) > 0 { if len(parameters) > 0 {
paramFilter = " and parameter in (" queryBuilder.WriteString(" and parameter in (")
for i, p := range parameters { for i, p := range parameters {
if i > 0 { if i > 0 {
paramFilter += ", " queryBuilder.WriteString(", ")
} }
named := fmt.Sprintf("param%v", i) named := "param" + strconv.Itoa(i)
paramFilter += "@" + named queryBuilder.WriteByte('@')
queryBuilder.WriteString(named)
sqlArgs = append(sqlArgs, sql.Named(named, p)) sqlArgs = append(sqlArgs, sql.Named(named, p))
} }
paramFilter += ")" queryBuilder.WriteByte(')')
}
// Path filter
pathFilter := ""
if len(posts) > 0 {
pathFilter = " and path in ("
for i, p := range posts {
if i > 0 {
pathFilter += ", "
}
named := fmt.Sprintf("path%v", i)
pathFilter += "@" + named
sqlArgs = append(sqlArgs, sql.Named(named, p.Path))
}
pathFilter += ")"
} }
// Order
queryBuilder.WriteString(" order by id")
// Query // Query
rows, err := d.query("select path, parameter, value from post_parameters where 1 = 1"+paramFilter+pathFilter+" order by id", sqlArgs...) rows, err := d.query(queryBuilder.String(), sqlArgs...)
if err != nil { if err != nil {
return err return err
} }
@ -519,16 +532,18 @@ group by name;
func (db *database) usesOfMediaFile(names ...string) (counts map[string]int, err error) { func (db *database) usesOfMediaFile(names ...string) (counts map[string]int, err error) {
sqlArgs := []interface{}{dbNoCache} sqlArgs := []interface{}{dbNoCache}
nameValues := "" var nameValues strings.Builder
for i, n := range names { for i, n := range names {
if i > 0 { if i > 0 {
nameValues += ", " nameValues.WriteString(", ")
} }
named := fmt.Sprintf("name%v", i) named := "name" + strconv.Itoa(i)
nameValues += fmt.Sprintf("(@%s)", named) nameValues.WriteString("(@")
nameValues.WriteString(named)
nameValues.WriteByte(')')
sqlArgs = append(sqlArgs, sql.Named(named, n)) sqlArgs = append(sqlArgs, sql.Named(named, n))
} }
rows, err := db.query(fmt.Sprintf(mediaUseSql, nameValues), sqlArgs...) rows, err := db.query(fmt.Sprintf(mediaUseSql, nameValues.String()), sqlArgs...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -48,11 +48,25 @@ func firstPostParameter(p *post, parameter string) string {
} }
func (a *goBlog) postHtml(p *post, absolute bool) template.HTML { func (a *goBlog) postHtml(p *post, absolute bool) template.HTML {
p.renderMutex.RLock()
// Check cache
if r, ok := p.renderCache[absolute]; ok && r != "" {
p.renderMutex.RUnlock()
return r
}
p.renderMutex.RUnlock()
// No cache, build it...
p.renderMutex.Lock() p.renderMutex.Lock()
defer p.renderMutex.Unlock() defer p.renderMutex.Unlock()
// Check cache // Build HTML
if r, ok := p.renderCache.Load(absolute); ok && r != nil { var htmlBuilder strings.Builder
return r.(template.HTML) // Add audio to the top
if audio, ok := p.Parameters["audio"]; ok && len(audio) > 0 {
for _, a := range audio {
htmlBuilder.WriteString(`<audio controls preload=none><source src="`)
htmlBuilder.WriteString(a)
htmlBuilder.WriteString(`"/></audio>`)
}
} }
// Render markdown // Render markdown
htmlContent, err := a.renderMarkdown(p.Content, absolute) htmlContent, err := a.renderMarkdown(p.Content, absolute)
@ -60,26 +74,23 @@ func (a *goBlog) postHtml(p *post, absolute bool) template.HTML {
log.Fatal(err) log.Fatal(err)
return "" return ""
} }
htmlContentStr := string(htmlContent) htmlBuilder.Write(htmlContent)
// Add audio to the top
if audio, ok := p.Parameters["audio"]; ok && len(audio) > 0 {
audios := ""
for _, a := range audio {
audios += fmt.Sprintf(`<audio controls preload=none><source src="%s"/></audio>`, a)
}
htmlContentStr = audios + htmlContentStr
}
// Add links to the bottom // Add links to the bottom
if link, ok := p.Parameters["link"]; ok && len(link) > 0 { if link, ok := p.Parameters["link"]; ok && len(link) > 0 {
links := ""
for _, l := range link { for _, l := range link {
links += fmt.Sprintf(`<p><a class=u-bookmark-of href="%s" target=_blank rel=noopener>%s</a></p>`, l, l) htmlBuilder.WriteString(`<p><a class=u-bookmark-of href="`)
htmlBuilder.WriteString(l)
htmlBuilder.WriteString(`" target=_blank rel=noopener>`)
htmlBuilder.WriteString(l)
htmlBuilder.WriteString(`</a></p>`)
} }
htmlContentStr += links
} }
// Cache // Cache
html := template.HTML(htmlContentStr) html := template.HTML(htmlBuilder.String())
p.renderCache.Store(absolute, html) if p.renderCache == nil {
p.renderCache = map[bool]template.HTML{}
}
p.renderCache[absolute] = html
return html return html
} }

View File

@ -247,3 +247,12 @@ func defaultIfEmpty(s, d string) string {
} }
return d return d
} }
func containsStrings(s string, subStrings ...string) bool {
for _, ss := range subStrings {
if strings.Contains(s, ss) {
return true
}
}
return false
}

View File

@ -135,37 +135,38 @@ type webmentionsRequestConfig struct {
} }
func buildWebmentionsQuery(config *webmentionsRequestConfig) (query string, args []interface{}) { func buildWebmentionsQuery(config *webmentionsRequestConfig) (query string, args []interface{}) {
args = []interface{}{} var queryBuilder strings.Builder
filter := "" queryBuilder.WriteString("select id, source, target, created, title, content, author, status from webmentions ")
if config != nil { if config != nil {
filter = "where 1 = 1" queryBuilder.WriteString("where 1")
if config.target != "" { if config.target != "" {
filter += " and lower(target) = lower(@target)" queryBuilder.WriteString(" and lower(target) = lower(@target)")
args = append(args, sql.Named("target", config.target)) args = append(args, sql.Named("target", config.target))
} }
if config.status != "" { if config.status != "" {
filter += " and status = @status" queryBuilder.WriteString(" and status = @status")
args = append(args, sql.Named("status", config.status)) args = append(args, sql.Named("status", config.status))
} }
if config.sourcelike != "" { if config.sourcelike != "" {
filter += " and lower(source) like @sourcelike" queryBuilder.WriteString(" and lower(source) like @sourcelike")
args = append(args, sql.Named("sourcelike", "%"+strings.ToLower(config.sourcelike)+"%")) args = append(args, sql.Named("sourcelike", "%"+strings.ToLower(config.sourcelike)+"%"))
} }
if config.id != 0 { if config.id != 0 {
filter += " and id = @id" queryBuilder.WriteString(" and id = @id")
args = append(args, sql.Named("id", config.id)) args = append(args, sql.Named("id", config.id))
} }
} }
order := "desc" queryBuilder.WriteString(" order by created ")
if config.asc { if config.asc {
order = "asc" queryBuilder.WriteString("asc")
} else {
queryBuilder.WriteString("desc")
} }
query = "select id, source, target, created, title, content, author, status from webmentions " + filter + " order by created " + order
if config.limit != 0 || config.offset != 0 { if config.limit != 0 || config.offset != 0 {
query += " limit @limit offset @offset" queryBuilder.WriteString(" limit @limit offset @offset")
args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset)) args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset))
} }
return query, args return queryBuilder.String(), args
} }
func (db *database) getWebmentions(config *webmentionsRequestConfig) ([]*mention, error) { func (db *database) getWebmentions(config *webmentionsRequestConfig) ([]*mention, error) {