From 1d3c2590706ad770fd290aa94c98aa89ffac4d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20Silkeb=C3=A6kken?= Date: Wed, 21 Nov 2012 11:33:10 +0100 Subject: [PATCH] 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(). --- examples/terminal/powerline.py | 2 +- examples/vim/powerline.py | 43 ++++++------ lib/core.py | 121 +++++++++++++++------------------ lib/renderers/terminal.py | 4 +- lib/renderers/vim.py | 8 +-- 5 files changed, 85 insertions(+), 93 deletions(-) diff --git a/examples/terminal/powerline.py b/examples/terminal/powerline.py index 0e6ab9fc..d16cdb4f 100755 --- a/examples/terminal/powerline.py +++ b/examples/terminal/powerline.py @@ -5,7 +5,7 @@ import os 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.renderers import TerminalSegmentRenderer diff --git a/examples/vim/powerline.py b/examples/vim/powerline.py index 435392ee..742f5a28 100644 --- a/examples/vim/powerline.py +++ b/examples/vim/powerline.py @@ -4,7 +4,7 @@ import vim import os import re -from lib.core import Powerline, Segment +from lib.core import Powerline, mksegment from lib.renderers import VimSegmentRenderer modes = { @@ -28,6 +28,9 @@ modes = { '!': 'SHELL', } +# We need to replace this private use glyph with a double-percent later +percent_placeholder = ''.decode('utf-8') + if hasattr(vim, 'bindeval'): # This branch is used to avoid invoking vim parser as much as possible @@ -137,7 +140,7 @@ def statusline(winnr): 'fileformat': vim.eval('&ff'), 'fileencoding': vim.eval('&fenc'), '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, 'linecurrent': str(line_current).rjust(3), 'colcurrent': ':' + str(col_current).ljust(2), @@ -151,29 +154,29 @@ def statusline(winnr): mode = None powerline = Powerline([ - Segment(mode, 22, 148, attr=Segment.ATTR_BOLD), - Segment(windata['paste'], 231, 166, attr=Segment.ATTR_BOLD), - Segment(windata['branch'], 250, 240, priority=10), - Segment(windata['readonly'], 196, 240, draw_divider=False), - Segment(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'])), - Segment(windata['modified'], 220, 240, attr=Segment.ATTR_BOLD), - Segment(windata['currenttag'], 246, 236, draw_divider=False, priority=100), - Segment(filler=True, fg=236, bg=236), - Segment(windata['fileformat'], 247, 236, side='r', priority=50), - Segment(windata['fileencoding'], 247, 236, side='r', priority=50), - Segment(windata['filetype'], 247, 236, side='r', priority=50), - Segment(windata['line_percent'], windata['line_percent_color'], 240, side='r', priority=30), - Segment('⭡ ', 239, 252, side='r'), - Segment(windata['linecurrent'], 235, 252, attr=Segment.ATTR_BOLD, side='r', draw_divider=False), - Segment(windata['colcurrent'], 244, 252, side='r', priority=30, draw_divider=False), + mksegment(mode, 22, 148, attr=Powerline.ATTR_BOLD), + mksegment(windata['paste'], 231, 166, attr=Powerline.ATTR_BOLD), + mksegment(windata['branch'], 250, 240, priority=10), + mksegment(windata['readonly'], 196, 240, draw_divider=False), + mksegment(windata['filepath'], 250, 240, draw_divider=False, priority=5), + mksegment(windata['filename'], windata['filename_color'], 240, attr=Powerline.ATTR_BOLD, draw_divider=not len(windata['modified'])), + mksegment(windata['modified'], 220, 240, attr=Powerline.ATTR_BOLD), + mksegment(windata['currenttag'], 246, 236, draw_divider=False, priority=100), + mksegment(filler=True, cterm_fg=236, cterm_bg=236), + mksegment(windata['fileformat'], 247, 236, side='r', priority=50), + mksegment(windata['fileencoding'], 247, 236, side='r', priority=50), + mksegment(windata['filetype'], 247, 236, side='r', priority=50), + mksegment(windata['line_percent'], windata['line_percent_color'], 240, side='r', priority=30), + mksegment(u'⭡ ', 239, 252, side='r'), + mksegment(windata['linecurrent'], 235, 252, attr=Powerline.ATTR_BOLD, side='r', draw_divider=False), + mksegment(windata['colcurrent'], 244, 252, side='r', priority=30, draw_divider=False), ]) renderer = VimSegmentRenderer() stl = powerline.render(renderer, winwidth) - # Escape percent chars in the statusline, but only if they aren't part of any stl escape sequence - stl = re.sub('(\w+)\%(?![-{()<=#*%])', '\\1%%', stl) + # Replace percent placeholders + stl = stl.replace(percent_placeholder, '%%') # Create highlighting groups for idx, hl in renderer.hl_groups.items(): diff --git a/lib/core.py b/lib/core.py index 25b57a91..c0baf222 100644 --- a/lib/core.py +++ b/lib/core.py @@ -3,15 +3,19 @@ from lib.colors import cterm_to_hex -class Powerline: +class Powerline(object): + ATTR_BOLD = 1 + ATTR_ITALIC = 2 + ATTR_UNDERLINE = 4 + dividers = { 'l': { - 'hard': '⮀', - 'soft': '⮁', + 'hard': u'⮀', + 'soft': u'⮁', }, 'r': { - 'hard': '⮂', - 'soft': '⮃', + 'hard': u'⮂', + 'soft': u'⮃', }, } @@ -21,7 +25,7 @@ class Powerline: Segments that have empty contents and aren't filler segments are 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): @@ -45,60 +49,60 @@ class Powerline: rendering is used for calculating the total width for dropping low-priority segments. ''' - rendered_highlighted = '' + rendered_highlighted = u'' segments_len = len(segments) - empty_segment = Segment() + empty_segment = mksegment() for idx, segment in enumerate(segments): prev = segments[idx - 1] if idx > 0 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 '' - divider_type = 'soft' if compare.bg == segment.bg else 'hard' - divider = self.dividers[segment.side][divider_type] + 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) + 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) + 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 - rendered_highlighted += segment.contents - elif segment.draw_divider and (divider_type == 'hard' or segment.side == compare.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.rendered_raw += outer_padding + segment.contents + ' ' + divider + ' ' - rendered_highlighted += segment_hl + outer_padding + segment.contents + ' ' + divider_hl + divider + ' ' + if segment['side'] == 'l': + segment['rendered_raw'] += outer_padding + segment['contents'] + ' ' + divider + ' ' + rendered_highlighted += segment_hl + outer_padding + segment['contents'] + ' ' + divider_hl + divider + ' ' else: - segment.rendered_raw += ' ' + divider + ' ' + segment.contents + outer_padding - rendered_highlighted += ' ' + divider_hl + divider + segment_hl + ' ' + segment.contents + outer_padding - elif segment.contents: + 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 - if segment.side == 'l': - segment.rendered_raw += outer_padding + segment.contents - rendered_highlighted += segment_hl + outer_padding + segment.contents + 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 + segment['rendered_raw'] += segment['contents'] + outer_padding + rendered_highlighted += segment_hl + segment['contents'] + outer_padding else: # Unknown segment type, skip it continue - return rendered_highlighted.decode('utf-8') + return rendered_highlighted rendered_highlighted = render_segments(self.segments) @@ -107,22 +111,21 @@ class Powerline: 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] + 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): - # 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) # 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: 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 + segment['contents'] = segments_fillers_contents # 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) @@ -132,39 +135,25 @@ class Powerline: 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')) + return len(''.join([segment['rendered_raw'] for segment in self.segments])) -class Segment: - ATTR_BOLD = 1 - ATTR_ITALIC = 2 - ATTR_UNDERLINE = 4 +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): + '''Convenience wrapper for segment generation. + ''' + 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): - '''Create a new Powerline segment. - ''' - self.contents = str(contents or '') - self.fg = fg - self.bg = bg - self.attr = attr - self.side = side - 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: - 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)) + return { + 'contents': contents, + 'fg': (cterm_fg, hex_fg or cterm_to_hex.get(cterm_fg, 0xffffff)), + 'bg': (cterm_bg, hex_bg or cterm_to_hex.get(cterm_bg, 0x000000)), + 'attr': attr, + 'side': side, + 'draw_divider': False if filler else draw_divider, + 'priority': priority, + 'filler': filler, + 'rendered_raw': u'', + } diff --git a/lib/renderers/terminal.py b/lib/renderers/terminal.py index b80c37e1..ff55dff2 100644 --- a/lib/renderers/terminal.py +++ b/lib/renderers/terminal.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from lib.core import Segment +from lib.core import Powerline from lib.renderers import SegmentRenderer @@ -32,7 +32,7 @@ class TerminalSegmentRenderer(SegmentRenderer): if attr is False: ansi += [22] else: - if attr & Segment.ATTR_BOLD: + if attr & Powerline.ATTR_BOLD: ansi += [1] return '[{0}m'.format(';'.join(str(attr) for attr in ansi)) diff --git a/lib/renderers/vim.py b/lib/renderers/vim.py index b91fd96c..07a4dbb2 100644 --- a/lib/renderers/vim.py +++ b/lib/renderers/vim.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from lib.core import Segment +from lib.core import Powerline from lib.renderers import SegmentRenderer @@ -41,11 +41,11 @@ class VimSegmentRenderer(SegmentRenderer): if attr: hl_group['attr'] = [] - if attr & Segment.ATTR_BOLD: + if attr & Powerline.ATTR_BOLD: hl_group['attr'].append('bold') - if attr & Segment.ATTR_ITALIC: + if attr & Powerline.ATTR_ITALIC: hl_group['attr'].append('italic') - if attr & Segment.ATTR_UNDERLINE: + if attr & Powerline.ATTR_UNDERLINE: hl_group['attr'].append('underline') hl_group['name'] = 'Pl_' + \