Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-04-29 - Cache Large Map Allocations at Instance Level
**Learning:** Returning a large `map[string]string` from a function like `colors.ColorMap()` on every plugin invocation creates significant repeated memory allocation overhead, particularly in parallel rendering operations like `StatusLine.Render()` which executes 5-10 times.
**Action:** Always cache large reference types (like maps) at the struct instance level (e.g. within `StatusLine`) during initialization to avoid redundant allocations in hot loops. Add fallback initialization `if sl.colorMap == nil` for backward compatibility with manual struct initialization in tests.
9 changes: 8 additions & 1 deletion internal/statusline/statusline.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type StatusLine struct {
isIdle bool
bashPlugins []plugin.Plugin // Cached discovered bash plugins
bashPluginsOnce sync.Once
colorMap map[string]string // Cached color map to avoid allocations
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To safely handle lazy initialization of colorMap in a concurrent environment, add a sync.Once field to the StatusLine struct. This is necessary because runPlugin is called from multiple goroutines in renderLine, and the current lazy initialization logic is not thread-safe.

	colorMap        map[string]string // Cached color map to avoid allocations
	colorMapOnce    sync.Once

}

// New creates a new StatusLine renderer
Expand All @@ -45,6 +46,7 @@ func New(input Input, cfg config.Config) *StatusLine {
pluginManager: plugin.NewManager(),
nativePlugins: plugins.NewRegistry(),
isIdle: checkIsIdle(input.SessionID),
colorMap: colors.ColorMap(),
}
}

Expand Down Expand Up @@ -612,6 +614,11 @@ func (sl *StatusLine) getConfigBool(key string, defVal bool) bool {
}

func (sl *StatusLine) runPlugin(name string) string {
// Initialize colorMap if nil (for tests that construct StatusLine manually)
if sl.colorMap == nil {
sl.colorMap = colors.ColorMap()
}
Comment on lines +618 to +620
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This lazy initialization is subject to a data race. Since runPlugin is executed within parallel goroutines (via renderLine), multiple threads may attempt to check and initialize sl.colorMap simultaneously if the struct was manually initialized (a scenario explicitly mentioned in the PR description for tests). Using sl.colorMapOnce ensures that the initialization happens exactly once and is thread-safe.

Suggested change
if sl.colorMap == nil {
sl.colorMap = colors.ColorMap()
}
sl.colorMapOnce.Do(func() {
if sl.colorMap == nil {
sl.colorMap = colors.ColorMap()
}
})


// Build plugin input
input := plugin.Input{
Prism: plugin.PrismContext{
Expand All @@ -634,7 +641,7 @@ func (sl *StatusLine) runPlugin(name string) string {
ContextWindowSize: sl.input.Context.ContextWindow,
},
Config: sl.getPluginConfig(name),
Colors: colors.ColorMap(),
Colors: sl.colorMap,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

By caching and reusing sl.colorMap, the same map instance is now shared across all plugin invocations. While this reduces allocations, it means that if any native plugin modifies the Colors map, it will affect other plugins and subsequent renders. Ensure that all native plugins treat the Colors map as read-only, or consider providing a copy if mutation is a risk.

}

// Try native plugin first (much faster - no subprocess)
Expand Down
Loading