Skip to content

feat (display): RTTank-based Progress Bar backend — scale, alarm limits, and stacking support (opt-in via preference)#3768

Open
emilioheredia-source wants to merge 11 commits intoControlSystemStudio:masterfrom
emilioheredia-source:progressbar_scale
Open

feat (display): RTTank-based Progress Bar backend — scale, alarm limits, and stacking support (opt-in via preference)#3768
emilioheredia-source wants to merge 11 commits intoControlSystemStudio:masterfrom
emilioheredia-source:progressbar_scale

Conversation

@emilioheredia-source
Copy link
Copy Markdown

@emilioheredia-source emilioheredia-source commented Apr 4, 2026

Summary

This PR introduces a completely new rendering backend for the existing ProgressBar widget, replacing the default JavaFX ProgressBar control with an RTTank-based implementation.

The RTTank backend uses a different rendering path than the stock JavaFX control. In practice update responsiveness can be similar, but it comes at the cost of higher CPU usage (the RT rendering loop is always spinning). For a more detailed discussion of rendering performance and CPU trade-offs, see the comments in #3766, which includes side-by-side video comparisons of both backends under different settings. Because the widget also looks slightly different from the original (font, scale markings, fill aesthetics), we chose not to make this the default — doing so would introduce visual breaking changes for every existing .bob screen that uses a ProgressBar.

Instead, the new backend is exposed as a hidden, opt-in preference:

org.csstudio.display.builder.representation/progressbar_scale_mode=false
  • When false (default): the original ProgressBarRepresentation is used — no change to existing screens whatsoever.
  • When true: RTProgressBarRepresentation is used instead, globally, for all Progress Bar widgets in that Phoebus instance.

This approach keeps the change non-breaking and makes the new backend accessible and testable in production-like conditions. If it proves stable and the visual difference is accepted (or resolved upstream), it could be made the default in a future release.

Visual examples: Side-by-side recordings of the original and RTTank-based Progress Bar (both at default and high-update-rate settings) are posted in the comments of #3766. Look for the comment that contains four embedded videos showing both rendering backends in action.


Design rationale and trade-offs

Dispatching two distinct rendering implementations under the same widget type is a known architectural compromise. The cleaner alternatives — introducing a dedicated widget type, or adding a per-widget renderer selector — were considered but ruled out: a new widget type would require migrating all existing .bob files, while a per-widget selector would complicate the model/representation contract without offering a clear path to adoption.

The global preference avoids both problems. It has no impact on the widget model, is trivially reversible, and allows the new backend to be evaluated on real operational screens without any file changes. This is treated as an interim approach: if the implementation proves stable and the visual differences are accepted or resolved, the new backend could be promoted to the default in a future release.


New capabilities (available only when progressbar_scale_mode=true)

Scale and axis

  • Numeric scale rendered by YAxisImpl alongside the bar
  • Configurable scale_visible, limits_from_pv, minimum, maximum, major_ticks, minor_ticks
  • show_minor_ticks, perpendicular_tick_labels properties respected

Alarm limits

  • Draws coloured alarm-limit markers (LOLO / LO / HI / HIHI) on the scale when show_limits=true

Look parity with Tank

  • foreground_color, background_color, fill_color, empty_color
  • skin (none / classic / button) from ScaledPVWidget
  • flat_track — when true, the fill level is drawn as a flat horizontal band

New properties (contributed by PR2 infrastructure)

  • show_scale_labels (boolean, default true): when false, tick marks are drawn but all label text is suppressed — useful for stacking multiple bars that share a single labelled reference scale.
  • inner_padding (integer 0–20 px, default 3): controls the gap between the fill bar and the widget border; replaces the previously hardcoded 3 px value.

Dependencies


Files changed

Area Files
RTPlot axis YAxisImpl.java
RTTank RTTank.java
Widget model ScaledPVWidget.java, TankWidget.java, ProgressBarWidget.java, Messages.java, messages.properties
Representations TankRepresentation.java, RTProgressBarRepresentation.java, RepresentationFactory.java, ProgressBarRepresentation.java
Preferences representation_preferences.properties
Changelog changelog.rst

Testing

Default / backward-compatibility check

  1. Open Phoebus without the preference set — all existing Progress Bar widgets look and behave identically to before. No visual or behavioral regressions expected.

Enable the new backend

  1. Add org.csstudio.display.builder.representation/progressbar_scale_mode=true to phoebus-settings/settings.ini and restart.
  2. Existing .bob files open normally; the widget editor now shows all new properties.

