Improve rendering performance

This change removes the Segment class as this takes forever to remove
from the segment array when removing low-priority segments. It has
instead been replaced by a wrapper function that works the same and
returns a working dict of all segment properties.

The regex substitution bottleneck in the vim example has been fixed by
using a single-character percent placeholder in vim segments which is
later replaced with a double percent using str.replace().
This commit is contained in:
Kim Silkebækken 2012-11-21 11:33:10 +01:00
parent f4e3d01d07
commit 1d3c259070
5 changed files with 85 additions and 93 deletions

View File

@ -5,7 +5,7 @@
import os import os
import sys import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from lib.core import Powerline, Segment from lib.core import Powerline, Segment
from lib.renderers import TerminalSegmentRenderer from lib.renderers import TerminalSegmentRenderer

View File

@ -4,7 +4,7 @@ import vim
import os import os
import re import re
from lib.core import Powerline, Segment from lib.core import Powerline, mksegment
from lib.renderers import VimSegmentRenderer from lib.renderers import VimSegmentRenderer
modes = { modes = {
@ -28,6 +28,9 @@ modes = {
'!': 'SHELL', '!': 'SHELL',
} }
# We need to replace this private use glyph with a double-percent later
percent_placeholder = ''.decode('utf-8')
if hasattr(vim, 'bindeval'): if hasattr(vim, 'bindeval'):
# This branch is used to avoid invoking vim parser as much as possible # This branch is used to avoid invoking vim parser as much as possible
@ -137,7 +140,7 @@ def statusline(winnr):
'fileformat': vim.eval('&ff'), 'fileformat': vim.eval('&ff'),
'fileencoding': vim.eval('&fenc'), 'fileencoding': vim.eval('&fenc'),
'filetype': vim.eval('&ft'), 'filetype': vim.eval('&ft'),
'line_percent': str(line_percent).rjust(3) + '%', 'line_percent': str(line_percent).rjust(3) + percent_placeholder,
'line_percent_color': line_percent_color, 'line_percent_color': line_percent_color,
'linecurrent': str(line_current).rjust(3), 'linecurrent': str(line_current).rjust(3),
'colcurrent': ':' + str(col_current).ljust(2), 'colcurrent': ':' + str(col_current).ljust(2),
@ -151,29 +154,29 @@ def statusline(winnr):
mode = None mode = None
powerline = Powerline([ powerline = Powerline([
Segment(mode, 22, 148, attr=Segment.ATTR_BOLD), mksegment(mode, 22, 148, attr=Powerline.ATTR_BOLD),
Segment(windata['paste'], 231, 166, attr=Segment.ATTR_BOLD), mksegment(windata['paste'], 231, 166, attr=Powerline.ATTR_BOLD),
Segment(windata['branch'], 250, 240, priority=10), mksegment(windata['branch'], 250, 240, priority=10),
Segment(windata['readonly'], 196, 240, draw_divider=False), mksegment(windata['readonly'], 196, 240, draw_divider=False),
Segment(windata['filepath'], 250, 240, draw_divider=False, priority=5), mksegment(windata['filepath'], 250, 240, draw_divider=False, priority=5),
Segment(windata['filename'], windata['filename_color'], 240, attr=Segment.ATTR_BOLD, draw_divider=not len(windata['modified'])), mksegment(windata['filename'], windata['filename_color'], 240, attr=Powerline.ATTR_BOLD, draw_divider=not len(windata['modified'])),
Segment(windata['modified'], 220, 240, attr=Segment.ATTR_BOLD), mksegment(windata['modified'], 220, 240, attr=Powerline.ATTR_BOLD),
Segment(windata['currenttag'], 246, 236, draw_divider=False, priority=100), mksegment(windata['currenttag'], 246, 236, draw_divider=False, priority=100),
Segment(filler=True, fg=236, bg=236), mksegment(filler=True, cterm_fg=236, cterm_bg=236),
Segment(windata['fileformat'], 247, 236, side='r', priority=50), mksegment(windata['fileformat'], 247, 236, side='r', priority=50),
Segment(windata['fileencoding'], 247, 236, side='r', priority=50), mksegment(windata['fileencoding'], 247, 236, side='r', priority=50),
Segment(windata['filetype'], 247, 236, side='r', priority=50), mksegment(windata['filetype'], 247, 236, side='r', priority=50),
Segment(windata['line_percent'], windata['line_percent_color'], 240, side='r', priority=30), mksegment(windata['line_percent'], windata['line_percent_color'], 240, side='r', priority=30),
Segment('', 239, 252, side='r'), mksegment(u'', 239, 252, side='r'),
Segment(windata['linecurrent'], 235, 252, attr=Segment.ATTR_BOLD, side='r', draw_divider=False), mksegment(windata['linecurrent'], 235, 252, attr=Powerline.ATTR_BOLD, side='r', draw_divider=False),
Segment(windata['colcurrent'], 244, 252, side='r', priority=30, draw_divider=False), mksegment(windata['colcurrent'], 244, 252, side='r', priority=30, draw_divider=False),
]) ])
renderer = VimSegmentRenderer() renderer = VimSegmentRenderer()
stl = powerline.render(renderer, winwidth) stl = powerline.render(renderer, winwidth)
# Escape percent chars in the statusline, but only if they aren't part of any stl escape sequence # Replace percent placeholders
stl = re.sub('(\w+)\%(?![-{()<=#*%])', '\\1%%', stl) stl = stl.replace(percent_placeholder, '%%')
# Create highlighting groups # Create highlighting groups
for idx, hl in renderer.hl_groups.items(): for idx, hl in renderer.hl_groups.items():

View File

@ -3,15 +3,19 @@
from lib.colors import cterm_to_hex from lib.colors import cterm_to_hex
class Powerline: class Powerline(object):
ATTR_BOLD = 1
ATTR_ITALIC = 2
ATTR_UNDERLINE = 4
dividers = { dividers = {
'l': { 'l': {
'hard': '', 'hard': u'',
'soft': '', 'soft': u'',
}, },
'r': { 'r': {
'hard': '', 'hard': u'',
'soft': '', 'soft': u'',
}, },
} }
@ -21,7 +25,7 @@ class Powerline:
Segments that have empty contents and aren't filler segments are Segments that have empty contents and aren't filler segments are
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 = {} self._hl = {}
def render(self, renderer, width=None): def render(self, renderer, width=None):
@ -45,60 +49,60 @@ 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_highlighted = '' rendered_highlighted = u''
segments_len = len(segments) segments_len = len(segments)
empty_segment = Segment() empty_segment = mksegment()
for idx, segment in enumerate(segments): for idx, segment in enumerate(segments):
prev = segments[idx - 1] if idx > 0 else empty_segment prev = segments[idx - 1] if idx > 0 else empty_segment
next = segments[idx + 1] if idx < segments_len - 1 else empty_segment next = segments[idx + 1] if idx < segments_len - 1 else empty_segment
compare = next if segment.side == 'l' else prev compare = next if segment['side'] == 'l' else prev
outer_padding = ' ' if idx == 0 or idx == segments_len - 1 else '' outer_padding = ' ' if idx == 0 or idx == segments_len - 1 else ''
divider_type = 'soft' if compare.bg == segment.bg else 'hard' 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 = '' divider_hl = ''
segment_hl = '' segment_hl = ''
if render_highlighted: if render_highlighted:
# Generate and cache renderer highlighting # Generate and cache renderer highlighting
if divider_type == 'hard': if divider_type == 'hard':
hl_key = (segment.bg, compare.bg) hl_key = (segment['bg'], compare['bg'])
if not hl_key in self._hl: if not hl_key in self._hl:
self._hl[hl_key] = renderer.hl(*hl_key) self._hl[hl_key] = renderer.hl(*hl_key)
divider_hl = self._hl[hl_key] divider_hl = self._hl[hl_key]
hl_key = (segment.fg, segment.bg, segment.attr) hl_key = (segment['fg'], segment['bg'], segment['attr'])
if not hl_key in self._hl: if not hl_key in self._hl:
self._hl[hl_key] = renderer.hl(*hl_key) self._hl[hl_key] = renderer.hl(*hl_key)
segment_hl = self._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
rendered_highlighted += segment.contents rendered_highlighted += segment['contents']
elif segment.draw_divider and (divider_type == 'hard' or segment.side == compare.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.rendered_raw += outer_padding + segment.contents + ' ' + divider + ' ' segment['rendered_raw'] += outer_padding + segment['contents'] + ' ' + divider + ' '
rendered_highlighted += segment_hl + outer_padding + segment.contents + ' ' + divider_hl + divider + ' ' rendered_highlighted += segment_hl + outer_padding + segment['contents'] + ' ' + divider_hl + divider + ' '
else: else:
segment.rendered_raw += ' ' + divider + ' ' + segment.contents + outer_padding segment['rendered_raw'] += ' ' + divider + ' ' + segment['contents'] + outer_padding
rendered_highlighted += ' ' + divider_hl + divider + segment_hl + ' ' + 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
if segment.side == 'l': if segment['side'] == 'l':
segment.rendered_raw += outer_padding + segment.contents segment['rendered_raw'] += outer_padding + segment['contents']
rendered_highlighted += segment_hl + outer_padding + segment.contents rendered_highlighted += segment_hl + outer_padding + segment['contents']
else: else:
segment.rendered_raw += segment.contents + outer_padding segment['rendered_raw'] += segment['contents'] + outer_padding
rendered_highlighted += segment_hl + 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
return rendered_highlighted.decode('utf-8') return rendered_highlighted
rendered_highlighted = render_segments(self.segments) rendered_highlighted = render_segments(self.segments)
@ -107,22 +111,21 @@ class Powerline:
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 self._total_len() > 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)
# 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 - self._total_len()), 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
return render_segments(self.segments) return render_segments(self.segments)
@ -132,39 +135,25 @@ class Powerline:
This method uses the rendered_raw property of the segments and requires This method uses the rendered_raw property of the segments and requires
that the segments have been rendered using the render() method first. 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')) return len(''.join([segment['rendered_raw'] for segment in self.segments]))
class Segment: def mksegment(contents=None, cterm_fg=False, cterm_bg=False, attr=False, hex_fg=False, hex_bg=False, side='l', draw_divider=True, priority=-1, filler=False):
ATTR_BOLD = 1 '''Convenience wrapper for segment generation.
ATTR_ITALIC = 2 '''
ATTR_UNDERLINE = 4 try:
contents = unicode(contents or u'')
except UnicodeDecodeError:
contents = contents.decode('utf-8') or u''
def __init__(self, contents=None, fg=False, bg=False, attr=False, side='l', draw_divider=True, priority=-1, filler=False): return {
'''Create a new Powerline segment. 'contents': contents,
''' 'fg': (cterm_fg, hex_fg or cterm_to_hex.get(cterm_fg, 0xffffff)),
self.contents = str(contents or '') 'bg': (cterm_bg, hex_bg or cterm_to_hex.get(cterm_bg, 0x000000)),
self.fg = fg 'attr': attr,
self.bg = bg 'side': side,
self.attr = attr 'draw_divider': False if filler else draw_divider,
self.side = side 'priority': priority,
self.draw_divider = draw_divider 'filler': filler,
self.priority = priority 'rendered_raw': u'',
self.filler = filler }
self.rendered_raw = ''
if self.filler:
# Filler segments should never have any dividers
self.draw_divider = False
try:
self.fg = (fg[0], fg[1])
except TypeError:
# Only the terminal color is defined, so we need to get the hex color
self.fg = (self.fg, cterm_to_hex.get(self.fg, 0xffffff))
try:
self.bg = (bg[0], bg[1])
except TypeError:
# Only the terminal color is defined, so we need to get the hex color
self.bg = (self.bg, cterm_to_hex.get(self.bg, 0x000000))

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from lib.core import Segment from lib.core import Powerline
from lib.renderers import SegmentRenderer from lib.renderers import SegmentRenderer
@ -32,7 +32,7 @@ class TerminalSegmentRenderer(SegmentRenderer):
if attr is False: if attr is False:
ansi += [22] ansi += [22]
else: else:
if attr & Segment.ATTR_BOLD: if attr & Powerline.ATTR_BOLD:
ansi += [1] ansi += [1]
return '[{0}m'.format(';'.join(str(attr) for attr in ansi)) return '[{0}m'.format(';'.join(str(attr) for attr in ansi))

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from lib.core import Segment from lib.core import Powerline
from lib.renderers import SegmentRenderer from lib.renderers import SegmentRenderer
@ -41,11 +41,11 @@ class VimSegmentRenderer(SegmentRenderer):
if attr: if attr:
hl_group['attr'] = [] hl_group['attr'] = []
if attr & Segment.ATTR_BOLD: if attr & Powerline.ATTR_BOLD:
hl_group['attr'].append('bold') hl_group['attr'].append('bold')
if attr & Segment.ATTR_ITALIC: if attr & Powerline.ATTR_ITALIC:
hl_group['attr'].append('italic') hl_group['attr'].append('italic')
if attr & Segment.ATTR_UNDERLINE: if attr & Powerline.ATTR_UNDERLINE:
hl_group['attr'].append('underline') hl_group['attr'].append('underline')
hl_group['name'] = 'Pl_' + \ hl_group['name'] = 'Pl_' + \