2222import java .util .logging .Level ;
2323import java .util .stream .Collectors ;
2424
25+ import org .phoebus .applications .alarm .AlarmSystem ;
26+ import org .phoebus .applications .alarm .ResettableTimeout ;
2527import org .phoebus .applications .alarm .client .AlarmClient ;
2628import org .phoebus .applications .alarm .client .AlarmClientLeaf ;
2729import org .phoebus .applications .alarm .client .AlarmClientListener ;
@@ -85,8 +87,28 @@ public class AlarmTreeView extends BorderPane implements AlarmClientListener
8587 private final Label no_server = AlarmUI .createNoServerLabel ();
8688 private final TreeView <AlarmTreeItem <?>> tree_view = new TreeView <>();
8789
90+ /** Model with current alarm tree, sends updates */
8891 private final AlarmClient model ;
8992
93+ /** Latch for initially pausing model listeners
94+ *
95+ * Imagine a large alarm tree that changes.
96+ * The alarm table can periodically display the current
97+ * alarms, it does not need to display every change right away.
98+ * The tree on the other hand must reflect every added or removed item,
99+ * because updates cannot be applied once the tree structure gets out of sync.
100+ * When the model is first started, there is a flurry of additions and removals,
101+ * which arrive in the order in which the tree was generated, not necessarily
102+ * in the order they're laid out in the hierarchy.
103+ * These can be slow to render, especially if displaying via a remote desktop (ssh-X).
104+ * The alarm tree view thus starts in stages:
105+ * 1) Wait for model to receive the bulk of initial additions and removals
106+ * 2) Add listeners to model changes, but block them via this latch
107+ * 3) Represent the initial model
108+ * 4) Release this latch to handle changes (blocked and those that arrive from now on)
109+ */
110+ private final CountDownLatch block_item_changes = new CountDownLatch (1 );
111+
90112 /** Map from alarm tree path to view's TreeItem */
91113 private final ConcurrentHashMap <String , TreeItem <AlarmTreeItem <?>>> path2view = new ConcurrentHashMap <>();
92114
@@ -146,13 +168,49 @@ public AlarmTreeView(final AlarmClient model)
146168 setTop (createToolbar ());
147169 setCenter (tree_view );
148170
149- tree_view .setRoot (createViewItem (model .getRoot ()));
150-
151- model .addListener (this );
152-
153171 createContextMenu ();
154172 addClickSupport ();
155173 addDragSupport ();
174+
175+ if (AlarmSystem .alarm_tree_startup_ms <= 0 )
176+ {
177+ // Original implementation:
178+ // Create initial (empty) representation,
179+ // register listener, then model gets started
180+ block_item_changes .countDown ();
181+ tree_view .setRoot (createViewItem (model .getRoot ()));
182+ model .addListener (AlarmTreeView .this );
183+ }
184+ else
185+ ResettableTimeout .timer .schedule (this ::startup , AlarmSystem .alarm_tree_startup_ms , TimeUnit .MILLISECONDS );
186+
187+ // Caller will start the model once we return from constructor
188+ }
189+
190+ private void startup ()
191+ {
192+ // Waited for model to receive the bulk of initial additions and removals...
193+ Platform .runLater (() ->
194+ {
195+ if (! model .isRunning ())
196+ {
197+ logger .log (Level .WARNING , model .getRoot ().getName () + " was disposed while waiting for alarm tree startup" );
198+ return ;
199+ }
200+ // Listen to model changes, but they're blocked,
201+ // so this blocks model changes from now on
202+ model .addListener (AlarmTreeView .this );
203+
204+ // Represent model that should by now be fairly complete
205+ tree_view .setRoot (createViewItem (model .getRoot ()));
206+
207+ // Set change indicator so that it clears when there are no more changes
208+ indicateChange ();
209+ showServerState (model .isServerAlive ());
210+
211+ // Un-block to handle changes from now on
212+ block_item_changes .countDown ();
213+ });
156214 }
157215
158216 private ToolBar createToolbar ()
@@ -170,7 +228,8 @@ private ToolBar createToolbar()
170228 ImageCache .getImageView (AlarmUI .class , "/icons/expand_alarms.png" ));
171229 show_alarms .setTooltip (new Tooltip ("Expand alarm tree to show active alarms" ));
172230 show_alarms .setOnAction (event -> expandAlarms (tree_view .getRoot ()));
173- return new ToolBar (no_server , ToolbarHelper .createSpring (), collapse , show_alarms );
231+
232+ return new ToolBar (no_server , changing , ToolbarHelper .createSpring (), collapse , show_alarms );
174233 }
175234
176235 ToolBar getToolbar ()
@@ -222,25 +281,29 @@ private void indicateChange()
222281 logger .log (Level .INFO , "Alarm tree changes start" );
223282 setCursor (Cursor .WAIT );
224283 final ObservableList <Node > items = getToolbar ().getItems ();
225- items .add (1 , changing );
284+ if (! items .contains (changing ))
285+ items .add (1 , changing );
226286 }
227287 else
228288 previous .cancel (false );
229289 }
230290
291+ /** @param alive Have we seen server messages? */
292+ private void showServerState (final boolean alive )
293+ {
294+ final ObservableList <Node > items = getToolbar ().getItems ();
295+ items .remove (no_server );
296+ if (! alive )
297+ // Place left of spring, collapse, expand_alarms,
298+ // i.e. right of potential AlarmConfigSelector
299+ items .add (items .size ()-3 , no_server );
300+ }
301+
231302 // AlarmClientModelListener
232303 @ Override
233304 public void serverStateChanged (final boolean alive )
234305 {
235- Platform .runLater (() ->
236- {
237- final ObservableList <Node > items = getToolbar ().getItems ();
238- items .remove (no_server );
239- if (! alive )
240- // Place left of spring, collapse, expand_alarms,
241- // i.e. right of potential AlarmConfigSelector
242- items .add (items .size ()-3 , no_server );
243- });
306+ Platform .runLater (() -> showServerState (alive ));
244307 }
245308
246309 // AlarmClientModelListener
@@ -257,11 +320,25 @@ public void serverDisableNotifyChanged(final boolean disable_notify)
257320 // NOP
258321 }
259322
323+ /** Block until changes to items should be shown */
324+ private void blockItemChanges ()
325+ {
326+ try
327+ {
328+ block_item_changes .await ();
329+ }
330+ catch (InterruptedException ex )
331+ {
332+ logger .log (Level .WARNING , "Blocker for item changes got interrupted" , ex );
333+ }
334+ }
335+
260336 // AlarmClientModelListener
261337 @ Override
262338 public void itemAdded (final AlarmTreeItem <?> item )
263339 {
264- // System.out.println("Add " + item.getPathName());
340+ blockItemChanges ();
341+ // System.out.println(Thread.currentThread() + " Add " + item.getPathName());
265342
266343 // Parent must already exist
267344 final AlarmTreeItem <BasicState > model_parent = item .getParent ();
@@ -311,7 +388,8 @@ public void itemAdded(final AlarmTreeItem<?> item)
311388 @ Override
312389 public void itemRemoved (final AlarmTreeItem <?> item )
313390 {
314- // System.out.println("Removed " + item.getPathName());
391+ blockItemChanges ();
392+ // System.out.println(Thread.currentThread() + " Removed " + item.getPathName());
315393
316394 // Remove item and all sub-items from model2ui
317395 final TreeItem <AlarmTreeItem <?>> view_item = removeViewItems (item );
@@ -365,7 +443,8 @@ private TreeItem<AlarmTreeItem<?>> removeViewItems(final AlarmTreeItem<?> item)
365443 @ Override
366444 public void itemUpdated (final AlarmTreeItem <?> item )
367445 {
368- // System.out.println("Updated " + item.getPathName());
446+ blockItemChanges ();
447+ // System.out.println(Thread.currentThread() + " Updated " + item.getPathName());
369448 final TreeItem <AlarmTreeItem <?>> view_item = path2view .get (item .getPathName ());
370449 if (view_item == null )
371450 {
0 commit comments