mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
Write to rotating ContentReWriter in transformer chain
This commit adds the interface ContentReWriter in the tranformer chain. This is backed by two pooled byte buffers, alternating between being the reader or the writer. This keeps the performance characteristic of the old implementation, but in a thread safe way. Fixes #911 Benchmark old vs new: benchmark old ns/op new ns/op delta BenchmarkAbsURL 17614 17384 -1.31% BenchmarkXMLAbsURL 9431 9248 -1.94% benchmark old allocs new allocs delta BenchmarkAbsURL 24 28 +16.67% BenchmarkXMLAbsURL 12 14 +16.67% benchmark old bytes new bytes delta BenchmarkAbsURL 3295 3424 +3.92% BenchmarkXMLAbsURL 1954 1987 +1.69%
This commit is contained in:
parent
9688ed2585
commit
98ee69bce2
5 changed files with 112 additions and 47 deletions
|
@ -17,8 +17,8 @@ func initAbsURLReplacer(baseURL string) {
|
||||||
func AbsURL(absURL string) (trs []link, err error) {
|
func AbsURL(absURL string) (trs []link, err error) {
|
||||||
initAbsURLReplacer(absURL)
|
initAbsURLReplacer(absURL)
|
||||||
|
|
||||||
trs = append(trs, func(content []byte) []byte {
|
trs = append(trs, func(rw ContentReWriter) {
|
||||||
return ar.replaceInHTML(content)
|
ar.replaceInHTML(rw)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,8 @@ func AbsURL(absURL string) (trs []link, err error) {
|
||||||
func AbsURLInXML(absURL string) (trs []link, err error) {
|
func AbsURLInXML(absURL string) (trs []link, err error) {
|
||||||
initAbsURLReplacer(absURL)
|
initAbsURLReplacer(absURL)
|
||||||
|
|
||||||
trs = append(trs, func(content []byte) []byte {
|
trs = append(trs, func(rw ContentReWriter) {
|
||||||
return ar.replaceInXML(content)
|
ar.replaceInXML(rw)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
bp "github.com/spf13/hugo/bufferpool"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
@ -33,7 +33,7 @@ type contentlexer struct {
|
||||||
state stateFunc
|
state stateFunc
|
||||||
prefixLookup *prefixes
|
prefixLookup *prefixes
|
||||||
|
|
||||||
b *bytes.Buffer
|
w io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
type stateFunc func(*contentlexer) stateFunc
|
type stateFunc func(*contentlexer) stateFunc
|
||||||
|
@ -95,7 +95,7 @@ func (l *contentlexer) match(r rune) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *contentlexer) emit() {
|
func (l *contentlexer) emit() {
|
||||||
l.b.Write(l.content[l.start:l.pos])
|
l.w.Write(l.content[l.start:l.pos])
|
||||||
l.start = l.pos
|
l.start = l.pos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ func checkCandidate(l *contentlexer) {
|
||||||
l.emit()
|
l.emit()
|
||||||
}
|
}
|
||||||
l.pos += len(m.match)
|
l.pos += len(m.match)
|
||||||
l.b.Write(m.replacement)
|
l.w.Write(m.replacement)
|
||||||
l.start = l.pos
|
l.start = l.pos
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -159,7 +159,6 @@ func (l *contentlexer) replace() {
|
||||||
}
|
}
|
||||||
l.width = width
|
l.width = width
|
||||||
l.pos += l.width
|
l.pos += l.width
|
||||||
|
|
||||||
if r == ' ' {
|
if r == ' ' {
|
||||||
l.prefixLookup.ms = matchStateWhitespace
|
l.prefixLookup.ms = matchStateWhitespace
|
||||||
} else if l.prefixLookup.ms != matchStateNone {
|
} else if l.prefixLookup.ms != matchStateNone {
|
||||||
|
@ -177,18 +176,16 @@ func (l *contentlexer) replace() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func doReplace(content []byte, matchers []absURLMatcher) []byte {
|
func doReplace(rw ContentReWriter, matchers []absURLMatcher) {
|
||||||
b := bp.GetBuffer()
|
|
||||||
defer bp.PutBuffer(b)
|
|
||||||
|
|
||||||
lexer := &contentlexer{content: content,
|
lexer := &contentlexer{
|
||||||
b: b,
|
content: rw.Content(),
|
||||||
|
w: rw,
|
||||||
prefixLookup: &prefixes{pr: mainPrefixRunes},
|
prefixLookup: &prefixes{pr: mainPrefixRunes},
|
||||||
matchers: matchers}
|
matchers: matchers}
|
||||||
|
|
||||||
lexer.replace()
|
lexer.replace()
|
||||||
|
|
||||||
return b.Bytes()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type absURLReplacer struct {
|
type absURLReplacer struct {
|
||||||
|
@ -229,10 +226,10 @@ func newAbsURLReplacer(baseURL string) *absURLReplacer {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (au *absURLReplacer) replaceInHTML(content []byte) []byte {
|
func (au *absURLReplacer) replaceInHTML(rw ContentReWriter) {
|
||||||
return doReplace(content, au.htmlMatchers)
|
doReplace(rw, au.htmlMatchers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (au *absURLReplacer) replaceInXML(content []byte) []byte {
|
func (au *absURLReplacer) replaceInXML(rw ContentReWriter) {
|
||||||
return doReplace(content, au.xmlMatchers)
|
doReplace(rw, au.xmlMatchers)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package transform
|
package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"bytes"
|
||||||
|
|
||||||
bp "github.com/spf13/hugo/bufferpool"
|
bp "github.com/spf13/hugo/bufferpool"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type trans func([]byte) []byte
|
type trans func(rw ContentReWriter)
|
||||||
|
|
||||||
type link trans
|
type link trans
|
||||||
|
|
||||||
|
@ -20,17 +20,62 @@ func NewEmptyTransforms() []link {
|
||||||
return make([]link, 0, 20)
|
return make([]link, 0, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chain) Apply(w io.Writer, r io.Reader) (err error) {
|
// ContentReWriter is an interface that enables rotation
|
||||||
buffer := bp.GetBuffer()
|
// of pooled buffers in the transformer chain.
|
||||||
defer bp.PutBuffer(buffer)
|
type ContentReWriter interface {
|
||||||
|
Content() []byte
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
buffer.ReadFrom(r)
|
// Implements ContentReWriter
|
||||||
b := buffer.Bytes()
|
// Content is read from the from-buffer,
|
||||||
for _, tr := range *c {
|
// and rewritten to to the to-buffer.
|
||||||
b = tr(b)
|
type fromToBuffer struct {
|
||||||
|
from *bytes.Buffer
|
||||||
|
to *bytes.Buffer
|
||||||
}
|
}
|
||||||
buffer.Reset()
|
|
||||||
buffer.Write(b)
|
func (ft fromToBuffer) Write(p []byte) (n int, err error) {
|
||||||
buffer.WriteTo(w)
|
return ft.to.Write(p)
|
||||||
return
|
}
|
||||||
|
|
||||||
|
func (ft fromToBuffer) Content() []byte {
|
||||||
|
return ft.from.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chain) Apply(w io.Writer, r io.Reader) error {
|
||||||
|
|
||||||
|
b1 := bp.GetBuffer()
|
||||||
|
defer bp.PutBuffer(b1)
|
||||||
|
|
||||||
|
b1.ReadFrom(r)
|
||||||
|
|
||||||
|
if len(*c) == 0 {
|
||||||
|
b1.WriteTo(w)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b2 := bp.GetBuffer()
|
||||||
|
defer bp.PutBuffer(b2)
|
||||||
|
|
||||||
|
fb := &fromToBuffer{from: b1, to: b2}
|
||||||
|
|
||||||
|
for i, tr := range *c {
|
||||||
|
if i > 0 {
|
||||||
|
if fb.from == b1 {
|
||||||
|
fb.from = b2
|
||||||
|
fb.to = b1
|
||||||
|
fb.to.Reset()
|
||||||
|
} else {
|
||||||
|
fb.from = b1
|
||||||
|
fb.to = b2
|
||||||
|
fb.to.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr(fb)
|
||||||
|
}
|
||||||
|
|
||||||
|
fb.to.WriteTo(w)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"github.com/spf13/hugo/helpers"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -54,6 +55,35 @@ func TestChainZeroTransformers(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChaingMultipleTransformers(t *testing.T) {
|
||||||
|
f1 := func(rw ContentReWriter) {
|
||||||
|
rw.Write(bytes.Replace(rw.Content(), []byte("f1"), []byte("f1r"), -1))
|
||||||
|
}
|
||||||
|
f2 := func(rw ContentReWriter) {
|
||||||
|
rw.Write(bytes.Replace(rw.Content(), []byte("f2"), []byte("f2r"), -1))
|
||||||
|
}
|
||||||
|
f3 := func(rw ContentReWriter) {
|
||||||
|
rw.Write(bytes.Replace(rw.Content(), []byte("f3"), []byte("f3r"), -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
f4 := func(rw ContentReWriter) {
|
||||||
|
rw.Write(bytes.Replace(rw.Content(), []byte("f4"), []byte("f4r"), -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := NewChain(f1, f2, f3, f4)
|
||||||
|
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
if err := tr.Apply(out, helpers.StringToReader("Test: f4 f3 f1 f2 f1 The End.")); err != nil {
|
||||||
|
t.Errorf("Multi transformer chain returned an error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "Test: f4r f3r f1r f2r f1r The End."
|
||||||
|
|
||||||
|
if string(out.Bytes()) != expected {
|
||||||
|
t.Errorf("Expected %s got %s", expected, string(out.Bytes()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkAbsURL(b *testing.B) {
|
func BenchmarkAbsURL(b *testing.B) {
|
||||||
absURL, _ := AbsURL("http://base")
|
absURL, _ := AbsURL("http://base")
|
||||||
tr := NewChain(absURL...)
|
tr := NewChain(absURL...)
|
||||||
|
|
|
@ -2,29 +2,22 @@ package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
jww "github.com/spf13/jwalterweatherman"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LiveReloadInject(content []byte) (injected []byte) {
|
func LiveReloadInject(rw ContentReWriter) {
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
jww.ERROR.Println("Recovered in LiveReloadInject", r)
|
|
||||||
injected = content
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
match := []byte("</body>")
|
match := []byte("</body>")
|
||||||
port := viper.GetString("port")
|
port := viper.GetString("port")
|
||||||
replace := []byte(`<script>document.write('<script src="http://'
|
replace := []byte(`<script>document.write('<script src="http://'
|
||||||
+ (location.host || 'localhost').split(':')[0]
|
+ (location.host || 'localhost').split(':')[0]
|
||||||
+ ':` + port + `/livereload.js?mindelay=10"></'
|
+ ':` + port + `/livereload.js?mindelay=10"></'
|
||||||
+ 'script>')</script></body>`)
|
+ 'script>')</script></body>`)
|
||||||
newcontent := bytes.Replace(content, match, replace, -1)
|
newcontent := bytes.Replace(rw.Content(), match, replace, -1)
|
||||||
|
|
||||||
if len(newcontent) == len(content) {
|
if len(newcontent) == len(rw.Content()) {
|
||||||
match := []byte("</BODY>")
|
match := []byte("</BODY>")
|
||||||
newcontent = bytes.Replace(content, match, replace, -1)
|
newcontent = bytes.Replace(rw.Content(), match, replace, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newcontent
|
rw.Write(newcontent)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue