tmf: Fix concurrent data access violations
[deliverable/tracecompass.git] / tmf / org.eclipse.tracecompass.tmf.ui / src / org / eclipse / tracecompass / internal / tmf / ui / views / TmfAlignmentSynchronizer.java
1 /*******************************************************************************
2 * Copyright (c) 2015, 2016 Ericsson
3 *
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *
9 * Contributors:
10 * Marc-Andre Laperle - Initial API and implementation
11 *******************************************************************************/
12
13 package org.eclipse.tracecompass.internal.tmf.ui.views;
14
15 import java.util.ArrayList;
16 import java.util.Collections;
17 import java.util.List;
18 import java.util.Timer;
19 import java.util.TimerTask;
20
21 import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
22 import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
23 import org.eclipse.core.runtime.preferences.InstanceScope;
24 import org.eclipse.swt.graphics.Point;
25 import org.eclipse.swt.widgets.Composite;
26 import org.eclipse.swt.widgets.Display;
27 import org.eclipse.swt.widgets.Shell;
28 import org.eclipse.tracecompass.internal.tmf.ui.Activator;
29 import org.eclipse.tracecompass.internal.tmf.ui.ITmfUIPreferences;
30 import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
31 import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
32 import org.eclipse.tracecompass.tmf.ui.signal.TmfTimeViewAlignmentInfo;
33 import org.eclipse.tracecompass.tmf.ui.signal.TmfTimeViewAlignmentSignal;
34 import org.eclipse.tracecompass.tmf.ui.views.ITmfTimeAligned;
35 import org.eclipse.tracecompass.tmf.ui.views.TmfView;
36 import org.eclipse.ui.IViewPart;
37 import org.eclipse.ui.IViewReference;
38 import org.eclipse.ui.IWorkbenchPage;
39 import org.eclipse.ui.IWorkbenchWindow;
40 import org.eclipse.ui.PlatformUI;
41
42 /**
43 * Receives various notifications for realignment and
44 * performs the alignment on the appropriate views.
45 *
46 * @since 1.0
47 */
48 public class TmfAlignmentSynchronizer {
49
50 private static final long THROTTLE_DELAY = 500;
51 private static final int NEAR_THRESHOLD = 10;
52
53 /** Singleton instance */
54 private static TmfAlignmentSynchronizer fInstance = null;
55
56 private final Timer fTimer;
57 private final List<AlignmentOperation> fPendingOperations = Collections.synchronizedList(new ArrayList<AlignmentOperation>());
58
59 private TimerTask fCurrentTask;
60
61 /**
62 * Constructor
63 */
64 private TmfAlignmentSynchronizer() {
65 TmfSignalManager.register(this);
66 fTimer = new Timer();
67 createPreferenceListener();
68 fCurrentTask = new TimerTask() {
69 @Override
70 public void run() {
71 /* Do nothing */
72 }
73 };
74 }
75
76 /**
77 * Get the alignment synchronizer's instance
78 *
79 * @return The singleton instance
80 */
81 public static synchronized TmfAlignmentSynchronizer getInstance() {
82 if (fInstance == null) {
83 fInstance = new TmfAlignmentSynchronizer();
84 }
85 return fInstance;
86 }
87
88 /**
89 * Disposes the alignment synchronizer
90 */
91 public void dispose() {
92 TmfSignalManager.deregister(this);
93 synchronized (fPendingOperations) {
94 fTimer.cancel();
95 fCurrentTask.cancel();
96 }
97 }
98
99 private IPreferenceChangeListener createPreferenceListener() {
100 IPreferenceChangeListener listener = new IPreferenceChangeListener() {
101
102 @Override
103 public void preferenceChange(PreferenceChangeEvent event) {
104 if (event.getKey().equals(ITmfUIPreferences.PREF_ALIGN_VIEWS)) {
105 Object oldValue = event.getOldValue();
106 Object newValue = event.getNewValue();
107 if (Boolean.toString(false).equals(oldValue) && Boolean.toString(true).equals(newValue)) {
108 realignViews();
109 } else if (Boolean.toString(true).equals(oldValue) && Boolean.toString(false).equals(newValue)) {
110 restoreViews();
111 }
112 }
113 }
114 };
115 InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID).addPreferenceChangeListener(listener);
116 return listener;
117 }
118
119 private class AlignmentOperation {
120 final TmfView fView;
121 final TmfTimeViewAlignmentInfo fAlignmentInfo;
122
123 public AlignmentOperation(TmfView view, TmfTimeViewAlignmentInfo timeViewAlignmentInfo) {
124 fView = view;
125 fAlignmentInfo = timeViewAlignmentInfo;
126 }
127 }
128
129 private class AlignTask extends TimerTask {
130
131 @Override
132 public void run() {
133 final List<AlignmentOperation> fCopy;
134 synchronized (fPendingOperations) {
135 fCopy = new ArrayList<>(fPendingOperations);
136 fPendingOperations.clear();
137 }
138 Display.getDefault().syncExec(new Runnable() {
139 @Override
140 public void run() {
141 performAllAlignments(fCopy);
142 }
143 });
144 }
145 }
146
147 /**
148 * Handle a view that was just resized.
149 *
150 * @param view
151 * the view that was resized
152 */
153 public void handleViewResized(TmfView view) {
154 TmfTimeViewAlignmentInfo alignmentInfo = new TmfTimeViewAlignmentInfo(view.getParentComposite().getShell(), getViewLocation(view), 0);
155
156 // Don't use a view that was just resized as a reference view.
157 // Otherwise, a view that was just
158 // created might use itself as a reference but we want to
159 // keep the existing alignment from the other views.
160 ITmfTimeAligned referenceView = getReferenceView(alignmentInfo, view);
161 if (referenceView != null) {
162 queueAlignment(referenceView.getTimeViewAlignmentInfo(), false);
163 }
164 }
165
166 /**
167 * Handle a view that was just closed.
168 *
169 * @param view
170 * the view that was closed
171 */
172 public void handleViewClosed(TmfView view) {
173 // Realign views so that they can use the maximum available width in the
174 // event that a narrow view was just closed
175 realignViews(view.getSite().getPage());
176 }
177
178 /**
179 * Process signal for alignment.
180 *
181 * @param signal the alignment signal
182 */
183 @TmfSignalHandler
184 public void timeViewAlignmentUpdated(TmfTimeViewAlignmentSignal signal) {
185 queueAlignment(signal.getTimeViewAlignmentInfo(), signal.IsSynchronous());
186 }
187
188 /**
189 * Perform all alignment operations for the specified alignment
190 * informations.
191 *
192 * <pre>
193 * - The alignment algorithm chooses the narrowest width to accommodate all views.
194 * - View positions are recomputed for extra accuracy since the views could have been moved or resized.
195 * - Based on the up-to-date view positions, only views that are near and aligned with each other
196 * </pre>
197 */
198 private static void performAllAlignments(final List<AlignmentOperation> alignments) {
199 for (final AlignmentOperation info : alignments) {
200 performAlignment(info);
201 }
202 }
203
204 private static void performAlignment(AlignmentOperation info) {
205
206 TmfView referenceView = info.fView;
207 if (isDisposedView(referenceView)) {
208 return;
209 }
210
211 TmfTimeViewAlignmentInfo alignmentInfo = info.fAlignmentInfo;
212 // The location of the view might have changed (resize, etc). Update the alignment info.
213 alignmentInfo = new TmfTimeViewAlignmentInfo(alignmentInfo.getShell(), getViewLocation(referenceView), getClampedTimeAxisOffset(alignmentInfo));
214
215 TmfView narrowestView = getNarrowestView(alignmentInfo);
216 if (narrowestView == null) {
217 // No valid view found for this alignment. This could mean that the views for this alignment are now too narrow (width == 0) or that shell is not a workbench window.
218 return;
219 }
220
221 int narrowestWidth = ((ITmfTimeAligned) narrowestView).getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo));
222 narrowestWidth = getClampedTimeAxisWidth(alignmentInfo, narrowestWidth);
223 IViewReference[] viewReferences = referenceView.getSite().getPage().getViewReferences();
224 for (IViewReference ref : viewReferences) {
225 IViewPart view = ref.getView(false);
226 if (isTimeAlignedView(view)) {
227 TmfView tmfView = (TmfView) view;
228 ITmfTimeAligned alignedView = (ITmfTimeAligned) view;
229 if (!isDisposedView(tmfView) && isViewLocationNear(getViewLocation(tmfView), alignmentInfo.getViewLocation())) {
230 alignedView.performAlign(getClampedTimeAxisOffset(alignmentInfo), narrowestWidth);
231 }
232 }
233 }
234 }
235
236 /**
237 * Realign all views
238 */
239 private void realignViews() {
240 for (IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) {
241 for (IWorkbenchPage page : window.getPages()) {
242 realignViews(page);
243 }
244 }
245 }
246
247 /**
248 * Realign views inside a given page
249 *
250 * @param page
251 * the workbench page
252 */
253 private void realignViews(IWorkbenchPage page) {
254 IViewReference[] viewReferences = page.getViewReferences();
255 for (IViewReference ref : viewReferences) {
256 IViewPart view = ref.getView(false);
257 if (isTimeAlignedView(view)) {
258 queueAlignment(((ITmfTimeAligned) view).getTimeViewAlignmentInfo(), false);
259 }
260 }
261 }
262
263 /**
264 * Restore the views to their respective maximum widths
265 */
266 private static void restoreViews() {
267 // We set the width to Integer.MAX_VALUE so that the
268 // views remove any "filler" space they might have.
269 for (IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) {
270 for (IWorkbenchPage page : window.getPages()) {
271 for (IViewReference ref : page.getViewReferences()) {
272 restoreView(ref);
273 }
274 }
275 }
276 }
277
278 private static void restoreView(IViewReference ref) {
279 IViewPart view = ref.getView(false);
280 if (isTimeAlignedView(view)) {
281 ITmfTimeAligned alignedView = (ITmfTimeAligned) view;
282 alignedView.performAlign(getClampedTimeAxisOffset(alignedView.getTimeViewAlignmentInfo()), Integer.MAX_VALUE);
283 }
284 }
285
286 private static boolean isTimeAlignedView(IViewPart view) {
287 if (view instanceof TmfView && view instanceof ITmfTimeAligned) {
288 Composite parentComposite = ((TmfView) view).getParentComposite();
289 if (parentComposite != null && !parentComposite.isDisposed()) {
290 return true;
291 }
292 }
293 return view instanceof TmfView && view instanceof ITmfTimeAligned;
294 }
295
296 private static boolean isDisposedView(TmfView view) {
297 Composite parentComposite = (view).getParentComposite();
298 return parentComposite != null && parentComposite.isDisposed();
299 }
300
301 /**
302 * Queue the operation for processing. If an operation is considered the
303 * same alignment (shell, location) as a previously queued one, it will
304 * replace the old one. This way, only one up-to-date alignment operation is
305 * kept per set of time-axis aligned views. The processing of the operation
306 * is also throttled (TimerTask).
307 *
308 * @param operation
309 * the operation to queue
310 */
311 private void queue(AlignmentOperation operation) {
312 synchronized(fPendingOperations) {
313 fCurrentTask.cancel();
314 for (AlignmentOperation pendingOperation : fPendingOperations) {
315 if (isSameAlignment(operation, pendingOperation)) {
316 fPendingOperations.remove(pendingOperation);
317 break;
318 }
319 }
320 fPendingOperations.add(operation);
321 fCurrentTask = new AlignTask();
322 fTimer.schedule(fCurrentTask, THROTTLE_DELAY);
323 }
324 }
325
326 /**
327 * Two operations are considered to be for the same set of time-axis aligned
328 * views if they are on the same Shell and near the same location.
329 */
330 private static boolean isSameAlignment(AlignmentOperation operation1, AlignmentOperation operation2) {
331 if (operation1.fView == operation2.fView) {
332 return true;
333 }
334
335 if (operation1.fAlignmentInfo.getShell() != operation2.fAlignmentInfo.getShell()) {
336 return false;
337 }
338
339 if (isViewLocationNear(getViewLocation(operation1.fView), getViewLocation(operation2.fView))) {
340 return true;
341 }
342
343 return false;
344 }
345
346 private static boolean isViewLocationNear(Point location1, Point location2) {
347 return Math.abs(location1.x - location2.x) < NEAR_THRESHOLD;
348 }
349
350 private static Point getViewLocation(TmfView view) {
351 return view.getParentComposite().toDisplay(0, 0);
352 }
353
354 private void queueAlignment(TmfTimeViewAlignmentInfo timeViewAlignmentInfo, boolean synchronous) {
355 if (isAlignViewsPreferenceEnabled()) {
356 IWorkbenchWindow workbenchWindow = getWorkbenchWindow(timeViewAlignmentInfo.getShell());
357 if (workbenchWindow == null || workbenchWindow.getActivePage() == null) {
358 // Only time aligned views that are part of a workbench window are supported
359 return;
360 }
361
362 // We need a view so that we can compute position right as we are
363 // about to realign the views. The view could have been resized,
364 // moved, etc.
365 TmfView view = (TmfView) getReferenceView(timeViewAlignmentInfo, null);
366 if (view == null) {
367 // No valid view found for this alignment
368 return;
369 }
370
371 AlignmentOperation operation = new AlignmentOperation(view, timeViewAlignmentInfo);
372 if (synchronous) {
373 performAlignment(operation);
374 } else {
375 queue(operation);
376 }
377 }
378 }
379
380 private static boolean isAlignViewsPreferenceEnabled() {
381 return InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID).getBoolean(ITmfUIPreferences.PREF_ALIGN_VIEWS, true);
382 }
383
384 /**
385 * Get a view that corresponds to the alignment information. The view is
386 * meant to be used as a "reference" for other views to align on. Heuristics
387 * are applied to choose the best view. For example, the view has to be
388 * visible. It also will prioritize the view with lowest time axis offset
389 * because most of the interesting data should be in the time widget.
390 *
391 * @param alignmentInfo
392 * alignment information
393 * @param blackListedView
394 * an optional black listed view that will not be used as
395 * reference (useful for a view that just got created)
396 * @return the reference view
397 */
398 private static ITmfTimeAligned getReferenceView(TmfTimeViewAlignmentInfo alignmentInfo, TmfView blackListedView) {
399 IWorkbenchWindow workbenchWindow = getWorkbenchWindow(alignmentInfo.getShell());
400 if (workbenchWindow == null || workbenchWindow.getActivePage() == null) {
401 // Only time aligned views that are part of a workbench window are supported
402 return null;
403 }
404 IWorkbenchPage page = workbenchWindow.getActivePage();
405
406 int lowestTimeAxisOffset = Integer.MAX_VALUE;
407 ITmfTimeAligned referenceView = null;
408 for (IViewReference ref : page.getViewReferences()) {
409 IViewPart view = ref.getView(false);
410 if (view != blackListedView && isTimeAlignedView(view)) {
411 if (isCandidateForReferenceView((TmfView) view, alignmentInfo, lowestTimeAxisOffset)) {
412 referenceView = (ITmfTimeAligned) view;
413 lowestTimeAxisOffset = getClampedTimeAxisOffset(referenceView.getTimeViewAlignmentInfo());
414 }
415 }
416 }
417 return referenceView;
418 }
419
420 private static boolean isCandidateForReferenceView(TmfView tmfView, TmfTimeViewAlignmentInfo alignmentInfo, int lowestTimeAxisOffset) {
421 ITmfTimeAligned alignedView = (ITmfTimeAligned) tmfView;
422 TmfTimeViewAlignmentInfo timeViewAlignmentInfo = alignedView.getTimeViewAlignmentInfo();
423 if (timeViewAlignmentInfo == null) {
424 return false;
425 }
426
427 if (isDisposedView(tmfView)) {
428 return false;
429 }
430
431 Composite parentComposite = tmfView.getParentComposite();
432 boolean isVisible = parentComposite != null && parentComposite.isVisible();
433 if (isVisible) {
434 boolean isViewLocationNear = isViewLocationNear(alignmentInfo.getViewLocation(), getViewLocation(tmfView));
435 boolean isLowestTimeAxisOffset = getClampedTimeAxisOffset(timeViewAlignmentInfo) < lowestTimeAxisOffset;
436 if (isViewLocationNear && isLowestTimeAxisOffset) {
437 int availableWidth = alignedView.getAvailableWidth(getClampedTimeAxisOffset(timeViewAlignmentInfo));
438 availableWidth = getClampedTimeAxisWidth(timeViewAlignmentInfo, availableWidth);
439 if (availableWidth > 0) {
440 return true;
441 }
442 }
443 }
444
445 return false;
446 }
447
448 /**
449 * Get the narrowest view that corresponds to the given alignment information.
450 */
451 private static TmfView getNarrowestView(TmfTimeViewAlignmentInfo alignmentInfo) {
452 IWorkbenchWindow workbenchWindow = getWorkbenchWindow(alignmentInfo.getShell());
453 if (workbenchWindow == null || workbenchWindow.getActivePage() == null) {
454 // Only time aligned views that are part of a workbench window are supported
455 return null;
456 }
457 IWorkbenchPage page = workbenchWindow.getActivePage();
458
459 int narrowestWidth = Integer.MAX_VALUE;
460 TmfView narrowestView = null;
461 for (IViewReference ref : page.getViewReferences()) {
462 IViewPart view = ref.getView(false);
463 if (isTimeAlignedView(view)) {
464 TmfView tmfView = (TmfView) view;
465 if (isCandidateForNarrowestView(tmfView, alignmentInfo, narrowestWidth)) {
466 narrowestWidth = ((ITmfTimeAligned) tmfView).getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo));
467 narrowestWidth = getClampedTimeAxisWidth(alignmentInfo, narrowestWidth);
468 narrowestView = tmfView;
469 }
470 }
471 }
472
473 return narrowestView;
474 }
475
476 private static int getClampedTimeAxisWidth(TmfTimeViewAlignmentInfo alignmentInfo, int width) {
477 int max = getMaxInt(alignmentInfo.getShell());
478 if (validateInt(width, max)) {
479 Activator.getDefault().logError("Time-axis width out of range (" + width + ")", new Throwable()); //$NON-NLS-1$//$NON-NLS-2$
480 }
481 return Math.min(max, Math.max(0, width));
482 }
483
484 private static int getClampedTimeAxisOffset(TmfTimeViewAlignmentInfo alignmentInfo) {
485 int timeAxisOffset = alignmentInfo.getTimeAxisOffset();
486 int max = getMaxInt(alignmentInfo.getShell());
487 if (validateInt(timeAxisOffset, max)) {
488 Activator.getDefault().logError("Time-axis offset out of range (" + timeAxisOffset + ")", new Throwable()); //$NON-NLS-1$//$NON-NLS-2$
489 }
490 return Math.min(max, Math.max(0, timeAxisOffset));
491 }
492
493 private static boolean validateInt(int value, int max) {
494 return value < 0 || value > max;
495 }
496
497 private static int getMaxInt(Shell shell) {
498 // Consider an integer to be buggy if it's bigger than 10 times the
499 // width of *all* monitors combined.
500 final int DISPLAY_WIDTH_FACTOR = 10;
501 return shell.getDisplay().getBounds().width * DISPLAY_WIDTH_FACTOR;
502 }
503
504 private static boolean isCandidateForNarrowestView(TmfView tmfView, TmfTimeViewAlignmentInfo alignmentInfo, int narrowestWidth) {
505 ITmfTimeAligned alignedView = (ITmfTimeAligned) tmfView;
506 TmfTimeViewAlignmentInfo timeViewAlignmentInfo = alignedView.getTimeViewAlignmentInfo();
507 if (timeViewAlignmentInfo == null) {
508 return false;
509 }
510
511 if (isDisposedView(tmfView)) {
512 return false;
513 }
514
515 Composite parentComposite = tmfView.getParentComposite();
516 boolean isVisible = parentComposite != null && parentComposite.isVisible();
517 if (isVisible) {
518 if (isViewLocationNear(getViewLocation(tmfView), alignmentInfo.getViewLocation())) {
519 int availableWidth = alignedView.getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo));
520 availableWidth = getClampedTimeAxisWidth(alignmentInfo, availableWidth);
521 boolean isNarrower = availableWidth < narrowestWidth && availableWidth > 0;
522 return isNarrower;
523 }
524 }
525
526 return false;
527 }
528
529 private static IWorkbenchWindow getWorkbenchWindow(Shell shell) {
530 for (IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) {
531 if (window.getShell().equals(shell)) {
532 return window;
533 }
534 }
535
536 return null;
537 }
538 }
This page took 0.048427 seconds and 5 git commands to generate.