/******************************************************************************* * Copyright (c) 2004, 2015 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.jface.viewers.deferred; /** * Allows a table to be accessed from a background thread. Provides a table-like public * interface that can accessed from a background thread. As updates arrive from the * background thread, it batches and schedules updates to the real table in the UI thread. * This class can be used with any widget that can be wrapped in the * <code>AbstractVirtualTable</code> interface. * * @since 3.1 */ /* package */ final class ConcurrentTableUpdator { /** * Wrapper for the real table. May only be accessed in the UI thread. */ private AbstractVirtualTable table; /** * The array of objects that have been sent to the UI. Elements are null * if they either haven't been sent yet or have been scheduled for clear. * Maps indices onto elements. */ private Object[] sentObjects = new Object[0]; /** * Map of elements to object indices (inverse of the knownObjects array) */ private IntHashMap knownIndices = new IntHashMap(); /** * Contains all known objects that have been sent here from the background * thread. */ private Object[] knownObjects = new Object[0]; // Minimum length for the pendingFlushes stack private static final int MIN_FLUSHLENGTH = 64; /** * Array of element indices. Contains elements scheduled to be * cleared. Only the beginning of the array is used. The number * of used elements is stored in lastClear */ private int[] pendingClears = new int[MIN_FLUSHLENGTH]; /** * Number of pending clears in the pendingClears array (this is normally * used instead of pendingClears.length since the * pendingClears array is usually larger than the actual number of pending * clears) */ private int lastClear = 0; /** * Last known visible range */ private volatile Range lastRange = new Range(0,0); /** * True iff a UI update has been scheduled */ private volatile boolean updateScheduled; /** * True iff this object has been disposed */ private volatile boolean disposed = false; /** * Object that holds a start index and length. Allows * the visible range to be returned as an atomic operation. */ public final static class Range { int start = 0; int length = 0; /** * @param s * @param l */ public Range(int s, int l) { start = s; length = l; } } /** * Runnable that can be posted with an asyncExec to schedule * an update to the real table. */ Runnable uiRunnable = () -> { updateScheduled = false; if(!table.getControl().isDisposed()) { updateTable(); } }; /** * Creates a new table updator * * @param table real table to update */ public ConcurrentTableUpdator(AbstractVirtualTable table) { this.table = table; } /** * Cleans up the updator object (but not the table itself). */ public void dispose() { disposed = true; } /** * True iff this object has been disposed. * * @return true iff dispose() has been called */ public boolean isDisposed() { return disposed; } /** * Returns the currently visible range * * @return the currently visible range */ public Range getVisibleRange() { return lastRange; } /** * Marks the given object as dirty. Will cause it to be cleared * in the table. * * @param toFlush */ public void clear(Object toFlush) { synchronized(this) { int currentIdx = knownIndices.get(toFlush, -1); // If we've never heard of this object, bail out. if (currentIdx == -1) { return; } pushClear(currentIdx); } } /** * Sets the size of the table. Called from a background thread. * * @param newTotal */ public void setTotalItems(int newTotal) { synchronized (this) { if (newTotal != knownObjects.length) { if (newTotal < knownObjects.length) { // Flush any objects that are being removed as a result of the resize for (int i = newTotal; i < knownObjects.length; i++) { Object toFlush = knownObjects[i]; if (toFlush != null) { knownIndices.remove(toFlush); } } } int minSize = Math.min(knownObjects.length, newTotal); Object[] newKnownObjects = new Object[newTotal]; System.arraycopy(knownObjects, 0, newKnownObjects, 0, minSize); knownObjects = newKnownObjects; scheduleUIUpdate(); } } } /** * Pushes an index onto the clear stack * * @param toClear row to clear */ private void pushClear(int toClear) { // If beyond the end of the table if (toClear >= sentObjects.length) { return; } // If already flushed or never sent if (sentObjects[toClear] == null) { return; } // Mark as flushed sentObjects[toClear] = null; if (lastClear >= pendingClears.length) { int newCapacity = Math.min(MIN_FLUSHLENGTH, lastClear * 2); int[] newPendingClears = new int[newCapacity]; System.arraycopy(pendingClears, 0, newPendingClears, 0, lastClear); pendingClears = newPendingClears; } pendingClears[lastClear++] = toClear; } /** * Sets the item on the given row to the given value. May be called from a background * thread. Schedules a UI update if necessary * * @param idx row to change * @param value new value for the given row */ public void replace(Object value, int idx) { // Keep the synchronized block as small as possible, since the UI may // be waiting on it. synchronized(this) { Object oldObject = knownObjects[idx]; if (oldObject != value) { if (oldObject != null) { knownIndices.remove(oldObject); } knownObjects[idx] = value; if (value != null) { int oldIndex = knownIndices.get(value, -1); if (oldIndex != -1) { knownObjects[oldIndex] = null; pushClear(oldIndex); } knownIndices.put(value, idx); } pushClear(idx); scheduleUIUpdate(); } } } /** * Schedules a UI update. Has no effect if an update has already been * scheduled. */ private void scheduleUIUpdate() { synchronized(this) { if (!updateScheduled) { updateScheduled = true; if(!table.getControl().isDisposed()) { table.getControl().getDisplay().asyncExec(uiRunnable); } } } } /** * Called in the UI thread by a SetData callback. Refreshes the * table if necessary. Returns true iff a refresh is needed. * @param includeIndex the index that should be included in the visible range. */ public void checkVisibleRange(int includeIndex) { int start = Math.min(table.getTopIndex() - 1, includeIndex); int length = Math.max(table.getVisibleItemCount(), includeIndex - start); Range r = lastRange; if (start != r.start || length != r.length) { updateTable(); } } /** * Updates the table. Sends any unsent items in the visible range to the table, * and clears any previously-visible items that have not yet been sent to the table. * Must be called from the UI thread. */ private void updateTable() { synchronized(this) { // Resize the table if necessary if (sentObjects.length != knownObjects.length) { Object[] newSentObjects = new Object[knownObjects.length]; System.arraycopy(newSentObjects, 0, sentObjects, 0, Math.min(newSentObjects.length, sentObjects.length)); sentObjects = newSentObjects; table.setItemCount(newSentObjects.length); } // Compute the currently visible range int start = Math.min(table.getTopIndex(), knownObjects.length); int length = Math.min(table.getVisibleItemCount(), knownObjects.length - start); int itemCount = table.getItemCount(); int oldStart = lastRange.start; int oldLen = lastRange.length; // Store the visible range. Do it BEFORE sending any table.clear calls, // since clearing a visible row will result in a SetData callback which // cause another table update if the visible range is different from // the stored values -- this could cause infinite recursion. lastRange = new Range(start, length); // Re-clear any items in the old range that were never filled in for(int idx = 0; idx < oldLen; idx++) { int row = idx + oldStart; // If this item is no longer visible if (row < itemCount && (row < start || row >= start + length)) { // Note: if we wanted to be really aggressive about clearing // items that are no longer visible, we could clear here unconditionally. // The current way of doing things won't clear a row if its contents are // up-to-date. if (sentObjects[row] == null) { table.clear(row); } } } // Process any pending clears if (lastClear > 0) { for (int i = 0; i < lastClear; i++) { int row = pendingClears[i]; if (row < sentObjects.length) { table.clear(row); } } if (pendingClears.length > MIN_FLUSHLENGTH) { pendingClears = new int[MIN_FLUSHLENGTH]; } lastClear = 0; } // Send any unsent items in the visible range for (int idx = 0; idx < length; idx++) { int row = idx + start; Object obj = knownObjects[row]; if (obj != null && obj != sentObjects[idx]) { table.replace(obj, row); sentObjects[idx] = obj; } } } } /** * Return the array of all known objects that have been sent here from the background * thread. * @return the array of all known objects */ public Object[] getKnownObjects() { return knownObjects; } }