Viewer: rate-limit hover feedback to ~15 Hz
The status bar, the resolution readout and the magnifier were all regenerated on every single mouse motion event. Each regeneration repaints, and on a remote X session a repaint uploads the whole window regardless of how little changed, so the pointer merely crossing the image saturates the link: measured with a counting relay in front of the X server, 50 motions over the image cost 273 MB, and a build with the hover work removed cost 18 KB. Rate-limit it. Two details matter: - The limit is applied inline, not from a timer. Running the update inside the mouse event keeps its damage in the same repaint as anything else that event triggers (a pan). A first attempt deferred the work to a timer instead, which split one repaint into two and made panning measurably worse. - The catch-up that reports the final position is debounced, not queued per skipped motion, so it fires once after the pointer stops rather than repeatedly mid-gesture. mouseHover() now takes the scene position and modifiers instead of the event, which also removes the identical mapToScene() from all four implementations. Hover traffic over 3 repeats: 173 MB mean -> 140 MB, and the run-to-run spread drops from +-14% to +-2%. The harness tops out near 30 motions/s, barely above the 15 Hz limit; a real mouse reports far faster, where the cap does more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,10 +77,9 @@ void JFJochAzIntImage::imageLoaded(std::shared_ptr<const JFJochReaderImage> in_i
|
||||
}
|
||||
}
|
||||
|
||||
void JFJochAzIntImage::mouseHover(QMouseEvent* event) {
|
||||
void JFJochAzIntImage::mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers) {
|
||||
if (!scene() || !image || W == 0 || H == 0) return;
|
||||
|
||||
QPointF scenePos = mapToScene(event->pos());
|
||||
int x = static_cast<int>(scenePos.x());
|
||||
int y = static_cast<int>(scenePos.y());
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class JFJochAzIntImage : public JFJochImage {
|
||||
float range_max = 1.0f;
|
||||
std::shared_ptr<const JFJochReaderImage> image;
|
||||
|
||||
void mouseHover(QMouseEvent* event) override;
|
||||
void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) override;
|
||||
void Clear();
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
signals:
|
||||
|
||||
@@ -79,8 +79,7 @@ void JFJochDiffractionImage::azimuthalHandles(const ROIAzimuthal &az, const Diff
|
||||
phimax = pt(d_mid, phi1);
|
||||
}
|
||||
|
||||
void JFJochDiffractionImage::mouseHover(QMouseEvent *event) {
|
||||
auto coord = mapToScene(event->pos());
|
||||
void JFJochDiffractionImage::mouseHover(const QPointF &coord, Qt::KeyboardModifiers) {
|
||||
|
||||
if (image && (coord.x() >= 0)
|
||||
&& (coord.x() < image->Dataset().experiment.GetXPixelsNum())
|
||||
|
||||
@@ -102,7 +102,7 @@ private:
|
||||
|
||||
float ice_ring_width_Q_recipA = 0.01;
|
||||
|
||||
void mouseHover(QMouseEvent* event) override;
|
||||
void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) override;
|
||||
|
||||
signals:
|
||||
void roiGeometryEdited(ROIDefinition rois);
|
||||
|
||||
@@ -63,13 +63,11 @@ void JFJochGridScanImage::loadData(const std::vector<float> &data, const GridSca
|
||||
CalcROI();
|
||||
}
|
||||
|
||||
void JFJochGridScanImage::mouseHover(QMouseEvent *event) {
|
||||
void JFJochGridScanImage::mouseHover(const QPointF &pt, Qt::KeyboardModifiers modifiers) {
|
||||
// Map mouse position to image pixel if inside bounds
|
||||
if (W == 0 || H == 0 || image_index.empty())
|
||||
return;
|
||||
|
||||
const QPointF pt = mapToScene(event->pos());
|
||||
|
||||
// Convert view coordinates to image pixel by truncation
|
||||
int x = static_cast<int>(pt.x());
|
||||
int y = static_cast<int>(pt.y());
|
||||
@@ -82,7 +80,7 @@ void JFJochGridScanImage::mouseHover(QMouseEvent *event) {
|
||||
return;
|
||||
int64_t image_id = image_index[idx];
|
||||
if (image_id >= 0) {
|
||||
if (event->modifiers() & Qt::ShiftModifier)
|
||||
if (modifiers & Qt::ShiftModifier)
|
||||
emit imageSelected(image_id);
|
||||
|
||||
if (one_over_d2) {
|
||||
|
||||
@@ -17,7 +17,7 @@ class JFJochGridScanImage : public JFJochImage {
|
||||
int64_t current_image_H = -1;
|
||||
bool one_over_d2 = false;
|
||||
|
||||
void mouseHover(QMouseEvent *event) override;
|
||||
void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
void loadImage(QMouseEvent *event);
|
||||
void addCustomOverlay() override;
|
||||
|
||||
@@ -61,6 +61,10 @@ JFJochImage::JFJochImage(QWidget *parent) : QGraphicsView(parent) {
|
||||
|
||||
// Optional: a sensible default colormap
|
||||
color_scale.Select(ColorScaleEnum::Indigo);
|
||||
|
||||
hover_tail_timer_ = new QTimer(this);
|
||||
hover_tail_timer_->setSingleShot(true);
|
||||
connect(hover_tail_timer_, &QTimer::timeout, this, &JFJochImage::UpdateHover);
|
||||
}
|
||||
|
||||
void JFJochImage::onScroll(int value) {
|
||||
@@ -69,6 +73,27 @@ void JFJochImage::onScroll(int value) {
|
||||
updateOverlay();
|
||||
}
|
||||
|
||||
void JFJochImage::UpdateHover() {
|
||||
hover_rate_.restart();
|
||||
mouseHover(hover_scene_pos_, hover_modifiers_);
|
||||
emit hoverScenePos(hover_scene_pos_);
|
||||
}
|
||||
|
||||
void JFJochImage::ScheduleHoverUpdate(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) {
|
||||
hover_scene_pos_ = scenePos;
|
||||
hover_modifiers_ = modifiers;
|
||||
|
||||
if (!hover_rate_.isValid() || hover_rate_.elapsed() >= kHoverIntervalMs) {
|
||||
hover_tail_timer_->stop();
|
||||
UpdateHover();
|
||||
return;
|
||||
}
|
||||
|
||||
// Too soon. Push the catch-up back instead of queueing one per skipped motion, so it fires
|
||||
// once, after the pointer stops -- a catch-up that fires mid-gesture is an extra repaint.
|
||||
hover_tail_timer_->start(kHoverIntervalMs);
|
||||
}
|
||||
|
||||
void JFJochImage::ScheduleRenderImage() {
|
||||
if (render_pending_)
|
||||
return;
|
||||
@@ -244,8 +269,7 @@ void JFJochImage::mouseMoveEvent(QMouseEvent *event) {
|
||||
return;
|
||||
|
||||
const QPointF scenePos = mapToScene(event->pos());
|
||||
mouseHover(event);
|
||||
emit hoverScenePos(scenePos);
|
||||
ScheduleHoverUpdate(scenePos, event->modifiers());
|
||||
QPointF delta;
|
||||
|
||||
switch (mouse_event_type) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QTimer>
|
||||
#include <QGraphicsView>
|
||||
#include <QGraphicsItem>
|
||||
#include <QImage>
|
||||
@@ -151,7 +153,24 @@ protected:
|
||||
QRectF roiBox;
|
||||
|
||||
static QPointF RoundPoint(const QPointF& p);
|
||||
virtual void mouseHover(QMouseEvent* event) = 0;
|
||||
virtual void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) = 0;
|
||||
|
||||
// Hover feedback (status bar, resolution readout, magnifier) used to be regenerated on every
|
||||
// single mouse motion. Each regeneration dirties the window, and on a remote X session every
|
||||
// repaint costs a full-window pixel upload, so the thing to minimise is the number of
|
||||
// repaints. ~15 Hz is far below the motion event rate and well above what the eye follows.
|
||||
//
|
||||
// The rate limit is applied inline rather than from a timer on purpose: running the update
|
||||
// inside the mouse event keeps its damage in the same repaint as anything else that event
|
||||
// triggers (a pan), instead of costing a second one. The timer only covers the tail, so the
|
||||
// final position is still reported once the pointer stops.
|
||||
static constexpr int kHoverIntervalMs = 66;
|
||||
void ScheduleHoverUpdate(const QPointF &scenePos, Qt::KeyboardModifiers modifiers);
|
||||
void UpdateHover();
|
||||
QPointF hover_scene_pos_;
|
||||
Qt::KeyboardModifiers hover_modifiers_ = Qt::NoModifier;
|
||||
QElapsedTimer hover_rate_;
|
||||
QTimer *hover_tail_timer_ = nullptr;
|
||||
|
||||
ResizeHandle hitTestROIHandle(const QPointF& scenePos, qreal tol = 3.0) const;
|
||||
|
||||
|
||||
@@ -41,9 +41,8 @@ void JFJochSimpleImage::setImage(std::shared_ptr<const SimpleImage> img) {
|
||||
}
|
||||
|
||||
|
||||
void JFJochSimpleImage::mouseHover(QMouseEvent *event) {
|
||||
void JFJochSimpleImage::mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers) {
|
||||
if (image_) {
|
||||
const QPointF scenePos = mapToScene(event->pos());
|
||||
// Hover feedback / status bar display
|
||||
if ((scenePos.x() >= 0)
|
||||
&& (scenePos.x() < image_->image.GetWidth())
|
||||
|
||||
@@ -25,7 +25,7 @@ class JFJochSimpleImage : public JFJochImage {
|
||||
void loadImageInternal(const uint8_t *input);
|
||||
void loadImageInternal();
|
||||
|
||||
void mouseHover(QMouseEvent *event) override;
|
||||
void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) override;
|
||||
public:
|
||||
explicit JFJochSimpleImage(QWidget *parent = nullptr);
|
||||
public slots:
|
||||
|
||||
Reference in New Issue
Block a user