Skip to content
56 changes: 49 additions & 7 deletions benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
.....................
ls.input: Mean +- std dev: 644 ns +- 23 ns

$ BENCHMARK=tests/captured/ls.input GEOMETRY=1024x1024 python benchmark.py -o results.json
.....................
ls.input: Mean +- std dev: 644 ns +- 23 ns

Environment variables:

BENCHMARK: the input file to feed pyte's Stream and render on the Screen
GEOMETRY: the dimensions of the screen with format "<lines>x<cols>" (default 24x80)

:copyright: (c) 2016-2021 by pyte authors and contributors,
see AUTHORS for details.
:license: LGPL, see LICENSE for more details.
Expand All @@ -27,21 +36,54 @@

import pyte


def make_benchmark(path, screen_cls):
with io.open(path, "rt", encoding="utf-8") as handle:
def setup(path, screen_cls, columns, lines):
with io.open(path, "rb") as handle:
data = handle.read()

stream = pyte.Stream(screen_cls(80, 24))
screen = screen_cls(columns, lines)
stream = pyte.ByteStream(screen)

return data, screen, stream

def make_stream_feed_benchmark(path, screen_cls, columns, lines):
data, _, stream = setup(path, screen_cls, columns, lines)
return partial(stream.feed, data)

def make_screen_display_benchmark(path, screen_cls, columns, lines):
data, screen, stream = setup(path, screen_cls, columns, lines)
stream.feed(data)
return lambda: screen.display

def make_screen_reset_benchmark(path, screen_cls, columns, lines):
data, screen, stream = setup(path, screen_cls, columns, lines)
stream.feed(data)
return screen.reset

def make_screen_resize_half_benchmark(path, screen_cls, columns, lines):
data, screen, stream = setup(path, screen_cls, columns, lines)
stream.feed(data)
return partial(screen.resize, lines=lines//2, columns=columns//2)

if __name__ == "__main__":
benchmark = os.environ["BENCHMARK"]
sys.argv.extend(["--inherit-environ", "BENCHMARK"])
lines, columns = map(int, os.environ.get("GEOMETRY", "24x80").split('x'))
sys.argv.extend(["--inherit-environ", "BENCHMARK,GEOMETRY"])

runner = Runner()

metadata = {
'input_file': benchmark,
'columns': columns,
'lines': lines
}

benchmark_name = os.path.basename(benchmark)
for screen_cls in [pyte.Screen, pyte.DiffScreen, pyte.HistoryScreen]:
name = os.path.basename(benchmark) + "->" + screen_cls.__name__
runner.bench_func(name, make_benchmark(benchmark, screen_cls))
screen_cls_name = screen_cls.__name__
for make_test in (make_stream_feed_benchmark, make_screen_display_benchmark, make_screen_reset_benchmark, make_screen_resize_half_benchmark):
scenario = make_test.__name__[5:-10] # remove make_ and _benchmark

name = f"[{scenario} {lines}x{columns}] {benchmark_name}->{screen_cls_name}"
metadata.update({'scenario': scenario, 'screen_cls': screen_cls_name})
runner.bench_func(name, make_test(benchmark, screen_cls, columns, lines), metadata=metadata)

51 changes: 51 additions & 0 deletions full_benchmark.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/bash

if [ "$#" != "1" -a "$#" != "2" ]; then
echo "Usage benchmark.sh <outputfile>"
echo "Usage benchmark.sh <outputfile> tracemalloc"
exit 1
fi

if [ "$2" = "tracemalloc" ]; then
tracemalloc="--tracemalloc"
elif [ "$2" = "" ]; then
tracemalloc=""
else
echo "Usage benchmark.sh <outputfile>"
echo "Usage benchmark.sh <outputfile> tracemalloc"
exit 1
fi

outputfile=$1

if [ ! -f benchmark.py ]; then
echo "File benchmark.py missing. Are you in the home folder of pyte project?"
exit 1
fi

for inputfile in $(ls -1 tests/captured/*.input); do
export GEOMETRY=24x80
echo "$inputfile - $GEOMETRY"
echo "======================"
BENCHMARK=$inputfile python benchmark.py $tracemalloc --append $outputfile

export GEOMETRY=240x800
echo "$inputfile - $GEOMETRY"
echo "======================"
BENCHMARK=$inputfile python benchmark.py $tracemalloc --append $outputfile

export GEOMETRY=2400x8000
echo "$inputfile - $GEOMETRY"
echo "======================"
BENCHMARK=$inputfile python benchmark.py $tracemalloc --append $outputfile

export GEOMETRY=24x8000
echo "$inputfile - $GEOMETRY"
echo "======================"
BENCHMARK=$inputfile python benchmark.py $tracemalloc --append $outputfile

export GEOMETRY=2400x80
echo "$inputfile - $GEOMETRY"
echo "======================"
BENCHMARK=$inputfile python benchmark.py $tracemalloc --append $outputfile
done
64 changes: 49 additions & 15 deletions pyte/screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Char(namedtuple("Char", [
"strikethrough",
"reverse",
"blink",
"width",
])):
"""A single styled on-screen character.

Expand All @@ -89,15 +90,16 @@ class Char(namedtuple("Char", [
during rendering. Defaults to ``False``.
:param bool blink: flag for rendering the character blinked. Defaults to
``False``.
:param bool width: the width in terms of cells to display this char.
"""
__slots__ = ()

def __new__(cls, data, fg="default", bg="default", bold=False,
def __new__(cls, data=" ", fg="default", bg="default", bold=False,
italics=False, underscore=False,
strikethrough=False, reverse=False, blink=False):
strikethrough=False, reverse=False, blink=False, width=wcwidth(" ")):
return super(Char, cls).__new__(cls, data, fg, bg, bold, italics,
underscore, strikethrough, reverse,
blink)
blink, width)


class Cursor:
Expand All @@ -111,7 +113,7 @@ class Cursor:
"""
__slots__ = ("x", "y", "attrs", "hidden")

def __init__(self, x, y, attrs=Char(" ")):
def __init__(self, x, y, attrs=Char(" ", width=wcwidth(" "))):
self.x = x
self.y = y
self.attrs = attrs
Expand Down Expand Up @@ -211,7 +213,7 @@ class Screen:
def default_char(self):
"""An empty character with default foreground and background colors."""
reverse = mo.DECSCNM in self.mode
return Char(data=" ", fg="default", bg="default", reverse=reverse)
return Char(data=" ", fg="default", bg="default", reverse=reverse, width=wcwidth(" "))

def __init__(self, columns, lines):
self.savepoints = []
Expand All @@ -228,18 +230,48 @@ def __repr__(self):
@property
def display(self):
"""A :func:`list` of screen lines as unicode strings."""
def render(line):
padding = self.default_char.data

prev_y = -1
output = []
columns = self.columns
for y, line in sorted(self.buffer.items()):
empty_lines = y - (prev_y + 1)
if empty_lines:
output.extend([padding * columns] * empty_lines)
prev_y = y

is_wide_char = False
for x in range(self.columns):
prev_x = -1
display_line = []
for x, cell in sorted(line.items()):
if x >= columns:
break

gap = x - (prev_x + 1)
if gap:
display_line.append(padding * gap)

prev_x = x

if is_wide_char: # Skip stub
is_wide_char = False
continue
char = line[x].data
assert sum(map(wcwidth, char[1:])) == 0
is_wide_char = wcwidth(char[0]) == 2
yield char
char = cell.data
is_wide_char = cell.width == 2
display_line.append(char)

gap = columns - (prev_x + 1)
if gap:
display_line.append(padding * gap)

output.append("".join(display_line))

empty_lines = self.lines - (prev_y + 1)
if empty_lines:
output.extend([padding * columns] * empty_lines)

return ["".join(render(self.buffer[y])) for y in range(self.lines)]
return output

def reset(self):
"""Reset the terminal to its initial state.
Expand Down Expand Up @@ -497,16 +529,18 @@ def draw(self, data):

line = self.buffer[self.cursor.y]
if char_width == 1:
line[self.cursor.x] = self.cursor.attrs._replace(data=char)
line[self.cursor.x] = self.cursor.attrs._replace(data=char, width=char_width)
elif char_width == 2:
# A two-cell character has a stub slot after it.
line[self.cursor.x] = self.cursor.attrs._replace(data=char)
line[self.cursor.x] = self.cursor.attrs._replace(data=char, width=char_width)
if self.cursor.x + 1 < self.columns:
line[self.cursor.x + 1] = self.cursor.attrs \
._replace(data="")
._replace(data="", width=0)
elif char_width == 0 and unicodedata.combining(char):
# A zero-cell character is combined with the previous
# character either on this or preceding line.
# Because char's width is zero, this will not change the width
# of the previous character.
if self.cursor.x:
last = line[self.cursor.x - 1]
normalized = unicodedata.normalize("NFC", last.data + char)
Expand Down
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pytest
pyperf == 1.7.1
pyperf >= 2.3.0
wcwidth
wheel
22 changes: 22 additions & 0 deletions tests/helpers/asserts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from wcwidth import wcwidth
def consistency_asserts(screen):
# Ensure that all the cells in the buffer, if they have
# a data of 2 or more code points, they all sum up 0 width
# In other words, the width of the cell is determinated by the
# width of the first code point.
for y in range(screen.lines):
for x in range(screen.columns):
data = screen.buffer[y][x].data
assert sum(map(wcwidth, data[1:])) == 0


# Ensure consistency between the real width (computed here
# with wcwidth(...)) and the char.width attribute
for y in range(screen.lines):
for x in range(screen.columns):
char = screen.buffer[y][x]
if char.data:
assert wcwidth(char.data[0]) == char.width
else:
assert char.data == ""
assert char.width == 0
Loading