perf(template): use pool instead of instances

This commit is contained in:
Jan De Dobbeleer
2025-07-29 13:42:00 +02:00
committed by Jan De Dobbeleer
parent 13b857109c
commit 386971a3e9
24 changed files with 187 additions and 153 deletions
+1 -3
View File
@@ -21,9 +21,7 @@
"problemMatcher": "$go",
"args": [
"build",
"-v",
"-ldflags",
"\"-s -w -X 'github.com/jandedobbeleer/oh-my-posh/src/build.Version=development-$(git --no-pager log -1 --pretty=%h-%s)' -extldflags '-static'\""
"-v"
]
},
{
+2 -4
View File
@@ -132,10 +132,8 @@ func (c Ansi) ResolveTemplate() Ansi {
return emptyColor
}
tmpl := &template.Text{
Template: string(c),
Context: nil,
}
tmpl := template.New(string(c), nil)
defer tmpl.Release()
text, err := tmpl.Render()
if err != nil {
+2 -3
View File
@@ -84,9 +84,8 @@ func (cfg *Config) getPalette() color.Palette {
return cfg.Palette
}
tmpl := &template.Text{
Template: cfg.Palettes.Template,
}
tmpl := template.New(cfg.Palettes.Template, nil)
defer tmpl.Release()
key, err := tmpl.Render()
if err != nil {
+4 -8
View File
@@ -23,11 +23,9 @@ import (
type SegmentStyle string
func (s *SegmentStyle) resolve(context any) SegmentStyle {
txtTemplate := &template.Text{
Context: context,
}
txtTemplate := template.New(string(*s), context)
defer txtTemplate.Release()
txtTemplate.Template = string(*s)
value, err := txtTemplate.Render()
// default to Plain
@@ -318,10 +316,8 @@ func (segment *Segment) string() string {
segment.Template = segment.writer.Template()
}
tmpl := &template.Text{
Template: segment.Template,
Context: segment.writer,
}
tmpl := template.New(segment.Template, segment.writer)
defer tmpl.Release()
text, err := tmpl.Render()
if err != nil {
+8 -15
View File
@@ -16,7 +16,6 @@ var cycle *color.Cycle = &color.Cycle{}
type Engine struct {
Env runtime.Environment
templateCache *template.Text
Config *config.Config
activeSegment *config.Segment
previousActiveSegment *config.Segment
@@ -42,17 +41,6 @@ const (
PREVIEW = "preview"
)
func (e *Engine) templateText(text string, context any) *template.Text {
if e.templateCache == nil {
e.templateCache = &template.Text{}
}
e.templateCache.Template = text
e.templateCache.Context = context
return e.templateCache
}
func (e *Engine) write(text string) {
// Grow capacity proactively if needed
if e.prompt.Cap() < e.prompt.Len()+len(text) {
@@ -120,7 +108,8 @@ func (e *Engine) pwd() {
}
// Allow template logic to define when to enable the PWD (when supported)
tmpl := e.templateText(e.Config.PWD, nil)
tmpl := template.New(e.Config.PWD, nil)
defer tmpl.Release()
pwdType, err := tmpl.Render()
if err != nil || pwdType == "" {
return
@@ -167,7 +156,9 @@ func (e *Engine) shouldFill(filler string, padLength int) (string, bool) {
return "", false
}
tmpl := e.templateText(filler, e)
tmpl := template.New(filler, e)
defer tmpl.Release()
var err error
if filler, err = tmpl.Render(); err != nil {
return "", false
@@ -188,7 +179,9 @@ func (e *Engine) shouldFill(filler string, padLength int) (string, bool) {
}
func (e *Engine) getTitleTemplateText() string {
tmpl := e.templateText(e.Config.ConsoleTitleTemplate, nil)
tmpl := template.New(e.Config.ConsoleTitleTemplate, nil)
defer tmpl.Release()
if text, err := tmpl.Render(); err == nil {
return text
}
+4 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/color"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
)
@@ -55,7 +56,9 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
}
}
tmpl := e.templateText(getTemplate(prompt.Template), nil)
tmpl := template.New(getTemplate(prompt.Template), nil)
defer tmpl.Release()
promptText, err := tmpl.Render()
if err != nil {
promptText = err.Error()
+2 -3
View File
@@ -31,13 +31,12 @@ func (h *HTTP) Enabled() bool {
method := h.props.GetString(METHOD, "GET")
tmpl := &template.Text{
Template: url,
}
tmpl := template.New(url, nil)
if resolved, err := tmpl.Render(); err == nil {
url = resolved
}
tmpl.Release()
result, err := h.getResult(url, method)
if err != nil {
+2 -4
View File
@@ -296,10 +296,8 @@ func (l *language) buildVersionURL() {
return
}
tmpl := &template.Text{
Template: versionURLTemplate,
Context: l.version,
}
tmpl := template.New(versionURLTemplate, l.version)
defer tmpl.Release()
url, err := tmpl.Render()
if err != nil {
+8 -16
View File
@@ -276,10 +276,8 @@ func (pt *Path) setStyle() {
}
// make sure we resolve all templates
tmpl := &template.Text{
Template: pt.Path,
Context: pt,
}
tmpl := template.New(pt.Path, pt)
defer tmpl.Release()
if text, err := tmpl.Render(); err == nil {
pt.Path = text
@@ -292,10 +290,8 @@ func (pt *Path) getMaxWidth() int {
return 0
}
tmpl := &template.Text{
Template: width,
Context: pt,
}
tmpl := template.New(width, pt)
defer tmpl.Release()
text, err := tmpl.Render()
if err != nil {
@@ -324,10 +320,8 @@ func (pt *Path) getFolderSeparator() string {
return separator
}
tmpl := &template.Text{
Template: separatorTemplate,
Context: pt,
}
tmpl := template.New(separatorTemplate, pt)
defer tmpl.Release()
text, err := tmpl.Render()
if err != nil {
@@ -658,10 +652,8 @@ func (pt *Path) setMappedLocations() {
continue
}
tmpl := &template.Text{
Template: key,
Context: pt,
}
tmpl := template.New(key, pt)
defer tmpl.Release()
location, err := tmpl.Render()
if err != nil {
+2 -4
View File
@@ -32,10 +32,8 @@ func renderTemplateNoTrimSpace(env *mock.Environment, segmentTemplate string, co
}
template.Init(env, nil, nil)
tmpl := &template.Text{
Template: segmentTemplate,
Context: context,
}
tmpl := template.New(segmentTemplate, context)
defer tmpl.Release()
text, err := tmpl.Render()
if err != nil {
+2 -4
View File
@@ -161,10 +161,8 @@ func (s *scm) formatBranch(branch string) string {
return branch
}
tmpl := &template.Text{
Template: branchTemplate,
Context: struct{ Branch string }{Branch: branch},
}
tmpl := template.New(branchTemplate, struct{ Branch string }{Branch: branch})
defer tmpl.Release()
text, err := tmpl.Render()
if err != nil {
+12 -11
View File
@@ -16,10 +16,9 @@ const (
type Status struct {
base
template *template.Text
String string
Meaning string
Error bool
String string
Meaning string
Error bool
}
func (s *Status) Template() string {
@@ -42,19 +41,19 @@ func (s *Status) Enabled() bool {
func (s *Status) formatStatus(status int, pipeStatus string) string {
statusTemplate := s.props.GetString(StatusTemplate, "{{ .Code }}")
s.template = &template.Text{
Template: statusTemplate,
}
if status != 0 {
s.Error = true
}
if pipeStatus == "" {
s.template.Context = s
if text, err := s.template.Render(); err == nil {
tmpl := template.New(statusTemplate, s)
defer tmpl.Release()
if text, err := tmpl.Render(); err == nil {
return text
}
return strconv.Itoa(status)
}
@@ -90,8 +89,10 @@ func (s *Status) formatStatus(status int, pipeStatus string) string {
context.Code = code
s.template.Context = context
text, err := s.template.Render()
tmpl := template.New(statusTemplate, context)
defer tmpl.Release()
text, err := tmpl.Render()
if err != nil {
write(codeStr)
continue
+2 -4
View File
@@ -56,9 +56,7 @@ func (w *Wakatime) setAPIData() error {
func (w *Wakatime) getURL() (string, error) {
url := w.props.GetString(URL, "")
tmpl := &template.Text{
Template: url,
Context: w,
}
tmpl := template.New(url, w)
defer tmpl.Release()
return tmpl.Render()
}
+2 -4
View File
@@ -28,10 +28,7 @@ func TestGlob(t *testing.T) {
Init(env, nil, nil)
for _, tc := range cases {
tmpl := &Text{
Template: tc.Template,
Context: nil,
}
tmpl := New(tc.Template, nil)
text, err := tmpl.Render()
if tc.ShouldError {
@@ -40,5 +37,6 @@ func TestGlob(t *testing.T) {
}
assert.Equal(t, tc.Expected, text, tc.Case)
tmpl.Release()
}
}
+5
View File
@@ -24,6 +24,7 @@ var (
shell string
env runtime.Environment
knownFields sync.Map
textPool *generics.Pool[*Text]
)
func Init(environment runtime.Environment, vars maps.Simple[any], aliases *maps.Config) {
@@ -38,6 +39,10 @@ func Init(environment runtime.Environment, vars maps.Simple[any], aliases *maps.
}
})
textPool = generics.NewPool(func() *Text {
return &Text{}
})
if Cache != nil {
return
}
+5 -8
View File
@@ -28,10 +28,7 @@ func TestUrl(t *testing.T) {
Init(env, nil, nil)
for _, tc := range cases {
tmpl := &Text{
Template: tc.Template,
Context: nil,
}
tmpl := New(tc.Template, nil)
text, err := tmpl.Render()
if tc.ShouldError {
@@ -40,6 +37,7 @@ func TestUrl(t *testing.T) {
}
assert.Equal(t, tc.Expected, text, tc.Case)
tmpl.Release()
}
}
@@ -53,12 +51,11 @@ func TestPath(t *testing.T) {
}
for _, tc := range cases {
tmpl := &Text{
Template: tc.Template,
Context: nil,
}
tmpl := New(tc.Template, nil)
text, _ := tmpl.Render()
assert.Equal(t, tc.Expected, text, tc.Case)
tmpl.Release()
}
}
+6 -8
View File
@@ -37,13 +37,12 @@ func (l List) Join(context any) string {
return ""
}
txtTemplate := &Text{
Context: context,
}
txtTemplate := New("", context)
defer txtTemplate.Release()
var buffer strings.Builder
for _, tmpl := range l {
txtTemplate.Template = tmpl
txtTemplate.template = tmpl
value, err := txtTemplate.Render()
if err != nil || len(strings.TrimSpace(value)) == 0 {
continue
@@ -60,12 +59,11 @@ func (l List) FirstMatch(context any, defaultValue string) string {
return defaultValue
}
txtTemplate := &Text{
Context: context,
}
txtTemplate := New("", context)
defer txtTemplate.Release()
for _, tmpl := range l {
txtTemplate.Template = tmpl
txtTemplate.template = tmpl
value, err := txtTemplate.Render()
if err != nil || len(strings.TrimSpace(value)) == 0 {
continue
+2 -4
View File
@@ -18,10 +18,7 @@ func TestHResult(t *testing.T) {
}
for _, tc := range cases {
tmpl := &Text{
Template: tc.Template,
Context: nil,
}
tmpl := New(tc.Template, nil)
text, err := tmpl.Render()
if tc.ShouldError {
@@ -30,5 +27,6 @@ func TestHResult(t *testing.T) {
}
assert.Equal(t, tc.Expected, text, tc.Case)
tmpl.Release()
}
}
+56
View File
@@ -0,0 +1,56 @@
package template
import (
"testing"
"github.com/jandedobbeleer/oh-my-posh/src/cache"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
"github.com/stretchr/testify/assert"
)
func TestTextPool(t *testing.T) {
env := new(mock.Environment)
env.On("Shell").Return("foo")
Cache = new(cache.Template)
Init(env, nil, nil)
// Test that New() returns a Text instance
text1 := New("Hello {{ .Name }}", map[string]any{"Name": "World"})
assert.NotNil(t, text1)
// Test rendering
result, err := text1.Render()
assert.NoError(t, err)
assert.Equal(t, "Hello World", result)
// Release back to pool
text1.Release()
// Verify fields are reset (we can't check them directly since they're unexported)
// But we can test by creating a new instance and verifying it works
text2 := New("", nil)
assert.NotNil(t, text2)
// Test empty template
result2, err := text2.Render()
assert.NoError(t, err)
assert.Equal(t, "", result2)
text2.Release()
}
func TestTextPoolFallback(t *testing.T) {
// Test fallback when pool is not initialized
originalPool := textPool
textPool = nil
text := New("test", nil)
assert.NotNil(t, text)
// Should work without panic
text.Release()
// Restore
textPool = originalPool
}
+2 -2
View File
@@ -20,7 +20,7 @@ type context struct {
}
func (c *context) init(t *Text) {
c.Data = t.Context
c.Data = t.context
c.Getenv = env.Getenv
c.Template = *Cache
}
@@ -41,7 +41,7 @@ func (t *renderer) release() {
}
func (t *renderer) execute(text *Text) (string, error) {
tmpl, err := t.template.Parse(text.Template)
tmpl, err := t.template.Parse(text.template)
if err != nil {
log.Error(err)
return "", errors.New(InvalidTemplate)
+2 -4
View File
@@ -25,10 +25,7 @@ func TestRoundSeconds(t *testing.T) {
}
for _, tc := range cases {
tmpl := &Text{
Template: tc.Template,
Context: nil,
}
tmpl := New(tc.Template, nil)
text, err := tmpl.Render()
if tc.ShouldError {
@@ -37,5 +34,6 @@ func TestRoundSeconds(t *testing.T) {
}
assert.Equal(t, tc.Expected, text, tc.Case)
tmpl.Release()
}
}
+2 -4
View File
@@ -22,10 +22,7 @@ func TestTrunc(t *testing.T) {
}
for _, tc := range cases {
tmpl := &Text{
Template: tc.Template,
Context: nil,
}
tmpl := New(tc.Template, nil)
text, err := tmpl.Render()
if tc.ShouldError {
@@ -34,6 +31,7 @@ func TestTrunc(t *testing.T) {
}
assert.Equal(t, tc.Expected, text, tc.Case)
tmpl.Release()
}
}
+34 -11
View File
@@ -12,15 +12,38 @@ import (
)
type Text struct {
Context Data
Template string
context Data
template string
}
// New returns a Text instance from the pool with the given template and context
func New(template string, context any) *Text {
if textPool == nil {
// Fallback if pool is not initialized yet
return &Text{context: context, template: template}
}
text := textPool.Get()
text.template = template
text.context = context
return text
}
// Release resets the Text instance and returns it to the pool
func (t *Text) Release() {
t.context = nil
t.template = ""
if textPool != nil {
textPool.Put(t)
}
}
func (t *Text) Render() (string, error) {
defer log.Trace(time.Now(), t.Template)
defer log.Trace(time.Now(), t.template)
if !strings.Contains(t.Template, "{{") || !strings.Contains(t.Template, "}}") {
return t.Template, nil
if !strings.Contains(t.template, "{{") || !strings.Contains(t.template, "}}") {
return t.template, nil
}
t.patchTemplate()
@@ -33,18 +56,18 @@ func (t *Text) Render() (string, error) {
func (t *Text) patchTemplate() {
fields := &fields{}
fields.init(t.Context)
fields.init(t.context)
var result, property string
var inProperty, inTemplate bool
for i, char := range t.Template {
for i, char := range t.template {
// define start or end of template
if !inTemplate && char == '{' {
if i-1 >= 0 && rune(t.Template[i-1]) == '{' {
if i-1 >= 0 && rune(t.template[i-1]) == '{' {
inTemplate = true
}
} else if inTemplate && char == '}' {
if i-1 >= 0 && rune(t.Template[i-1]) == '}' {
if i-1 >= 0 && rune(t.template[i-1]) == '}' {
inTemplate = false
}
}
@@ -120,9 +143,9 @@ func (t *Text) patchTemplate() {
}
// return the result and remaining unresolved property
t.Template = result + property
t.template = result + property
log.Debug(t.Template)
log.Debug(t.template)
}
type fields struct {
+20 -28
View File
@@ -157,10 +157,8 @@ func TestRenderTemplate(t *testing.T) {
}
for _, tc := range cases {
tmpl := &Text{
Template: tc.Template,
Context: tc.Context,
}
tmpl := New(tc.Template, tc.Context)
defer tmpl.Release()
env := new(mock.Environment)
env.On("Shell").Return("foo")
@@ -253,10 +251,8 @@ func TestRenderTemplateEnvVar(t *testing.T) {
}
Init(env, nil, nil)
tmpl := &Text{
Template: tc.Template,
Context: tc.Context,
}
tmpl := New(tc.Template, tc.Context)
defer tmpl.Release()
text, err := tmpl.Render()
if tc.ShouldError {
@@ -347,20 +343,20 @@ func TestPatchTemplate(t *testing.T) {
Init(env, nil, nil)
for _, tc := range cases {
tmpl := &Text{
Template: tc.Template,
Context: map[string]any{
"OS": true,
"World": true,
"WorldTrend": "chaos",
"Working": true,
"Staging": true,
"CPU": true,
},
context := map[string]any{
"OS": true,
"World": true,
"WorldTrend": "chaos",
"Working": true,
"Staging": true,
"CPU": true,
}
tmpl := New(tc.Template, context)
tmpl.patchTemplate()
assert.Equal(t, tc.Expected, tmpl.Template, tc.Case)
assert.Equal(t, tc.Expected, tmpl.template, tc.Case)
tmpl.Release()
}
}
@@ -376,13 +372,11 @@ func TestPatchTemplateStruct(t *testing.T) {
Cache = new(cache.Template)
Init(env, nil, nil)
tmpl := &Text{
Template: "{{ .Hello }}",
Context: Foo{},
}
tmpl := New("{{ .Hello }}", Foo{})
tmpl.patchTemplate()
assert.Equal(t, "{{ .Data.Hello }}", tmpl.Template)
assert.Equal(t, "{{ .Data.Hello }}", tmpl.template)
tmpl.Release()
}
func TestSegmentContains(t *testing.T) {
@@ -406,10 +400,8 @@ func TestSegmentContains(t *testing.T) {
Init(env, nil, nil)
for _, tc := range cases {
tmpl := &Text{
Template: tc.Template,
Context: nil,
}
tmpl := New(tc.Template, nil)
defer tmpl.Release()
text, _ := tmpl.Render()
assert.Equal(t, tc.Expected, text, tc.Case)