Scale and axis

  1. Toggle scale_visible on/off — scale appears and disappears cleanly.
  2. Set minimum and maximum explicitly; verify the axis range and tick spacing update.
  3. Set major_ticks and minor_ticks counts; verify tick density changes on the scale.
  4. Toggle show_minor_ticks — minor tick marks appear/disappear without affecting major ticks.
  5. Toggle perpendicular_tick_labels — labels rotate between parallel and perpendicular.

Number format

  1. Change the widget's format and precision properties; verify axis labels update accordingly.

Alarm limits

  1. Enable limits_from_pv and connect a PV that has LOLO/LO/HI/HIHI alarm limits defined; verify coloured marker lines appear on the scale at the correct positions.
  2. Toggle show_limits off — markers disappear while the scale remains.

Look and colours

  1. Change foreground_color, background_color, fill_color, empty_color — verify each affects the correct part of the widget.
  2. Cycle through skin values (none / classic / button) and verify the fill style changes.
  3. Toggle flat_track — verify the fill switches between a tapered and a flat horizontal band.

New properties

  1. Set show_scale_labels=false on both a Tank and a ProgressBar — tick marks must remain visible but all label text must be suppressed.
  2. Change inner_padding from 0 to 20 — verify the gap between the fill bar and the border grows and shrinks live without requiring a restart.

No-regression on Tank widget

  1. Open a screen with Tank widgets (not ProgressBar); confirm show_scale_labels and inner_padding work identically there, since the properties are shared via RTTank.

Switch ProgressBarWidget from PVWidget to ScaledPVWidget and replace
the JFX ProgressBar control with RTTank (border width hardcoded to 0).

ProgressBarWidget inherits format, precision, alarm limit lines and
alarm colours from ScaledPVWidget.  New properties scale_visible and
show_minor_ticks (both off/on by default, matching Tank) are added.
All existing .bob XML property names are preserved (limits_from_pv,
minimum, maximum, fill_color, background_color, horizontal, log_scale)
so existing files load without migration.

ProgressBarRepresentation wires the same lookChanged / valueChanged /
limitsChanged / orientationChanged pattern as TankRepresentation and
maps background_color to both setBackground() and setEmptyColor() so
the unfilled area matches the declared background.
…and perpendicular labels to ProgressBar

Move propScaleVisible, propShowMinorTicks, propOppositeScaleVisible, and
propPerpendicularTickLabels from TankWidget to their proper home in
ScaledPVWidget. This removes the TankWidget import smell in ProgressBarWidget
and makes all four properties available to any ScaledPVWidget subclass.

ProgressBarWidget gains opposite_scale_visible and perpendicular_tick_labels,
wired through ProgressBarRepresentation to RTTank.setRightScaleVisible() and
RTTank.setPerpendicularTickLabels() respectively.
… ProgressBar

propTankBorderWidth (XML: 'tank_border_width') removed from TankWidget.
Replaced by propBorderWidth (XML: 'border_width') in ScaledPVWidget so
all scaled widgets share the same descriptor.

TankWidget.CustomConfigurator migrates old 'tank_border_width' XML elements
to 'border_width' transparently, so existing .bob files still load.

ProgressBarWidget gains border_width_prop (default 0), wired to
RTTank.setBorderWidth() in ProgressBarRepresentation.
… format()

In compute(), relabelTicks() is now called after the major tick list is
built so that the user-specified NumberFormat override (set via
setLabelFormat()) takes effect for all ticks, including the forced
boundary ticks added by the guard at the end.

format() now checks getLabelFormatOverride() directly so that one-off
calls (e.g. from axis tooltip code) also respect the override, rather
than always falling back to num_fmt.

RTTank.significantDigitsFormat() now post-processes String.format('%g')
output via normaliseExponent(): uppercase E, no leading zeros on the
exponent, no '+' sign — matching the style produced by
LinearTicks.createExponentialFormat() and standard scientific notation
(MATLAB, NumPy, LabVIEW).
…M/BOY converters

ScaledPVWidget: fix javadoc ('show_limits' -> 'show_alarm_limits');
reorder imports to standard Java convention (static, java.*, org.*).

TankWidget: remove the tank_border_width XML migration block that was
added prematurely before any .bob files using that key existed.

ProgressBarWidget: drop three dead imports left over after property
descriptors moved to ScaledPVWidget. Add BOY OPI migration for
<show_scale> -> scale_visible and <scale_font> -> font, which do not
auto-map because their XML element names differ.

