Skip to content

Commit 74df9bd

Browse files
committed
Alarm tree: Accumulate initial model updates for faster UI startup
1 parent 8fd7bc7 commit 74df9bd

File tree

5 files changed

+124
-22
lines changed

5 files changed

+124
-22
lines changed

app/alarm/model/src/main/java/org/phoebus/applications/alarm/AlarmSystem.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ public class AlarmSystem
9595
/** Limit for the number of context menu items */
9696
@Preference public static int alarm_menu_max_items;
9797

98+
/** Initial Alarm Tree UI update delay [ms] */
99+
@Preference public static int alarm_tree_startup_ms;
100+
98101
/** Alarm table columns */
99102
@Preference public static String[] alarm_table_columns;
100103

app/alarm/model/src/main/java/org/phoebus/applications/alarm/ResettableTimeout.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2018 Oak Ridge National Laboratory.
2+
* Copyright (c) 2018-2022 Oak Ridge National Laboratory.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -31,7 +31,7 @@ public class ResettableTimeout
3131
{
3232
private final long timeout_secs;
3333

34-
private final ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("ResettableTimeout"));
34+
public static final ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("ResettableTimeout"));
3535
private final CountDownLatch no_more_messages = new CountDownLatch(1);
3636
private final Runnable signal_no_more_messages = () -> no_more_messages.countDown();
3737
private final AtomicReference<ScheduledFuture<?>> timeout = new AtomicReference<>();

app/alarm/model/src/main/java/org/phoebus/applications/alarm/client/AlarmClient.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2018-2021 Oak Ridge National Laboratory.
2+
* Copyright (c) 2018-2022 Oak Ridge National Laboratory.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -90,7 +90,7 @@ public class AlarmClient
9090
private long last_state_update = 0;
9191

9292
/** Timeout, not seen any messages from server? */
93-
private boolean has_timed_out = false;
93+
private volatile boolean has_timed_out = false;
9494

9595
/** @param server Kafka Server host:port
9696
* @param config_name Name of alarm tree root
@@ -571,6 +571,12 @@ public void acknowledge(final AlarmTreeItem<?> item, final boolean acknowledge)
571571
}
572572
}
573573

574+
/** @return <code>true</code> if connected to server, else updates have timed out */
575+
public boolean isServerAlive()
576+
{
577+
return !has_timed_out;
578+
}
579+
574580
/** Check if there have been any messages from server */
575581
private void checkServerState()
576582
{

app/alarm/model/src/main/resources/alarm_preferences.properties

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ alarm_area_font_size=15
4545
# 'display' and 'command' menu entries.
4646
alarm_menu_max_items=10
4747

48+
# Initial Alarm Tree UI update delay [ms]
49+
#
50+
# The initial flurry of alarm tree updates can be slow
51+
# to render. By allowing the alarm client to accumulate
52+
# alarm tree information for a little time and then
53+
# performing an initial bulk representation, the overall
54+
# alarm tree startup can be faster, especially when
55+
# the UI is viewed via a remote desktop
56+
#
57+
# Set to 0 for original implementation where
58+
# all alarm tree items are added to the model
59+
# as they are received in initial flurry of updates.
60+
alarm_tree_startup_ms=2000
61+
4862
# Order of columns in alarm table
4963
# Allows re-ordering as well as omitting columns
5064
alarm_table_columns=Icon, PV, Description, Alarm Severity, Alarm Status, Alarm Time, Alarm Value, PV Severity, PV Status

app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/AlarmTreeView.java

Lines changed: 97 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.util.logging.Level;
2323
import java.util.stream.Collectors;
2424

25+
import org.phoebus.applications.alarm.AlarmSystem;
26+
import org.phoebus.applications.alarm.ResettableTimeout;
2527
import org.phoebus.applications.alarm.client.AlarmClient;
2628
import org.phoebus.applications.alarm.client.AlarmClientLeaf;
2729
import 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

Comments
 (0)