2222import java .util .logging .Level ;
2323import java .util .stream .Collectors ;
2424
25+ import org .phoebus .applications .alarm .AlarmSystem ;
2526import org .phoebus .applications .alarm .client .AlarmClient ;
2627import org .phoebus .applications .alarm .client .AlarmClientLeaf ;
2728import org .phoebus .applications .alarm .client .AlarmClientListener ;
@@ -85,8 +86,28 @@ public class AlarmTreeView extends BorderPane implements AlarmClientListener
8586 private final Label no_server = AlarmUI .createNoServerLabel ();
8687 private final TreeView <AlarmTreeItem <?>> tree_view = new TreeView <>();
8788
89+ /** Model with current alarm tree, sends updates */
8890 private final AlarmClient model ;
8991
92+ /** Latch for initially pausing model listeners
93+ *
94+ * Imagine a large alarm tree that changes.
95+ * The alarm table can periodically display the current
96+ * alarms, it does not need to display every change right away.
97+ * The tree on the other hand must reflect every added or removed item,
98+ * because updates cannot be applied once the tree structure gets out of sync.
99+ * When the model is first started, there is a flurry of additions and removals,
100+ * which arrive in the order in which the tree was generated, not necessarily
101+ * in the order they're laid out in the hierarchy.
102+ * These can be slow to render, especially if displaying via a remote desktop (ssh-X).
103+ * The alarm tree view thus starts in stages:
104+ * 1) Wait for model to receive the bulk of initial additions and removals
105+ * 2) Add listeners to model changes, but block them via this latch
106+ * 3) Represent the initial model
107+ * 4) Release this latch to handle changes (blocked and those that arrive from now on)
108+ */
109+ private final CountDownLatch block_item_changes = new CountDownLatch (1 );
110+
90111 /** Map from alarm tree path to view's TreeItem */
91112 private final ConcurrentHashMap <String , TreeItem <AlarmTreeItem <?>>> path2view = new ConcurrentHashMap <>();
92113
@@ -146,13 +167,49 @@ public AlarmTreeView(final AlarmClient model)
146167 setTop (createToolbar ());
147168 setCenter (tree_view );
148169
149- tree_view .setRoot (createViewItem (model .getRoot ()));
150-
151- model .addListener (this );
152-
153170 createContextMenu ();
154171 addClickSupport ();
155172 addDragSupport ();
173+
174+ if (AlarmSystem .alarm_tree_startup_ms <= 0 )
175+ {
176+ // Original implementation:
177+ // Create initial (empty) representation,
178+ // register listener, then model gets started
179+ block_item_changes .countDown ();
180+ tree_view .setRoot (createViewItem (model .getRoot ()));
181+ model .addListener (AlarmTreeView .this );
182+ }
183+ else
184+ UpdateThrottle .TIMER .schedule (this ::startup , AlarmSystem .alarm_tree_startup_ms , TimeUnit .MILLISECONDS );
185+
186+ // Caller will start the model once we return from constructor
187+ }
188+
189+ private void startup ()
190+ {
191+ // Waited for model to receive the bulk of initial additions and removals...
192+ Platform .runLater (() ->
193+ {
194+ if (! model .isRunning ())
195+ {
196+ logger .log (Level .WARNING , model .getRoot ().getName () + " was disposed while waiting for alarm tree startup" );
197+ return ;
198+ }
199+ // Listen to model changes, but they're blocked,
200+ // so this blocks model changes from now on
201+ model .addListener (AlarmTreeView .this );
202+
203+ // Represent model that should by now be fairly complete
204+ tree_view .setRoot (createViewItem (model .getRoot ()));
205+
206+ // Set change indicator so that it clears when there are no more changes
207+ indicateChange ();
208+ showServerState (model .isServerAlive ());
209+
210+ // Un-block to handle changes from now on
211+ block_item_changes .countDown ();
212+ });
156213 }
157214
158215 private ToolBar createToolbar ()
@@ -170,7 +227,8 @@ private ToolBar createToolbar()
170227 ImageCache .getImageView (AlarmUI .class , "/icons/expand_alarms.png" ));
171228 show_alarms .setTooltip (new Tooltip ("Expand alarm tree to show active alarms" ));
172229 show_alarms .setOnAction (event -> expandAlarms (tree_view .getRoot ()));
173- return new ToolBar (no_server , ToolbarHelper .createSpring (), collapse , show_alarms );
230+
231+ return new ToolBar (no_server , changing , ToolbarHelper .createSpring (), collapse , show_alarms );
174232 }
175233
176234 ToolBar getToolbar ()
@@ -222,25 +280,29 @@ private void indicateChange()
222280 logger .log (Level .INFO , "Alarm tree changes start" );
223281 setCursor (Cursor .WAIT );
224282 final ObservableList <Node > items = getToolbar ().getItems ();
225- items .add (1 , changing );
283+ if (! items .contains (changing ))
284+ items .add (1 , changing );
226285 }
227286 else
228287 previous .cancel (false );
229288 }
230289
290+ /** @param alive Have we seen server messages? */
291+ private void showServerState (final boolean alive )
292+ {
293+ final ObservableList <Node > items = getToolbar ().getItems ();
294+ items .remove (no_server );
295+ if (! alive )
296+ // Place left of spring, collapse, expand_alarms,
297+ // i.e. right of potential AlarmConfigSelector
298+ items .add (items .size ()-3 , no_server );
299+ }
300+
231301 // AlarmClientModelListener
232302 @ Override
233303 public void serverStateChanged (final boolean alive )
234304 {
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- });
305+ Platform .runLater (() -> showServerState (alive ));
244306 }
245307
246308 // AlarmClientModelListener
@@ -257,11 +319,25 @@ public void serverDisableNotifyChanged(final boolean disable_notify)
257319 // NOP
258320 }
259321
322+ /** Block until changes to items should be shown */
323+ private void blockItemChanges ()
324+ {
325+ try
326+ {
327+ block_item_changes .await ();
328+ }
329+ catch (InterruptedException ex )
330+ {
331+ logger .log (Level .WARNING , "Blocker for item changes got interrupted" , ex );
332+ }
333+ }
334+
260335 // AlarmClientModelListener
261336 @ Override
262337 public void itemAdded (final AlarmTreeItem <?> item )
263338 {
264- // System.out.println("Add " + item.getPathName());
339+ blockItemChanges ();
340+ // System.out.println(Thread.currentThread() + " Add " + item.getPathName());
265341
266342 // Parent must already exist
267343 final AlarmTreeItem <BasicState > model_parent = item .getParent ();
@@ -311,7 +387,8 @@ public void itemAdded(final AlarmTreeItem<?> item)
311387 @ Override
312388 public void itemRemoved (final AlarmTreeItem <?> item )
313389 {
314- // System.out.println("Removed " + item.getPathName());
390+ blockItemChanges ();
391+ // System.out.println(Thread.currentThread() + " Removed " + item.getPathName());
315392
316393 // Remove item and all sub-items from model2ui
317394 final TreeItem <AlarmTreeItem <?>> view_item = removeViewItems (item );
@@ -365,7 +442,8 @@ private TreeItem<AlarmTreeItem<?>> removeViewItems(final AlarmTreeItem<?> item)
365442 @ Override
366443 public void itemUpdated (final AlarmTreeItem <?> item )
367444 {
368- // System.out.println("Updated " + item.getPathName());
445+ blockItemChanges ();
446+ // System.out.println(Thread.currentThread() + " Updated " + item.getPathName());
369447 final TreeItem <AlarmTreeItem <?>> view_item = path2view .get (item .getPathName ());
370448 if (view_item == null )
371449 {
0 commit comments