ProgressBarRepresentation: remove stale setBorderWidth(0) call and its
misleading 'always hidden' comment from createJFXNode(); the property
is now user-configurable. Update class javadoc to match.

Convert_activeBarClass (EDM -> Phoebus): map five previously ignored
EDM properties: showScale -> scale_visible; precision (when > 0);
min/max when limitsFromDb is false and the range is valid; border
boolean -> 1 px border_width.
String.replaceFirst() recompiles the Pattern on every call.
Move the pattern to a static final EXP_LEADING_ZEROS constant so it
is compiled once at class-load time.
Three related improvements to widget rendering performance and architecture:

## 1. RTTank parallel rendering (rtplot)

Root cause: UpdateThrottle.TIMER is a single-thread shared executor.  On
displays with many Tank/ProgressBar widgets all renders serialise on one
thread, capping throughput at ~5 s/sweep for 200 widgets.

Fix: RTTank constructor now passes Activator.thread_pool (N-core pool) when
parallel_rendering=true, TIMER otherwise.  Default is false — no behaviour
change for existing installs.

New preference: org.csstudio.javafx.rtplot/parallel_rendering (default false)

## 2. ProgressBar scale-mode rendering (display)

New preference org.csstudio.display.builder.representation/progressbar_scale_mode:
- false (default): stock JFX ProgressBar — original behaviour, no change
- true: RTProgressBarRepresentation backed by RTTank, adding a numeric scale,
  tick format/precision, dual scale, alarm-limit lines, and parallel rendering

Architecture:
- RTScaledWidgetRepresentation<W extends ScaledPVWidget>: new abstract base
  class that eliminates ~90 % duplication between TankRepresentation and
  RTProgressBarRepresentation.  Handles value/range updates, alarm limits,
  orientation transforms, and throttle wiring in one place.
- TankRepresentation: refactored to a thin subclass (~30 lines).
- RTProgressBarRepresentation: new thin subclass for the scale-mode bar.
- ProgressBarRepresentation: restored to the unmodified upstream JFX bar
  (closed for modification).
- BaseWidgetRepresentations: one-line conditional dispatch on preference.

Performance fix in RTScaledWidgetRepresentation.valueChanged():
  scale.setValueRange() (tick layout recomputation) is now skipped when a
  pure PV value arrives and limits_from_pv=false, saving needless work at
  up to 20 Hz per widget.

## 3. Property panel filtering (editor)

When progressbar_scale_mode=false, scale-only properties (scale_visible,
format, precision, alarm limits, etc.) are hidden in the property editor
via a set-membership check in PropertyPanelSection.fill().  Uses
ProgressBarWidget.SCALE_MODE_PROPS — a static constant in the model class —
so the editor (which already depends on representation) can read the
preference and apply the filter with no new module dependencies.

Tested on CLS OPI workstation with 200 RTTank widgets (Tank + ProgressBar
mix).  With parallel_rendering=true the visible refresh lag drops from ~5 s
to <200 ms.
border_alarm_sensitive and border_width are handled by RegionBaseRepresentation
for all RegionBaseRepresentation subclasses, regardless of rendering mode.
They were incorrectly included in SCALE_MODE_PROPS, which caused the property
editor to hide them when progressbar_scale_mode=false — preventing users from
setting alarm-sensitive borders or custom widget borders on the classic JFX
progress bar.

Also add 'requires restart' note to parallel_rendering preference comment
for consistency with progressbar_scale_mode documentation.
Add two RTTank rendering modes used only by RTProgressBarRepresentation:

flat_track: paints the unfilled track with a solid colour instead of the
  default left→center gradient. The stock JFX ProgressBar has a flat
  background; the gradient was appearing as a dark band in the middle of
  the track background, opposite to the intended visual.

inner_padding: extra inset from all four canvas edges to the plot body.
  When scale_visible=false the canvas fills edge-to-edge; 3 px of padding
  recreates the inset margin of the default JFX ProgressBar CSS (~7 px
  total, ~3 px per side visible), so .bob files from older Phoebus versions
  render consistently after enabling progressbar_scale_mode.
  Set to 0 automatically when scale_visible=true (the scale label area
  provides the visual framing).

Both features default to off so the Tank widget is completely unaffected.
RTProgressBarRepresentation activates them in configureTank() / applyLookToTank().
…ressBar

Two new widget properties extending the RTTank rendering path:

