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

View File

@ -1,7 +1,4 @@
def cterm_to_hex(cterm_color): cterm_to_hex = {
'''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, 16: 0x000000, 17: 0x00005f, 18: 0x000087, 19: 0x0000af, 20: 0x0000d7, 21: 0x0000ff,
22: 0x005f00, 23: 0x005f5f, 24: 0x005f87, 25: 0x005faf, 26: 0x005fd7, 27: 0x005fff, 22: 0x005f00, 23: 0x005f5f, 24: 0x005f87, 25: 0x005faf, 26: 0x005fd7, 27: 0x005fff,
28: 0x008700, 29: 0x00875f, 30: 0x008787, 31: 0x0087af, 32: 0x0087d7, 33: 0x0087ff, 28: 0x008700, 29: 0x00875f, 30: 0x008787, 31: 0x0087af, 32: 0x0087d7, 33: 0x0087ff,
@ -43,12 +40,3 @@ def cterm_to_hex(cterm_color):
244: 0x808080, 245: 0x8a8a8a, 246: 0x949494, 247: 0x9e9e9e, 248: 0xa8a8a8, 249: 0xb2b2b2, 244: 0x808080, 245: 0x8a8a8a, 246: 0x949494, 247: 0x9e9e9e, 248: 0xa8a8a8, 249: 0xb2b2b2,
250: 0xbcbcbc, 251: 0xc6c6c6, 252: 0xd0d0d0, 253: 0xdadada, 254: 0xe4e4e4, 255: 0xeeeeee, 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

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from lib.colors import cterm_to_hex
class Powerline: class Powerline:
dividers = { dividers = {
@ -20,6 +22,7 @@ class Powerline:
dropped from the segment array. dropped from the segment array.
''' '''
self.segments = [segment for segment in segments if segment.contents or segment.filler] self.segments = [segment for segment in segments if segment.contents or segment.filler]
self._hl = {}
def render(self, renderer, width=None): def render(self, renderer, width=None):
'''Render all the segments with the specified renderer. '''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 provided they will fill the remaining space until the desired width is
reached. reached.
''' '''
def render_segments(segments, render_raw=True, render_highlighted=True): def render_segments(segments, render_highlighted=True):
'''Render a segment array. '''Render a segment array.
By default this function renders both raw (un-highlighted segments 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 rendering is used for calculating the total width for dropping
low-priority segments. low-priority segments.
''' '''
rendered_raw = ''
rendered_highlighted = '' rendered_highlighted = ''
segments_len = len(segments)
empty_segment = Segment()
for idx, segment in enumerate(segments): for idx, segment in enumerate(segments):
prev = segments[idx - 1] if idx > 0 else Segment() prev = segments[idx - 1] if idx > 0 else empty_segment
next = segments[idx + 1] if idx < len(segments) - 1 else Segment() next = segments[idx + 1] if idx < segments_len - 1 else empty_segment
compare_segment = next if segment.side == 'l' else prev compare = next if segment.side == 'l' else prev
divider_type = 'soft' if compare_segment.bg == segment.bg else 'hard' 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 = 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: if segment.filler:
# Filler segments shouldn't be padded # Filler segments shouldn't be padded
segment_format = '{contents}' rendered_highlighted += segment.contents
elif segment.draw_divider and (divider_type == 'hard' or segment.side == compare_segment.side): elif segment.draw_divider and (divider_type == 'hard' or segment.side == compare.side):
# Draw divider if specified, and if the next segment is on # Draw divider if specified, and if the next segment is on
# the opposite side only draw the divider if it's a hard # the opposite side only draw the divider if it's a hard
# divider # divider
if segment.side == 'l': 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: 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: elif segment.contents:
# Segments without divider # 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: else:
# Unknown segment type, skip it # Unknown segment type, skip it
continue continue
if render_raw is True and segment.filler is False: return rendered_highlighted.decode('utf-8')
# 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 '',
)
if render_highlighted is True: rendered_highlighted = render_segments(self.segments)
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)
if not width: if not width:
# No width specified, so we don't need to crop or pad anything # 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 # 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] 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]) self.segments.remove(segments_priority[0])
segments_priority.pop(0) segments_priority.pop(0)
rendered = render_segments(self.segments, render_highlighted=False)
# Distribute the remaining space on the filler segments # Distribute the remaining space on the filler segments
segments_fillers = [segment for segment in self.segments if segment.filler is True] segments_fillers = [segment for segment in self.segments if segment.filler is True]
if segments_fillers: 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 segments_fillers_contents = ' ' * segments_fillers_len
for segment in segments_fillers: for segment in segments_fillers:
segment.contents = segments_fillers_contents segment.contents = segments_fillers_contents
# Add remainder whitespace to the first filler segment # Add remainder whitespace to the first filler segment
segments_fillers[0].contents += ' ' * segments_fillers_remainder segments_fillers[0].contents += ' ' * segments_fillers_remainder
# Do a final render now that we have handled the cropping and padding return render_segments(self.segments)
rendered = render_segments(self.segments, render_raw=False)
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: class Segment:
@ -144,23 +151,20 @@ class Segment:
self.draw_divider = draw_divider self.draw_divider = draw_divider
self.priority = priority self.priority = priority
self.filler = filler self.filler = filler
self.rendered_raw = ''
if self.filler: if self.filler:
# Filler segments should never have any dividers # Filler segments should never have any dividers
self.draw_divider = False self.draw_divider = False
try: try:
if len(self.fg) != 2: self.fg = (fg[0], fg[1])
raise TypeError
except TypeError: except TypeError:
# Only the terminal color is defined, so we need to get the hex color # 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.get(self.fg, 0xffffff))
self.fg = [self.fg, cterm_to_hex(self.fg)]
try: try:
if len(self.bg) != 2: self.bg = (bg[0], bg[1])
raise TypeError
except TypeError: except TypeError:
# Only the terminal color is defined, so we need to get the hex color # 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.get(self.bg, 0x000000))
self.bg = [self.bg, cterm_to_hex(self.bg)]

View File

@ -17,18 +17,20 @@ class VimSegmentRenderer(SegmentRenderer):
False, the argument is reset to the terminal defaults. If an argument 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. is a valid color or attribute, it's added to the vim highlight group.
''' '''
# 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 not (fg, bg, attr) in self.hl_groups:
hl_group = { hl_group = {
'ctermfg': 'NONE', 'ctermfg': 'NONE',
'guifg': 'NONE', 'guifg': 'NONE',
'ctermbg': 'NONE', 'ctermbg': 'NONE',
'guibg': 'NONE', 'guibg': 'NONE',
'attr': ['NONE'], 'attr': ['NONE'],
'name': '',
} }
# 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: if fg is not None and fg is not False:
hl_group['ctermfg'] = fg[0] hl_group['ctermfg'] = fg[0]
hl_group['guifg'] = fg[1] hl_group['guifg'] = fg[1]
@ -37,7 +39,7 @@ class VimSegmentRenderer(SegmentRenderer):
hl_group['ctermbg'] = bg[0] hl_group['ctermbg'] = bg[0]
hl_group['guibg'] = bg[1] hl_group['guibg'] = bg[1]
if attr is not None and attr is not False and attr != 0: if attr:
hl_group['attr'] = [] hl_group['attr'] = []
if attr & Segment.ATTR_BOLD: if attr & Segment.ATTR_BOLD:
hl_group['attr'].append('bold') hl_group['attr'].append('bold')
@ -46,14 +48,13 @@ class VimSegmentRenderer(SegmentRenderer):
if attr & Segment.ATTR_UNDERLINE: if attr & Segment.ATTR_UNDERLINE:
hl_group['attr'].append('underline') hl_group['attr'].append('underline')
hl_group_name = 'Pl_{ctermfg}_{guifg}_{ctermbg}_{guibg}_{attr}'.format( hl_group['name'] = 'Pl_' + \
ctermfg=hl_group['ctermfg'], str(hl_group['ctermfg']) + '_' + \
guifg=hl_group['guifg'], str(hl_group['guifg']) + '_' + \
ctermbg=hl_group['ctermbg'], str(hl_group['ctermbg']) + '_' + \
guibg=hl_group['guibg'], str(hl_group['guibg']) + '_' + \
attr=''.join(attr[0] for attr in hl_group['attr']), ''.join(hl_group['attr'])
)
self.hl_groups[hl_group_name] = hl_group self.hl_groups[(fg, bg, attr)] = hl_group
return '%#{0}#'.format(hl_group_name) return '%#' + self.hl_groups[(fg, bg, attr)]['name'] + '#'