Skip to content

Commit

Permalink
Create segments in parallel
Browse files Browse the repository at this point in the history
Powerline being integrated directly in users shell, the perceived
performance and time to render and display the prompt is a critical
part of the program.

As discussed in #121, there are some integrations that requires
executing sub-programs and hence takes longer to render.

In order to improve the perceived speed, generate the segments data in
parallel, before sorting them to ensure the order the user required on
the command line.

After my testing, I am seeing that using modules
user,host,ssh,docker,kube,cwd,perms,git improves from 120 ms without the
update to 30 ms with the update.
  • Loading branch information
tjamet committed Dec 12, 2019
1 parent c9f514f commit 162cab3
Show file tree
Hide file tree
Showing 34 changed files with 261 additions and 159 deletions.
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"io/ioutil"
"os"
"strings"

pwl "github.com/justjanne/powerline-go/powerline"
)

type alignment int
Expand Down Expand Up @@ -88,7 +90,7 @@ func getValidCwd() string {
return cwd
}

var modules = map[string]func(*powerline){
var modules = map[string]func(*powerline) []pwl.Segment{
"aws": segmentAWS,
"cwd": segmentCwd,
"docker": segmentDocker,
Expand Down
59 changes: 50 additions & 9 deletions powerline.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"strconv"
"strings"
"sync"

pwl "github.com/justjanne/powerline-go/powerline"
"github.com/mattn/go-runewidth"
Expand Down Expand Up @@ -43,6 +44,11 @@ type powerline struct {
appendEastAsianPadding int
}

type prioritizedSegments struct {
i int
segs []pwl.Segment
}

func newPowerline(args args, cwd string, priorities map[string]int, align alignment) *powerline {
p := new(powerline)
p.args = args
Expand Down Expand Up @@ -82,17 +88,48 @@ func newPowerline(args args, cwd string, priorities map[string]int, align alignm
} else {
mods = *args.ModulesRight
}
for _, module := range strings.Split(mods, ",") {
elem, ok := modules[module]
if ok {
elem(p)
} else {
if ok := segmentPlugin(p, module); !ok {
println("Module not found: " + module)
initSegments(p, strings.Split(mods, ","))

return p
}

func initSegments(p *powerline, mods []string) {
orderedSegments := map[int][]pwl.Segment{}
c := make(chan prioritizedSegments, len(mods))
wg := sync.WaitGroup{}
for i, module := range mods {
wg.Add(1)
go func(w *sync.WaitGroup, i int, module string, c chan prioritizedSegments) {
elem, ok := modules[module]
if ok {
c <- prioritizedSegments{
i: i,
segs: elem(p),
}
} else {
s, ok := segmentPlugin(p, module)
if ok {
c <- prioritizedSegments{
i: i,
segs: s,
}
} else {
println("Module not found: " + module)
}
}
wg.Done()
}(&wg, i, module, c)
}
wg.Wait()
close(c)
for s := range c {
orderedSegments[s.i] = s.segs
}
for i := 0; i < len(mods); i++ {
for _, seg := range orderedSegments[i] {
p.appendSegment(seg.Name, seg)
}
}
return p
}

func (p *powerline) color(prefix string, code uint8) string {
Expand Down Expand Up @@ -128,7 +165,11 @@ func (p *powerline) appendSegment(origin string, segment pwl.Segment) {
priority, _ := p.priorities[origin]
segment.Priority += priority
segment.Width = segment.ComputeWidth(*p.args.Condensed)
p.Segments[p.curSegment] = append(p.Segments[p.curSegment], segment)
if segment.NewLine {
p.newRow()
} else {
p.Segments[p.curSegment] = append(p.Segments[p.curSegment], segment)
}
}

func (p *powerline) newRow() {
Expand Down
3 changes: 3 additions & 0 deletions powerline/powerline.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

// Segment describes an information to display on the command line prompt
type Segment struct {
Name string
// Content is the text to be displayed on the command line prompt
Content string
// Foreground is the text color (see https://misc.flogisoft.com/bash/tip_colors_and_formatting#background1)
Expand All @@ -21,6 +22,8 @@ type Segment struct {
// HideSeparators indicated not to display any separator with next segment.
HideSeparators bool
Width int
// NewLine defines a newline segment to break the powerline in multi lines
NewLine bool
}

func (s Segment) ComputeWidth(condensed bool) int {
Expand Down
8 changes: 5 additions & 3 deletions segment-aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import (
"os"
)

func segmentAWS(p *powerline) {
func segmentAWS(p *powerline) []pwl.Segment {
profile := os.Getenv("AWS_PROFILE")
region := os.Getenv("AWS_DEFAULT_REGION")
if profile != "" {
var r string
if region != "" {
r = " (" + region + ")"
}
p.appendSegment("aws", pwl.Segment{
return []pwl.Segment{{
Name: "aws",
Content: profile + r,
Foreground: p.theme.AWSFg,
Background: p.theme.AWSBg,
})
}}
}
return []pwl.Segment{}
}
12 changes: 7 additions & 5 deletions segment-cwd.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func getColor(p *powerline, pathSegment pathSegment, isLastDir bool) (uint8, uin
return p.theme.PathFg, p.theme.PathBg, false
}

func segmentCwd(p *powerline) {
func segmentCwd(p *powerline) (segments []pwl.Segment) {
cwd := p.cwd
if cwd == "" {
cwd, _ = os.LookupEnv("PWD")
Expand All @@ -168,7 +168,8 @@ func segmentCwd(p *powerline) {
cwd = "~" + cwd[len(home):]
}

p.appendSegment("cwd", pwl.Segment{
segments = append(segments, pwl.Segment{
Name: "cwd",
Content: cwd,
Foreground: p.theme.CwdFg,
Background: p.theme.PathBg,
Expand Down Expand Up @@ -222,12 +223,13 @@ func segmentCwd(p *powerline) {
}
}

origin := "cwd-path"
segment.Name = "cwd-path"
if isLastDir {
origin = "cwd"
segment.Name = "cwd"
}

p.appendSegment(origin, segment)
segments = append(segments, segment)
}
}
return segments
}
8 changes: 5 additions & 3 deletions segment-docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
pwl "github.com/justjanne/powerline-go/powerline"
)

func segmentDocker(p *powerline) {
func segmentDocker(p *powerline) []pwl.Segment {
var docker string
dockerMachineName, _ := os.LookupEnv("DOCKER_MACHINE_NAME")
dockerHost, _ := os.LookupEnv("DOCKER_HOST")
Expand All @@ -22,10 +22,12 @@ func segmentDocker(p *powerline) {
}

if docker != "" {
p.appendSegment("docker", pwl.Segment{
return []pwl.Segment{{
Name: "docker",
Content: docker,
Foreground: p.theme.DockerMachineFg,
Background: p.theme.DockerMachineBg,
})
}}
}
return []pwl.Segment{}
}
8 changes: 5 additions & 3 deletions segment-dotenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"os"
)

func segmentDotEnv(p *powerline) {
func segmentDotEnv(p *powerline) []pwl.Segment {
files := []string{".env", ".envrc"}
dotEnv := false
for _, file := range files {
Expand All @@ -16,10 +16,12 @@ func segmentDotEnv(p *powerline) {
}
}
if dotEnv {
p.appendSegment("dotenv", pwl.Segment{
return []pwl.Segment{{
Name: "dotenv",
Content: "\u2235",
Foreground: p.theme.DotEnvFg,
Background: p.theme.DotEnvBg,
})
}}
}
return []pwl.Segment{}
}
22 changes: 12 additions & 10 deletions segment-duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ const (
hours int64 = minutes * 60
)

func segmentDuration(p *powerline) {
func segmentDuration(p *powerline) []pwl.Segment {
if p.args.Duration == nil || *p.args.Duration == "" {
p.appendSegment("duration", pwl.Segment{
return []pwl.Segment{{
Name: "duration",
Content: "No duration",
Foreground: p.theme.DurationFg,
Background: p.theme.DurationBg,
})
return
}}
}

durationValue := strings.Trim(*p.args.Duration, "'\"")
Expand All @@ -44,16 +44,16 @@ func segmentDuration(p *powerline) {
durationFloat, err := strconv.ParseFloat(durationValue, 64)
durationMinFloat, _ := strconv.ParseFloat(durationMinValue, 64)
if err != nil {
p.appendSegment("duration", pwl.Segment{
return []pwl.Segment{{
Name: "duration",
Content: fmt.Sprintf("Failed to convert '%s' to a number", *p.args.Duration),
Foreground: p.theme.DurationFg,
Background: p.theme.DurationBg,
})
return
}}
}

if durationFloat < durationMinFloat {
return
return []pwl.Segment{}
}

duration := time.Duration(durationFloat * float64(time.Second.Nanoseconds()))
Expand Down Expand Up @@ -88,10 +88,12 @@ func segmentDuration(p *powerline) {
content = fmt.Sprintf("%d\u00B5s", ns/microseconds)
}

p.appendSegment("duration", pwl.Segment{
return []pwl.Segment{{
Name: "duration",
Content: content,
Foreground: p.theme.DurationFg,
Background: p.theme.DurationBg,
})
}}
}
return []pwl.Segment{}
}
8 changes: 5 additions & 3 deletions segment-exitcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,20 @@ func getMeaningFromExitCode(exitCode int) string {
}
}

func segmentExitCode(p *powerline) {
func segmentExitCode(p *powerline) []pwl.Segment {
var meaning string
if *p.args.PrevError != 0 {
if *p.args.NumericExitCodes {
meaning = strconv.Itoa(*p.args.PrevError)
} else {
meaning = getMeaningFromExitCode(*p.args.PrevError)
}
p.appendSegment("exit", pwl.Segment{
return []pwl.Segment{{
Name: "exit",
Content: meaning,
Foreground: p.theme.CmdFailedFg,
Background: p.theme.CmdFailedBg,
})
}}
}
return []pwl.Segment{}
}
41 changes: 23 additions & 18 deletions segment-git.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,27 @@ func (r repoStats) dirty() bool {
return r.untracked+r.notStaged+r.staged+r.conflicted > 0
}

func addRepoStatsSegment(p *powerline, nChanges int, symbol string, foreground uint8, background uint8) {
func addRepoStatsSegment(nChanges int, symbol string, foreground uint8, background uint8) []pwl.Segment {
if nChanges > 0 {
p.appendSegment("git-status", pwl.Segment{
return []pwl.Segment{{
Name: "git-status",
Content: fmt.Sprintf("%d%s", nChanges, symbol),
Foreground: foreground,
Background: background,
})
}}
}
return []pwl.Segment{}
}

func (r repoStats) addToPowerline(p *powerline) {
addRepoStatsSegment(p, r.ahead, p.symbolTemplates.RepoAhead, p.theme.GitAheadFg, p.theme.GitAheadBg)
addRepoStatsSegment(p, r.behind, p.symbolTemplates.RepoBehind, p.theme.GitBehindFg, p.theme.GitBehindBg)
addRepoStatsSegment(p, r.staged, p.symbolTemplates.RepoStaged, p.theme.GitStagedFg, p.theme.GitStagedBg)
addRepoStatsSegment(p, r.notStaged, p.symbolTemplates.RepoNotStaged, p.theme.GitNotStagedFg, p.theme.GitNotStagedBg)
addRepoStatsSegment(p, r.untracked, p.symbolTemplates.RepoUntracked, p.theme.GitUntrackedFg, p.theme.GitUntrackedBg)
addRepoStatsSegment(p, r.conflicted, p.symbolTemplates.RepoConflicted, p.theme.GitConflictedFg, p.theme.GitConflictedBg)
addRepoStatsSegment(p, r.stashed, p.symbolTemplates.RepoStashed, p.theme.GitStashedFg, p.theme.GitStashedBg)
func (r repoStats) GitSegments(p *powerline) (segments []pwl.Segment) {
segments = append(segments, addRepoStatsSegment(r.ahead, p.symbolTemplates.RepoAhead, p.theme.GitAheadFg, p.theme.GitAheadBg)...)
segments = append(segments, addRepoStatsSegment(r.behind, p.symbolTemplates.RepoBehind, p.theme.GitBehindFg, p.theme.GitBehindBg)...)
segments = append(segments, addRepoStatsSegment(r.staged, p.symbolTemplates.RepoStaged, p.theme.GitStagedFg, p.theme.GitStagedBg)...)
segments = append(segments, addRepoStatsSegment(r.notStaged, p.symbolTemplates.RepoNotStaged, p.theme.GitNotStagedFg, p.theme.GitNotStagedBg)...)
segments = append(segments, addRepoStatsSegment(r.untracked, p.symbolTemplates.RepoUntracked, p.theme.GitUntrackedFg, p.theme.GitUntrackedBg)...)
segments = append(segments, addRepoStatsSegment(r.conflicted, p.symbolTemplates.RepoConflicted, p.theme.GitConflictedFg, p.theme.GitConflictedBg)...)
segments = append(segments, addRepoStatsSegment(r.stashed, p.symbolTemplates.RepoStashed, p.theme.GitStashedFg, p.theme.GitStashedBg)...)
return
}

var branchRegex = regexp.MustCompile(`^## (?P<local>\S+?)(\.{3}(?P<remote>\S+?)( \[(ahead (?P<ahead>\d+)(, )?)?(behind (?P<behind>\d+))?])?)?$`)
Expand Down Expand Up @@ -124,21 +127,21 @@ func parseGitStats(status []string) repoStats {
return stats
}

func segmentGit(p *powerline) {
func segmentGit(p *powerline) []pwl.Segment {
if len(p.ignoreRepos) > 0 {
out, err := runGitCommand("git", "rev-parse", "--show-toplevel")
if err != nil {
return
return []pwl.Segment{}
}
out = strings.TrimSpace(out)
if p.ignoreRepos[out] {
return
return []pwl.Segment{}
}
}

out, err := runGitCommand("git", "status", "--porcelain", "-b", "--ignore-submodules")
if err != nil {
return
return []pwl.Segment{}
}

status := strings.Split(out, "\n")
Expand Down Expand Up @@ -172,10 +175,12 @@ func segmentGit(p *powerline) {
stats.stashed = len(strings.Split(out, "\n")) - 1
}

p.appendSegment("git-branch", pwl.Segment{
segments := []pwl.Segment{{
Name: "git-branch",
Content: branch,
Foreground: foreground,
Background: background,
})
stats.addToPowerline(p)
}}
segments = append(segments, stats.GitSegments(p)...)
return segments
}
Loading

0 comments on commit 162cab3

Please sign in to comment.