Improve rendering performance

This commit almost doubles the segment rendering performance. This is
accomplished by caching a lot of data like highlighting groups, moving
some calculations out of loops, and by performing less function calls
overall.

When a width is specified the main speed improvement comes from avoiding
rendering the raw segments over and over until the statusline is short
enough. Instead, the raw rendering is stored as a segment property and
the combined length of all these renderings is used when removing
low-priority segments instead. This results in a maximum of two
rendering passes.

Some "less pythonic" solutions have been chosen some places for
performance reasons, e.g. joining strings instead of appending and
joining lists.

Overall this commit appears to make the performance equal or better than
the legacy vimscript implementation. Later optimizations (in particular
finding another method than remove() for removing low-priority segments)
may make this version of Powerline far superior both in terms of
functionality and performance.
This commit is contained in:
Kim Silkebækken 2012-11-21 09:57:16 +01:00
parent 6c5316a058
commit f4e3d01d07
4 changed files with 134 additions and 141 deletions

View File

@ -176,13 +176,13 @@ def statusline(winnr):
stl = re.sub('(\w+)\%(?![-{()<=#*%])', '\\1%%', stl)
# Create highlighting groups
for group, hl in renderer.hl_groups.items():
if vim_funcs['hlexists'](group):
for idx, hl in renderer.hl_groups.items():
if vim_funcs['hlexists'](hl['name']):
# Only create hl group if it doesn't already exist
continue
vim.command('hi {group} ctermfg={ctermfg} guifg={guifg} guibg={guibg} ctermbg={ctermbg} cterm={attr} gui={attr}'.format(
group=group,
group=hl['name'],
ctermfg=hl['ctermfg'],
guifg='#{0:06x}'.format(hl['guifg']) if hl['guifg'] != 'NONE' else 'NONE',
ctermbg=hl['ctermbg'],

View File

@ -1,54 +1,42 @@
def cterm_to_hex(cterm_color):
'''Translate a cterm color index into the corresponding hex/RGB color.
'''
color_dict = {
16: 0x000000, 17: 0x00005f, 18: 0x000087, 19: 0x0000af, 20: 0x0000d7, 21: 0x0000ff,
22: 0x005f00, 23: 0x005f5f, 24: 0x005f87, 25: 0x005faf, 26: 0x005fd7, 27: 0x005fff,
28: 0x008700, 29: 0x00875f, 30: 0x008787, 31: 0x0087af, 32: 0x0087d7, 33: 0x0087ff,
34: 0x00af00, 35: 0x00af5f, 36: 0x00af87, 37: 0x00afaf, 38: 0x00afd7, 39: 0x00afff,
40: 0x00d700, 41: 0x00d75f, 42: 0x00d787, 43: 0x00d7af, 44: 0x00d7d7, 45: 0x00d7ff,
46: 0x00ff00, 47: 0x00ff5f, 48: 0x00ff87, 49: 0x00ffaf, 50: 0x00ffd7, 51: 0x00ffff,
52: 0x5f0000, 53: 0x5f005f, 54: 0x5f0087, 55: 0x5f00af, 56: 0x5f00d7, 57: 0x5f00ff,
58: 0x5f5f00, 59: 0x5f5f5f, 60: 0x5f5f87, 61: 0x5f5faf, 62: 0x5f5fd7, 63: 0x5f5fff,
64: 0x5f8700, 65: 0x5f875f, 66: 0x5f8787, 67: 0x5f87af, 68: 0x5f87d7, 69: 0x5f87ff,
70: 0x5faf00, 71: 0x5faf5f, 72: 0x5faf87, 73: 0x5fafaf, 74: 0x5fafd7, 75: 0x5fafff,
76: 0x5fd700, 77: 0x5fd75f, 78: 0x5fd787, 79: 0x5fd7af, 80: 0x5fd7d7, 81: 0x5fd7ff,
82: 0x5fff00, 83: 0x5fff5f, 84: 0x5fff87, 85: 0x5fffaf, 86: 0x5fffd7, 87: 0x5fffff,
88: 0x870000, 89: 0x87005f, 90: 0x870087, 91: 0x8700af, 92: 0x8700d7, 93: 0x8700ff,
94: 0x875f00, 95: 0x875f5f, 96: 0x875f87, 97: 0x875faf, 98: 0x875fd7, 99: 0x875fff,
100: 0x878700, 101: 0x87875f, 102: 0x878787, 103: 0x8787af, 104: 0x8787d7, 105: 0x8787ff,
106: 0x87af00, 107: 0x87af5f, 108: 0x87af87, 109: 0x87afaf, 110: 0x87afd7, 111: 0x87afff,
112: 0x87d700, 113: 0x87d75f, 114: 0x87d787, 115: 0x87d7af, 116: 0x87d7d7, 117: 0x87d7ff,
118: 0x87ff00, 119: 0x87ff5f, 120: 0x87ff87, 121: 0x87ffaf, 122: 0x87ffd7, 123: 0x87ffff,
124: 0xaf0000, 125: 0xaf005f, 126: 0xaf0087, 127: 0xaf00af, 128: 0xaf00d7, 129: 0xaf00ff,
130: 0xaf5f00, 131: 0xaf5f5f, 132: 0xaf5f87, 133: 0xaf5faf, 134: 0xaf5fd7, 135: 0xaf5fff,
136: 0xaf8700, 137: 0xaf875f, 138: 0xaf8787, 139: 0xaf87af, 140: 0xaf87d7, 141: 0xaf87ff,
142: 0xafaf00, 143: 0xafaf5f, 144: 0xafaf87, 145: 0xafafaf, 146: 0xafafd7, 147: 0xafafff,
148: 0xafd700, 149: 0xafd75f, 150: 0xafd787, 151: 0xafd7af, 152: 0xafd7d7, 153: 0xafd7ff,
154: 0xafff00, 155: 0xafff5f, 156: 0xafff87, 157: 0xafffaf, 158: 0xafffd7, 159: 0xafffff,
160: 0xd70000, 161: 0xd7005f, 162: 0xd70087, 163: 0xd700af, 164: 0xd700d7, 165: 0xd700ff,
166: 0xd75f00, 167: 0xd75f5f, 168: 0xd75f87, 169: 0xd75faf, 170: 0xd75fd7, 171: 0xd75fff,
172: 0xd78700, 173: 0xd7875f, 174: 0xd78787, 175: 0xd787af, 176: 0xd787d7, 177: 0xd787ff,
178: 0xd7af00, 179: 0xd7af5f, 180: 0xd7af87, 181: 0xd7afaf, 182: 0xd7afd7, 183: 0xd7afff,
184: 0xd7d700, 185: 0xd7d75f, 186: 0xd7d787, 187: 0xd7d7af, 188: 0xd7d7d7, 189: 0xd7d7ff,
190: 0xd7ff00, 191: 0xd7ff5f, 192: 0xd7ff87, 193: 0xd7ffaf, 194: 0xd7ffd7, 195: 0xd7ffff,
196: 0xff0000, 197: 0xff005f, 198: 0xff0087, 199: 0xff00af, 200: 0xff00d7, 201: 0xff00ff,
202: 0xff5f00, 203: 0xff5f5f, 204: 0xff5f87, 205: 0xff5faf, 206: 0xff5fd7, 207: 0xff5fff,
208: 0xff8700, 209: 0xff875f, 210: 0xff8787, 211: 0xff87af, 212: 0xff87d7, 213: 0xff87ff,
214: 0xffaf00, 215: 0xffaf5f, 216: 0xffaf87, 217: 0xffafaf, 218: 0xffafd7, 219: 0xffafff,
220: 0xffd700, 221: 0xffd75f, 222: 0xffd787, 223: 0xffd7af, 224: 0xffd7d7, 225: 0xffd7ff,
226: 0xffff00, 227: 0xffff5f, 228: 0xffff87, 229: 0xffffaf, 230: 0xffffd7, 231: 0xffffff,
232: 0x080808, 233: 0x121212, 234: 0x1c1c1c, 235: 0x262626, 236: 0x303030, 237: 0x3a3a3a,
238: 0x444444, 239: 0x4e4e4e, 240: 0x585858, 241: 0x626262, 242: 0x6c6c6c, 243: 0x767676,
244: 0x808080, 245: 0x8a8a8a, 246: 0x949494, 247: 0x9e9e9e, 248: 0xa8a8a8, 249: 0xb2b2b2,
250: 0xbcbcbc, 251: 0xc6c6c6, 252: 0xd0d0d0, 253: 0xdadada, 254: 0xe4e4e4, 255: 0xeeeeee,
}
if not cterm_color:
return None
try:
return color_dict[cterm_color]
except KeyError:
import sys
sys.stderr.write('Invalid cterm color index: {0}\n'.format(cterm_color))
return None
cterm_to_hex = {
16: 0x000000, 17: 0x00005f, 18: 0x000087, 19: 0x0000af, 20: 0x0000d7, 21: 0x0000ff,
22: 0x005f00, 23: 0x005f5f, 24: 0x005f87, 25: 0x005faf, 26: 0x005fd7, 27: 0x005fff,
28: 0x008700, 29: 0x00875f, 30: 0x008787, 31: 0x0087af, 32: 0x0087d7, 33: 0x0087ff,
34: 0x00af00, 35: 0x00af5f, 36: 0x00af87, 37: 0x00afaf, 38: 0x00afd7, 39: 0x00afff,
40: 0x00d700, 41: 0x00d75f, 42: 0x00d787, 43: 0x00d7af, 44: 0x00d7d7, 45: 0x00d7ff,
46: 0x00ff00, 47: 0x00ff5f, 48: 0x00ff87, 49: 0x00ffaf, 50: 0x00ffd7, 51: 0x00ffff,
52: 0x5f0000, 53: 0x5f005f, 54: 0x5f0087, 55: 0x5f00af, 56: 0x5f00d7, 57: 0x5f00ff,
58: 0x5f5f00, 59: 0x5f5f5f, 60: 0x5f5f87, 61: 0x5f5faf, 62: 0x5f5fd7, 63: 0x5f5fff,
64: 0x5f8700, 65: 0x5f875f, 66: 0x5f8787, 67: 0x5f87af, 68: 0x5f87d7, 69: 0x5f87ff,
70: 0x5faf00, 71: 0x5faf5f, 72: 0x5faf87, 73: 0x5fafaf, 74: 0x5fafd7, 75: 0x5fafff,
76: 0x5fd700, 77: 0x5fd75f, 78: 0x5fd787, 79: 0x5fd7af, 80: 0x5fd7d7, 81: 0x5fd7ff,
82: 0x5fff00, 83: 0x5fff5f, 84: 0x5fff87, 85: 0x5fffaf, 86: 0x5fffd7, 87: 0x5fffff,
88: 0x870000, 89: 0x87005f, 90: 0x870087, 91: 0x8700af, 92: 0x8700d7, 93: 0x8700ff,
94: 0x875f00, 95: 0x875f5f, 96: 0x875f87, 97: 0x875faf, 98: 0x875fd7, 99: 0x875fff,
100: 0x878700, 101: 0x87875f, 102: 0x878787, 103: 0x8787af, 104: 0x8787d7, 105: 0x8787ff,
106: 0x87af00, 107: 0x87af5f, 108: 0x87af87, 109: 0x87afaf, 110: 0x87afd7, 111: 0x87afff,
112: 0x87d700, 113: 0x87d75f, 114: 0x87d787, 115: 0x87d7af, 116: 0x87d7d7, 117: 0x87d7ff,
118: 0x87ff00, 119: 0x87ff5f, 120: 0x87ff87, 121: 0x87ffaf, 122: 0x87ffd7, 123: 0x87ffff,
124: 0xaf0000, 125: 0xaf005f, 126: 0xaf0087, 127: 0xaf00af, 128: 0xaf00d7, 129: 0xaf00ff,
130: 0xaf5f00, 131: 0xaf5f5f, 132: 0xaf5f87, 133: 0xaf5faf, 134: 0xaf5fd7, 135: 0xaf5fff,
136: 0xaf8700, 137: 0xaf875f, 138: 0xaf8787, 139: 0xaf87af, 140: 0xaf87d7, 141: 0xaf87ff,
142: 0xafaf00, 143: 0xafaf5f, 144: 0xafaf87, 145: 0xafafaf, 146: 0xafafd7, 147: 0xafafff,
148: 0xafd700, 149: 0xafd75f, 150: 0xafd787, 151: 0xafd7af, 152: 0xafd7d7, 153: 0xafd7ff,
154: 0xafff00, 155: 0xafff5f, 156: 0xafff87, 157: 0xafffaf, 158: 0xafffd7, 159: 0xafffff,
160: 0xd70000, 161: 0xd7005f, 162: 0xd70087, 163: 0xd700af, 164: 0xd700d7, 165: 0xd700ff,
166: 0xd75f00, 167: 0xd75f5f, 168: 0xd75f87, 169: 0xd75faf, 170: 0xd75fd7, 171: 0xd75fff,
172: 0xd78700, 173: 0xd7875f, 174: 0xd78787, 175: 0xd787af, 176: 0xd787d7, 177: 0xd787ff,
178: 0xd7af00, 179: 0xd7af5f, 180: 0xd7af87, 181: 0xd7afaf, 182: 0xd7afd7, 183: 0xd7afff,
184: 0xd7d700, 185: 0xd7d75f, 186: 0xd7d787, 187: 0xd7d7af, 188: 0xd7d7d7, 189: 0xd7d7ff,
190: 0xd7ff00, 191: 0xd7ff5f, 192: 0xd7ff87, 193: 0xd7ffaf, 194: 0xd7ffd7, 195: 0xd7ffff,
196: 0xff0000, 197: 0xff005f, 198: 0xff0087, 199: 0xff00af, 200: 0xff00d7, 201: 0xff00ff,
202: 0xff5f00, 203: 0xff5f5f, 204: 0xff5f87, 205: 0xff5faf, 206: 0xff5fd7, 207: 0xff5fff,
208: 0xff8700, 209: 0xff875f, 210: 0xff8787, 211: 0xff87af, 212: 0xff87d7, 213: 0xff87ff,
214: 0xffaf00, 215: 0xffaf5f, 216: 0xffaf87, 217: 0xffafaf, 218: 0xffafd7, 219: 0xffafff,
220: 0xffd700, 221: 0xffd75f, 222: 0xffd787, 223: 0xffd7af, 224: 0xffd7d7, 225: 0xffd7ff,
226: 0xffff00, 227: 0xffff5f, 228: 0xffff87, 229: 0xffffaf, 230: 0xffffd7, 231: 0xffffff,
232: 0x080808, 233: 0x121212, 234: 0x1c1c1c, 235: 0x262626, 236: 0x303030, 237: 0x3a3a3a,
238: 0x444444, 239: 0x4e4e4e, 240: 0x585858, 241: 0x626262, 242: 0x6c6c6c, 243: 0x767676,
244: 0x808080, 245: 0x8a8a8a, 246: 0x949494, 247: 0x9e9e9e, 248: 0xa8a8a8, 249: 0xb2b2b2,
250: 0xbcbcbc, 251: 0xc6c6c6, 252: 0xd0d0d0, 253: 0xdadada, 254: 0xe4e4e4, 255: 0xeeeeee,
}

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from lib.colors import cterm_to_hex
class Powerline:
dividers = {
@ -20,6 +22,7 @@ class Powerline:
dropped from the segment array.
'''
self.segments = [segment for segment in segments if segment.contents or segment.filler]
self._hl = {}
def render(self, renderer, width=None):
'''Render all the segments with the specified renderer.
@ -34,7 +37,7 @@ class Powerline:
provided they will fill the remaining space until the desired width is
reached.
'''
def render_segments(segments, render_raw=True, render_highlighted=True):
def render_segments(segments, render_highlighted=True):
'''Render a segment array.
By default this function renders both raw (un-highlighted segments
@ -42,90 +45,94 @@ class Powerline:
rendering is used for calculating the total width for dropping
low-priority segments.
'''
rendered_raw = ''
rendered_highlighted = ''
segments_len = len(segments)
empty_segment = Segment()
for idx, segment in enumerate(segments):
prev = segments[idx - 1] if idx > 0 else Segment()
next = segments[idx + 1] if idx < len(segments) - 1 else Segment()
prev = segments[idx - 1] if idx > 0 else empty_segment
next = segments[idx + 1] if idx < segments_len - 1 else empty_segment
compare_segment = next if segment.side == 'l' else prev
divider_type = 'soft' if compare_segment.bg == segment.bg else 'hard'
compare = next if segment.side == 'l' else prev
outer_padding = ' ' if idx == 0 or idx == segments_len - 1 else ''
divider_type = 'soft' if compare.bg == segment.bg else 'hard'
divider = self.dividers[segment.side][divider_type]
divider_hl = ''
segment_hl = ''
if render_highlighted:
# Generate and cache renderer highlighting
if divider_type == 'hard':
hl_key = (segment.bg, compare.bg)
if not hl_key in self._hl:
self._hl[hl_key] = renderer.hl(*hl_key)
divider_hl = self._hl[hl_key]
hl_key = (segment.fg, segment.bg, segment.attr)
if not hl_key in self._hl:
self._hl[hl_key] = renderer.hl(*hl_key)
segment_hl = self._hl[hl_key]
if segment.filler:
# Filler segments shouldn't be padded
segment_format = '{contents}'
elif segment.draw_divider and (divider_type == 'hard' or segment.side == compare_segment.side):
rendered_highlighted += segment.contents
elif segment.draw_divider and (divider_type == 'hard' or segment.side == compare.side):
# Draw divider if specified, and if the next segment is on
# the opposite side only draw the divider if it's a hard
# divider
if segment.side == 'l':
segment_format = '{segment_hl}{outer_padding}{contents} {divider_hl}{divider} '
segment.rendered_raw += outer_padding + segment.contents + ' ' + divider + ' '
rendered_highlighted += segment_hl + outer_padding + segment.contents + ' ' + divider_hl + divider + ' '
else:
segment_format = ' {divider_hl}{divider}{segment_hl} {contents}{outer_padding}'
segment.rendered_raw += ' ' + divider + ' ' + segment.contents + outer_padding
rendered_highlighted += ' ' + divider_hl + divider + segment_hl + ' ' + segment.contents + outer_padding
elif segment.contents:
# Segments without divider
segment_format = '{segment_hl}{contents}{outer_padding}'
if segment.side == 'l':
segment.rendered_raw += outer_padding + segment.contents
rendered_highlighted += segment_hl + outer_padding + segment.contents
else:
segment.rendered_raw += segment.contents + outer_padding
rendered_highlighted += segment_hl + segment.contents + outer_padding
else:
# Unknown segment type, skip it
continue
if render_raw is True and segment.filler is False:
# Filler segments must be empty when used e.g. in vim (the
# %=%< segment which disappears), so they will be skipped
# when calculating the width using the raw rendering
rendered_raw += segment_format.format(
divider=divider,
contents=segment.contents,
divider_hl='',
segment_hl='',
outer_padding=' ' if idx == 0 or idx == len(segments) - 1 else '',
)
return rendered_highlighted.decode('utf-8')
if render_highlighted is True:
rendered_highlighted += segment_format.format(
divider=divider,
contents=segment.contents,
divider_hl='' if divider_type == 'soft' else renderer.hl(segment.bg, compare_segment.bg),
segment_hl=renderer.hl(segment.fg, segment.bg, segment.attr),
outer_padding=' ' if idx == 0 or idx == len(segments) - 1 else '',
)
return {
'highlighted': rendered_highlighted.decode('utf-8'),
'raw': rendered_raw.decode('utf-8'),
}
rendered = render_segments(self.segments)
rendered_highlighted = render_segments(self.segments)
if not width:
# No width specified, so we don't need to crop or pad anything
return rendered['highlighted']
return rendered_highlighted
# Create an ordered list of segments that can be dropped
segments_priority = [segment for segment in sorted(self.segments, key=lambda segment: segment.priority, reverse=True) if segment.priority > 0]
while len(rendered['raw']) > width and len(segments_priority):
while self._total_len() > width and len(segments_priority):
# FIXME The remove method is quite expensive and we should find another way of removing low-priority segments
self.segments.remove(segments_priority[0])
segments_priority.pop(0)
rendered = render_segments(self.segments, render_highlighted=False)
# Distribute the remaining space on the filler segments
segments_fillers = [segment for segment in self.segments if segment.filler is True]
if segments_fillers:
segments_fillers_len, segments_fillers_remainder = divmod((width - len(rendered['raw'])), len(segments_fillers))
segments_fillers_len, segments_fillers_remainder = divmod((width - self._total_len()), len(segments_fillers))
segments_fillers_contents = ' ' * segments_fillers_len
for segment in segments_fillers:
segment.contents = segments_fillers_contents
# Add remainder whitespace to the first filler segment
segments_fillers[0].contents += ' ' * segments_fillers_remainder
# Do a final render now that we have handled the cropping and padding
rendered = render_segments(self.segments, render_raw=False)
return render_segments(self.segments)
return rendered['highlighted']
def _total_len(self):
'''Return total/rendered length of all segments.
This method uses the rendered_raw property of the segments and requires
that the segments have been rendered using the render() method first.
'''
return len(''.join([segment.rendered_raw for segment in self.segments]).decode('utf-8'))
class Segment:
@ -144,23 +151,20 @@ class Segment:
self.draw_divider = draw_divider
self.priority = priority
self.filler = filler
self.rendered_raw = ''
if self.filler:
# Filler segments should never have any dividers
self.draw_divider = False
try:
if len(self.fg) != 2:
raise TypeError
self.fg = (fg[0], fg[1])
except TypeError:
# Only the terminal color is defined, so we need to get the hex color
from lib.colors import cterm_to_hex
self.fg = [self.fg, cterm_to_hex(self.fg)]
self.fg = (self.fg, cterm_to_hex.get(self.fg, 0xffffff))
try:
if len(self.bg) != 2:
raise TypeError
self.bg = (bg[0], bg[1])
except TypeError:
# Only the terminal color is defined, so we need to get the hex color
from lib.colors import cterm_to_hex
self.bg = [self.bg, cterm_to_hex(self.bg)]
self.bg = (self.bg, cterm_to_hex.get(self.bg, 0x000000))

View File

@ -17,43 +17,44 @@ class VimSegmentRenderer(SegmentRenderer):
False, the argument is reset to the terminal defaults. If an argument
is a valid color or attribute, it's added to the vim highlight group.
'''
hl_group = {
'ctermfg': 'NONE',
'guifg': 'NONE',
'ctermbg': 'NONE',
'guibg': 'NONE',
'attr': ['NONE'],
}
# We don't need to explicitly reset attributes in vim, so skip those calls
if not attr and not bg and not fg:
return ''
if fg is not None and fg is not False:
hl_group['ctermfg'] = fg[0]
hl_group['guifg'] = fg[1]
if not (fg, bg, attr) in self.hl_groups:
hl_group = {
'ctermfg': 'NONE',
'guifg': 'NONE',
'ctermbg': 'NONE',
'guibg': 'NONE',
'attr': ['NONE'],
'name': '',
}
if bg is not None and bg is not False:
hl_group['ctermbg'] = bg[0]
hl_group['guibg'] = bg[1]
if fg is not None and fg is not False:
hl_group['ctermfg'] = fg[0]
hl_group['guifg'] = fg[1]
if attr is not None and attr is not False and attr != 0:
hl_group['attr'] = []
if attr & Segment.ATTR_BOLD:
hl_group['attr'].append('bold')
if attr & Segment.ATTR_ITALIC:
hl_group['attr'].append('italic')
if attr & Segment.ATTR_UNDERLINE:
hl_group['attr'].append('underline')
if bg is not None and bg is not False:
hl_group['ctermbg'] = bg[0]
hl_group['guibg'] = bg[1]
hl_group_name = 'Pl_{ctermfg}_{guifg}_{ctermbg}_{guibg}_{attr}'.format(
ctermfg=hl_group['ctermfg'],
guifg=hl_group['guifg'],
ctermbg=hl_group['ctermbg'],
guibg=hl_group['guibg'],
attr=''.join(attr[0] for attr in hl_group['attr']),
)
if attr:
hl_group['attr'] = []
if attr & Segment.ATTR_BOLD:
hl_group['attr'].append('bold')
if attr & Segment.ATTR_ITALIC:
hl_group['attr'].append('italic')
if attr & Segment.ATTR_UNDERLINE:
hl_group['attr'].append('underline')
self.hl_groups[hl_group_name] = hl_group
hl_group['name'] = 'Pl_' + \
str(hl_group['ctermfg']) + '_' + \
str(hl_group['guifg']) + '_' + \
str(hl_group['ctermbg']) + '_' + \
str(hl_group['guibg']) + '_' + \
''.join(hl_group['attr'])
return '%#{0}#'.format(hl_group_name)
self.hl_groups[(fg, bg, attr)] = hl_group
return '%#' + self.hl_groups[(fg, bg, attr)]['name'] + '#'