From 9ef82fb898ce0f0de8270b5da7a16a37a194a249 Mon Sep 17 00:00:00 2001 From: tiresiasfromthebai Date: Wed, 15 Jul 2026 22:18:41 +0200 Subject: [PATCH] perf: coalesce Array Visualizer rebuilds and halve scatter restyle In scatter mode a two-array (X/Y) plot rebuilt the table and chart once per property setter (data, offset, stride) and restyled every marker four times, so a ~4000-point phase portrait froze for ~0.8s per refresh. - SeerArrayWidget: add a BulkUpdate RAII guard that defers create() until a burst of setters completes, so a refresh does one rebuild, not several. - redecorateScatterSeries: drop the two redundant transparent passes; after addSeries() the series holds the theme pen/brush, so setPen(NoPen)/ setBrush() are already real transitions and either re-pushes both to the markers. ~800ms -> ~200ms per refresh at 4000 points; line/spline and marker appearance unchanged. Add tests/helloscatter, a 4000-point two-array phase portrait with a breakpoint loop, to reproduce the slow refresh. --- src/SeerArrayVisualizerWidget.cpp | 31 +++++++++++++----- src/SeerArrayWidget.cpp | 37 ++++++++++++++++++++++ src/SeerArrayWidget.h | 20 ++++++++++++ tests/helloscatter/Makefile | 20 ++++++++++++ tests/helloscatter/helloscatter.cpp | 49 +++++++++++++++++++++++++++++ 5 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 tests/helloscatter/Makefile create mode 100644 tests/helloscatter/helloscatter.cpp diff --git a/src/SeerArrayVisualizerWidget.cpp b/src/SeerArrayVisualizerWidget.cpp index c4d6dec..7dcb88b 100644 --- a/src/SeerArrayVisualizerWidget.cpp +++ b/src/SeerArrayVisualizerWidget.cpp @@ -28,11 +28,18 @@ // Qt Charts 6.x workaround: scatter marker items can be (re)created without // inheriting the series' pen/brush, leaving near-invisible ~1px markers. // Small arrays usually render fine; large ones (e.g. 4000 points) reliably -// degrade. The series-level state is correct in both cases (verified with an -// instrumented build), so the style is lost at the marker-item level inside -// Qt Charts. Because the series setters are guarded (a call with an unchanged -// value is a no-op), we force two *actual* value transitions so the style is -// pushed down to the marker items unconditionally. +// degrade. The series-level state is correct in both cases, so the style is +// lost at the marker-item level inside Qt Charts. Re-applying the pen/brush +// after the series is added pushes the style down to the marker items. +// +// Two passes suffice, not four. This runs after addSeries(), so the series +// already carries the chart theme's pen/brush; setPen(NoPen) and setBrush() +// are therefore real value transitions (no built-in Qt theme uses NoPen for +// a scatter pen nor this exact colour for the brush) and are not swallowed +// by the guarded setters. Even if one were a no-op, the other setter's +// update re-pushes both pen and brush to every marker item. Two passes over +// the (up to thousands of) marker items instead of four roughly halves this +// step's cost, with no visible change to the markers. // static void redecorateScatterSeries (QXYSeries* series) { @@ -42,8 +49,6 @@ static void redecorateScatterSeries (QXYSeries* series) { return; } - scatter->setPen(QPen(Qt::transparent)); - scatter->setBrush(QBrush(Qt::transparent)); scatter->setPen(QPen(Qt::NoPen)); scatter->setBrush(QBrush(QColor(31, 119, 180))); } @@ -164,6 +169,10 @@ void SeerArrayVisualizerWidget::setAVariableName (const QString& name) { return; } + // Coalesce this variable's setup (address reset + data/offset/stride) into + // a single table rebuild; the guard flushes it when it leaves scope. + SeerArrayWidget::BulkUpdate bulk(arrayTableWidget); + setAVariableAddress(""); // Clear old contents. @@ -282,6 +291,10 @@ void SeerArrayVisualizerWidget::setBVariableName (const QString& name) { return; } + // Coalesce this variable's setup (address reset + data/offset/stride) into + // a single table rebuild; the guard flushes it when it leaves scope. + SeerArrayWidget::BulkUpdate bulk(arrayTableWidget); + setBVariableAddress(""); // Clear old contents. @@ -540,6 +553,8 @@ void SeerArrayVisualizerWidget::handleText (const QString& text) { // Give the byte array to the hex widget. bool ok; + SeerArrayWidget::BulkUpdate bulk(arrayTableWidget); + arrayTableWidget->setAData(arrayTableWidget->aLabel(), new SeerArrayWidget::DataStorageArray(array)); if (aArrayOffsetLineEdit->text() != "") { @@ -587,6 +602,8 @@ void SeerArrayVisualizerWidget::handleText (const QString& text) { // Give the byte array to the hex widget. bool ok; + SeerArrayWidget::BulkUpdate bulk(arrayTableWidget); + arrayTableWidget->setBData(arrayTableWidget->bLabel(), new SeerArrayWidget::DataStorageArray(array)); if (bArrayOffsetLineEdit->text() != "") { diff --git a/src/SeerArrayWidget.cpp b/src/SeerArrayWidget.cpp index f330233..8062fec 100644 --- a/src/SeerArrayWidget.cpp +++ b/src/SeerArrayWidget.cpp @@ -33,6 +33,9 @@ SeerArrayWidget::SeerArrayWidget(QWidget* parent) : QTableWidget(parent) { _bAddressOffset = 0; _bAddressStride = 1; + _bulkUpdateDepth = 0; + _createPending = false; + setAAddressOffset(0); setAAddressStride(1); @@ -314,6 +317,14 @@ void SeerArrayWidget::setBData(const QString& label, SeerArrayWidget::DataStorag void SeerArrayWidget::create () { + // Coalesce a burst of property changes: while a bulk update is active, + // defer the (expensive) table rebuild and just record that one is pending. + // endBulkUpdate() performs the single rebuild once the burst is complete. + if (_bulkUpdateDepth > 0) { + _createPending = true; + return; + } + // Clear the table. We're going to recreate it. clear(); setRowCount(0); @@ -584,6 +595,32 @@ void SeerArrayWidget::create () { emit dataChanged(); } +void SeerArrayWidget::beginBulkUpdate () { + + _bulkUpdateDepth++; +} + +void SeerArrayWidget::endBulkUpdate () { + + if (_bulkUpdateDepth > 0) { + _bulkUpdateDepth--; + } + + // Once fully unwound, do the single deferred rebuild (if any setter fired). + if (_bulkUpdateDepth == 0 && _createPending) { + _createPending = false; + create(); + } +} + +SeerArrayWidget::BulkUpdate::BulkUpdate (SeerArrayWidget* widget) : _widget(widget) { + _widget->beginBulkUpdate(); +} + +SeerArrayWidget::BulkUpdate::~BulkUpdate () { + _widget->endBulkUpdate(); +} + SeerArrayWidget::DataStorageArray::DataStorageArray(const QByteArray& arr) { _data = arr; } diff --git a/src/SeerArrayWidget.h b/src/SeerArrayWidget.h index b6c5e2f..d1dc90c 100644 --- a/src/SeerArrayWidget.h +++ b/src/SeerArrayWidget.h @@ -46,6 +46,21 @@ class SeerArrayWidget: public QTableWidget { int elementsPerLine () const; + // RAII guard: coalesces a burst of property changes into a single + // rebuild. While a guard is alive create() is deferred; the single + // rebuild happens when it goes out of scope. Nesting is supported. + // Prefer a guard over the private begin/end pair so the matching end + // is guaranteed even on early return or exception. + class BulkUpdate { + public: + explicit BulkUpdate (SeerArrayWidget* widget); + ~BulkUpdate (); + BulkUpdate (const BulkUpdate&) = delete; + BulkUpdate& operator= (const BulkUpdate&) = delete; + private: + SeerArrayWidget* _widget; + }; + const QString& aAxis () const; void setAAxis (const QString& axis); const QString& aLabel () const; @@ -88,6 +103,11 @@ class SeerArrayWidget: public QTableWidget { private: void create (); + void beginBulkUpdate (); + void endBulkUpdate (); + + int _bulkUpdateDepth; + bool _createPending; QString _aAxis; QString _aLabel; diff --git a/tests/helloscatter/Makefile b/tests/helloscatter/Makefile new file mode 100644 index 0000000..995ab49 --- /dev/null +++ b/tests/helloscatter/Makefile @@ -0,0 +1,20 @@ +# This is the default target, which will be built when +# you invoke make +.PHONY: all +all: helloscatter + +# This rule tells make how to build helloscatter from helloscatter.cpp +helloscatter: helloscatter.cpp + g++ -g -o helloscatter helloscatter.cpp + +# This rule tells make to copy helloscatter to the binaries subdirectory, +# creating it if necessary +.PHONY: install +install: + mkdir -p binaries + cp -p helloscatter binaries + +# This rule tells make to delete helloscatter +.PHONY: clean +clean: + rm -f helloscatter helloscatter.o diff --git a/tests/helloscatter/helloscatter.cpp b/tests/helloscatter/helloscatter.cpp new file mode 100644 index 0000000..b831546 --- /dev/null +++ b/tests/helloscatter/helloscatter.cpp @@ -0,0 +1,49 @@ +#include + +#define NPOINTS 4000 + +double pos[NPOINTS]; +double vel[NPOINTS]; + +// +// Fill pos/vel with the trajectory of a damped harmonic oscillator +// (semi-implicit Euler integration). +// +void integrate (double damping) { + + double x = 1.0; + double v = 0.0; + double w = 1.0; + double dt = 0.005; + + for (int i=0; i