## show_scale_labels  (Tank + ProgressBar, default: true)

When false, the axis scale draws tick marks but suppresses all label
text.  This enables tight stacked layouts where one labelled widget
leads and the rest show aligned tick marks only, saving horizontal (or
vertical) space without losing the alignment grid.

Changes:
  YAxisImpl   - volatile boolean show_labels (render thread / JFX thread)
              - getDesiredPixelSize: short-circuits to TICK_LENGTH when false
              - getPixelGaps: returns {0,0} — no label overhang at endpoints
              - paint: skips drawTickLabel + paintLabels when false
  RTTank      - setScaleLabelsVisible() delegates to both YAxisImpl axes;
                axes fire requestLayout/requestRefresh via plot_part_listener,
                so RTTank needs no extra need_layout or requestUpdate calls
  ScaledPVWidget  - propShowScaleLabels descriptor (shared base)
  TankWidget      - show_scale_labels property (default true)
  TankRepresentation  - listener + applyLookToTank call

## inner_padding  (ProgressBar only, default: 3, range: 0..20)

Configurable inset between the widget edge and the fill bar.  Replaces
the previous hardcoded 'scale_visible ? 0 : 3' logic. Set to 0 for an
edge-to-edge bar; keep at 3 to match the stock JFX ProgressBar CSS margin.

Changes:
  ProgressBarWidget      - propInnerPadding descriptor + property + accessor
                         - inner_padding added to SCALE_MODE_PROPS filter
                         - show_scale_labels added to SCALE_MODE_PROPS filter
  RTProgressBarRepresentation  - listener + applyLookToTank call

## Review fixes (same batch)

  - volatile on YAxisImpl.show_labels (thread-safety: render vs JFX thread)
  - removed redundant need_layout+requestUpdate in RTTank.setScaleLabelsVisible
  - corrected stale configureTank() javadoc in RTProgressBarRepresentation
  - updated ProgressBarWidget class javadoc to mention new properties
  - fixed double blank line in ProgressBarWidget imports
  - Messages.java + messages.properties: ShowScaleLabels, InnerPadding
  - docs/source/changelog.rst: ShowScaleLabels + InnerPadding entries added
@emilioheredia-source
Copy link
Copy Markdown
Author

This is likely my last new-feature PR for a while. If this one is accepted, the natural next step would be the Thermometer widget — applying the same opt-in backend pattern to add a scale, configurable label formats, and alarm limits, reusing the RTScaledWidgetRepresentation base class that Tank and this new ProgressBar implementation already share.
Thanks again for considering and taking the time to review these. Cheers.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 4, 2026

@emilioheredia-source
Copy link
Copy Markdown
Author

SonarCloud findings — triage note

SonarCloud flagged 54 issues on this PR. I reviewed all of them; here is the breakdown.

Fixed (2 issues introduced by this PR)

  • RTTank.java — "commented-out code" at L602: the multi-line inset comment contained arithmetic expressions that Sonar mis-identified as dead code. Condensed to a single descriptive line.
  • RTTank.java — cognitive complexity > 15 at L625 (updateImageBuffer): extracted the alarm-limit drawing block into a private drawAlarmLimits() helper, bringing complexity from 17 down to ~14.

Both fixes are in the latest commit on this branch.

Pre-existing / false positives (52 issues not touched)

Category Example Reason not fixed
snake_case field names fill_color, scale_visible, inner_padding, … Codebase-wide convention used by every Widget, RTPlot class, and the @Preference framework. Renaming our fields would be inconsistent.
"Extract assignment from expression" in defineProperties properties.add(x = prop.create(...)) Standard pattern in every Widget class in the project (pre-existing, not introduced here).
@Preference fields not final (parallel_rendering, progressbar_scale_mode) Activator.java, Preferences.java The AnnotatedPreferences framework writes to these fields at startup via reflection — they cannot be final. Same pattern in all other Activator/Preferences classes.
Messages.java fields not static final WidgetProperties_InnerPadding, WidgetProperties_ShowScaleLabels NLS framework populates them at class-load time via reflection, as it does for all 400+ existing fields in that file.
Method name clashes with superclass field (Blocker) propBorderWidth(), propScaleVisible(), … in ProgressBarWidget The same methods and pattern already exist in TankWidget (flagged separately as pre-existing issues in Sonar, visible with age "3 days"). We replicated the established pattern. The correct fix would be a broader refactor of the Widget accessor convention, which is out of scope for this PR.

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