piker/.claude/skills/pyqtgraph_rendering_optimiz...

240 lines
6.6 KiB
Markdown

# PyQtGraph Rendering Optimization Skill
Skill for researching and optimizing `pyqtgraph` graphics
primitives by leveraging `piker`'s existing extensions and
production-ready patterns.
## Research Flow
When tasked with optimizing rendering performance (particularly
for large datasets), follow this systematic approach:
### 1. Study Piker's Existing Primitives
Start by examining `piker.ui._curve` and related modules to
understand existing optimization patterns:
```python
# Key modules to review:
piker/ui/_curve.py # FlowGraphic, Curve, StepCurve
piker/ui/_editors.py # ArrowEditor, SelectRect
piker/ui/_annotate.py # Custom batch renderers
```
**Look for:**
- Use of `QPainterPath` for batch path rendering
- `QGraphicsItem` subclasses with custom `.paint()` methods
- Cache mode settings (`.setCacheMode()`)
- Coordinate system transformations (scene vs data vs pixel)
- Custom bounding rect calculations
### 2. Identify Upstream PyQtGraph Patterns
Once you understand piker's approach, search `pyqtgraph`
upstream for similar patterns:
**Key upstream modules:**
```python
pyqtgraph/graphicsItems/BarGraphItem.py
# Uses PrimitiveArray for batch rect rendering
pyqtgraph/graphicsItems/ScatterPlotItem.py
# Fragment-based rendering for large point clouds
pyqtgraph/functions.py
# Utility functions like makeArrowPath()
pyqtgraph/Qt/internals.py
# PrimitiveArray for batch drawing primitives
```
**Search techniques:**
- Look for `PrimitiveArray` usage (batch rect/point rendering)
- Find `QPainterPath` batching patterns
- Identify shared pen/brush reuse across items
- Check for coordinate transformation strategies
### 3. Apply Batch Rendering Patterns
**Core optimization principle:**
Creating individual `QGraphicsItem` instances is expensive.
Batch rendering eliminates per-item overhead.
**Pattern: Batch Rectangle Rendering**
```python
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
class BatchRectRenderer(pg.GraphicsObject):
def __init__(self, n_items):
super().__init__()
# allocate rect array once
self._rectarray = (
pg.Qt.internals.PrimitiveArray(QtCore.QRectF, 4)
)
# shared pen/brush (not per-item!)
self._pen = pg.mkPen('dad_blue', width=1)
self._brush = pg.functions.mkBrush('dad_blue')
def paint(self, p, opt, w):
# batch draw all rects in single call
p.setPen(self._pen)
p.setBrush(self._brush)
drawargs = self._rectarray.drawargs()
p.drawRects(*drawargs) # all at once!
```
**Pattern: Batch Path Rendering**
```python
class BatchPathRenderer(pg.GraphicsObject):
def __init__(self):
super().__init__()
self._path = QtGui.QPainterPath()
def paint(self, p, opt, w):
# single path draw for all geometry
p.setPen(self._pen)
p.setBrush(self._brush)
p.drawPath(self._path)
```
### 4. Handle Coordinate Systems Carefully
**Scene vs Data vs Pixel coordinates:**
```python
def paint(self, p, opt, w):
# save original transform (data -> scene)
orig_tr = p.transform()
# draw rects in data coordinates (zoom-sensitive)
p.setPen(self._rect_pen)
p.drawRects(*self._rectarray.drawargs())
# reset to scene coords for pixel-perfect arrows
p.resetTransform()
# build arrow path in scene/pixel coordinates
for spec in self._specs:
# transform data coords to scene
scene_pt = orig_tr.map(QPointF(x_data, y_data))
sx, sy = scene_pt.x(), scene_pt.y()
# arrow geometry in pixels (zoom-invariant!)
arrow_poly = QtGui.QPolygonF([
QPointF(sx, sy), # tip
QPointF(sx - 2, sy - 10), # left
QPointF(sx + 2, sy - 10), # right
])
arrow_path.addPolygon(arrow_poly)
p.drawPath(arrow_path)
# restore data coordinate system
p.setTransform(orig_tr)
```
### 5. Minimize Redundant State
**Share resources across all items:**
```python
# GOOD: one pen/brush for all items
self._shared_pen = pg.mkPen(color, width=1)
self._shared_brush = pg.functions.mkBrush(color)
# BAD: creating per-item (memory + time waste!)
for item in items:
item.setPen(pg.mkPen(color, width=1)) # NO!
```
### 6. Positioning and Updates
**For annotations that need repositioning:**
```python
def reposition(self, array):
'''
Update positions based on new array data.
'''
# vectorized timestamp lookups (not linear scans!)
time_to_row = self._build_lookup(array)
# update rect array in-place
rect_memory = self._rectarray.ndarray()
for i, spec in enumerate(self._specs):
row = time_to_row.get(spec['time'])
if row:
rect_memory[i, 0] = row['index'] # x
rect_memory[i, 1] = row['close'] # y
# ... width, height
# trigger repaint
self.update()
```
## Performance Expectations
**Individual items (baseline):**
- 1000+ items: ~5+ seconds to create
- Each item: ~5ms overhead (Qt object creation)
**Batch rendering (optimized):**
- 1000+ items: <100ms to create
- Single item: ~0.01ms per primitive in batch
- **Expected: 50-100x speedup**
## Common Pitfalls
1. **Don't mix coordinate systems within single paint call**
- Decide per-primitive: data coords or scene coords
- Use `p.transform()` / `p.resetTransform()` carefully
2. **Don't forget bounding rect updates**
- Override `.boundingRect()` to include all primitives
- Update when geometry changes via `.prepareGeometryChange()`
3. **Don't use ItemCoordinateCache for dynamic content**
- Use `DeviceCoordinateCache` for frequently updated items
- Or `NoCache` during interactive operations
4. **Don't trigger updates per-item in loops**
- Batch all changes, then single `.update()` call
## Example: Real-World Optimization
**Before (1285 individual pg.ArrowItem + SelectRect):**
```
Total creation time: 6.6 seconds
Per-item overhead: ~5ms
```
**After (single GapAnnotations batch renderer):**
```
Total creation time: 104ms (server) + 376ms (client)
Effective per-item: ~0.08ms
Speedup: ~36x client, ~180x server
```
## References
- `piker/ui/_curve.py` - Production FlowGraphic patterns
- `piker/ui/_annotate.py` - GapAnnotations batch renderer
- `pyqtgraph/graphicsItems/BarGraphItem.py` - PrimitiveArray
- `pyqtgraph/graphicsItems/ScatterPlotItem.py` - Fragments
- Qt docs: QGraphicsItem caching modes
## Skill Maintenance
Update this skill when:
- New batch rendering patterns discovered in pyqtgraph
- Performance bottlenecks identified in piker's rendering
- Coordinate system edge cases encountered
- New Qt/pyqtgraph APIs become available
---
*Last updated: 2026-01-31*
*Session: Batch gap annotation optimization*