Skip to content

make use of PyQt sip.array#2314

Merged
j9ac9k merged 4 commits intopyqtgraph:masterfrom
pijyoi:test_siparray
Jun 18, 2022
Merged

make use of PyQt sip.array#2314
j9ac9k merged 4 commits intopyqtgraph:masterfrom
pijyoi:test_siparray

Conversation

@pijyoi
Copy link
Copy Markdown
Contributor

@pijyoi pijyoi commented May 26, 2022

Quick test of PyQt sip.array.
It can be exercised by running PlotSpeedTest.py and triggering the segmented line mode (by increasing the line width for example).

@pijyoi pijyoi force-pushed the test_siparray branch 2 times, most recently from 039ce34 to 83a9026 Compare May 27, 2022 14:43
@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented May 28, 2022

pyqtgraph-free script to demonstrate any performance differences with using sip array.

from PyQt6 import QtCore, QtGui
import PyQt6.sip as sip
import numpy as np

import itertools
from time import perf_counter

class LineSegments:
    def __init__(self, use_sip_array):
        self.use_sip_array = hasattr(sip, 'array') and use_sip_array
        self.alloc(0)

    def alloc(self, size):
        if self.use_sip_array:
            self.objs = sip.array(QtCore.QLineF, size)
            vp = sip.voidptr(self.objs, len(self.objs)*4*8)
            self.arr = np.frombuffer(vp, dtype=np.float64).reshape((-1, 4))
        else:
            self.arr = np.empty((size, 4), dtype=np.float64)
            self.objs = list(map(sip.wrapinstance,
                itertools.count(self.arr.ctypes.data, self.arr.strides[0]),
                itertools.repeat(QtCore.QLineF, self.arr.shape[0])))

    def get(self, size):
        if size != self.arr.shape[0]:
            self.alloc(size)
        return self.objs, self.arr

def run(size, use_sip_array):
    qimg = QtGui.QImage(640, 480, QtGui.QImage.Format.Format_RGB32)
    qimg.fill(QtCore.Qt.GlobalColor.transparent)
    segments = LineSegments(use_sip_array)
    objs, arr = segments.get(size)
    arr[:, 0] = 0
    arr[:, 1] = 0
    arr[:, 2] = 10
    arr[:, 3] = 10

    painter = QtGui.QPainter(qimg)
    painter.setPen(QtCore.Qt.GlobalColor.cyan)

    t0 = perf_counter()
    painter.drawLines(objs)
    t1 = perf_counter()

    painter.end()
    return t1 - t0

size = 1_000_000
for use_sip_array in [False, True]:
    dt = run(size, use_sip_array)
    print(f'{use_sip_array=} {dt:.3f}')

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented May 29, 2022

MWE take 2. Make it more explicit that the same lines object is being re-used for multiple frames.

from PyQt6 import QtCore, QtGui
import PyQt6.sip as sip
import numpy as np

import itertools
from time import perf_counter

class LineSegments:
    def __init__(self, use_sip_array):
        self.use_sip_array = hasattr(sip, 'array') and use_sip_array
        self.alloc(0)

    def alloc(self, size):
        if self.use_sip_array:
            self.objs = sip.array(QtCore.QLineF, size)
            vp = sip.voidptr(self.objs, len(self.objs)*4*8)
            self.arr = np.frombuffer(vp, dtype=np.float64).reshape((-1, 4))
        else:
            self.arr = np.empty((size, 4), dtype=np.float64)
            self.objs = list(map(sip.wrapinstance,
                itertools.count(self.arr.ctypes.data, self.arr.strides[0]),
                itertools.repeat(QtCore.QLineF, self.arr.shape[0])))

    def get(self, size):
        if size != self.arr.shape[0]:
            self.alloc(size)
        return self.objs, self.arr

def generate_pattern(nsegs, size):
    rng = np.random.default_rng()
    x = rng.random(nsegs) * size
    y = rng.random(nsegs) * size
    arr = np.empty((nsegs, 4), dtype=np.float64)
    arr[:, 0] = x
    arr[:, 1] = y
    arr[:, 2] = x + 2
    arr[:, 3] = y + 2
    return arr

nsegs = 10_000
size = 500
nframes = 100
pattern = generate_pattern(nsegs, size)

def run(use_sip_array):
    # generate lines once
    segments = LineSegments(use_sip_array)
    lines, memory = segments.get(nsegs)
    memory[:] = pattern

    # draw multiple frames using the same lines array
    t0 = perf_counter()
    for _ in range(nframes):
        qimg = QtGui.QImage(size, size, QtGui.QImage.Format.Format_RGB32)
        qimg.fill(QtCore.Qt.GlobalColor.transparent)
        painter = QtGui.QPainter(qimg)
        painter.setPen(QtCore.Qt.GlobalColor.cyan)
        painter.drawLines(lines)
        painter.end()
    t1 = perf_counter()
    return t1 - t0

for use_sip_array in [False, True]:
    duration = run(use_sip_array)
    fps = int(nframes / duration)
    print(f'{use_sip_array=} {duration=:.3f} {fps=}')

@pijyoi pijyoi force-pushed the test_siparray branch 3 times, most recently from a5c6e41 to afc1fd8 Compare June 6, 2022 06:52
@pijyoi pijyoi changed the title try out PyQt sip.array make use of PyQt sip.array Jun 6, 2022
@pijyoi pijyoi marked this pull request as ready for review June 6, 2022 06:54
pijyoi added 2 commits June 14, 2022 07:04
'finite'
- all finite     -> handled by 'all'
- not all finite -> handled by 'array'

the previous implementation assumed that segs was a list, which is no
longer the case with sip.array
@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Jun 18, 2022

Note that https://pypi.org/project/PyQt6/6.3.1/ has been released and supports this feature.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jun 18, 2022

Note that https://pypi.org/project/PyQt6/6.3.1/ has been released and supports this feature.

Nice! Probably should get on someone to merge this ASAP 👀

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Jun 18, 2022

Tested this and got a performance improvement on both PlotSpeedTest and ScatterPlotSpeedTest on macOS; also considering this is now a zero-copy operation, this should be a vast improvement. Thanks for your efforts in implementing this @pijyoi !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants