Copyright (c) 2000, 2015 IBM Corporation and others. This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which accompanies this distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ SPDX-License-Identifier: EPL-2.0 Contributors: IBM Corporation - initial API and implementation James Blackburn (Broadcom Corp.) - ongoing development
/******************************************************************************* * Copyright (c) 2000, 2015 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * Contributors: * IBM Corporation - initial API and implementation * James Blackburn (Broadcom Corp.) - ongoing development *******************************************************************************/
package org.eclipse.core.internal.events; import java.util.*; import org.eclipse.core.internal.events.ResourceChangeListenerList.ListenerEntry; import org.eclipse.core.internal.resources.*; import org.eclipse.core.internal.utils.Messages; import org.eclipse.core.internal.utils.Policy; import org.eclipse.core.internal.watson.ElementTree; import org.eclipse.core.resources.*; import org.eclipse.core.runtime.*; import org.eclipse.core.runtime.jobs.Job; public class NotificationManager implements IManager, ILifecycleListener { class NotifyJob extends Job { private final ICoreRunnable noop = monitor -> { // do nothing }; public NotifyJob() { super(Messages.resources_updating); setSystem(true); } @Override public IStatus run(IProgressMonitor monitor) { if (monitor.isCanceled()) return Status.CANCEL_STATUS; notificationRequested = true; try { workspace.run(noop, null, IResource.NONE, null); } catch (CoreException e) { return e.getStatus(); } return Status.OK_STATUS; } @Override public boolean belongsTo(Object family) { return NotificationManager.class == family; } } private static final long NOTIFICATION_DELAY = 1500;
The Threads that are currently avoiding notification.
/** * The Threads that are currently avoiding notification. */
private final Set<Thread> avoidNotify = Collections.synchronizedSet(new HashSet<>());
Indicates whether a notification is currently in progress. Used to avoid causing a notification to be requested as a result of another notification.
/** * Indicates whether a notification is currently in progress. Used to avoid * causing a notification to be requested as a result of another notification. */
protected boolean isNotifying; // if there are no changes between the current tree and the last delta state then we // can reuse the lastDelta (if any). If the lastMarkerChangeId is different then the current // one then we have to update that delta with new marker change info
last delta we broadcast
/** * last delta we broadcast */
private ResourceDelta lastDelta;
the marker change Id the last time we computed a delta
/** * the marker change Id the last time we computed a delta */
private long lastDeltaId;
tree the last time we computed a delta
/** * tree the last time we computed a delta */
private ElementTree lastDeltaState; protected long lastNotifyDuration = 0L;
the marker change id at the end of the last POST_AUTO_BUILD
/** * the marker change id at the end of the last POST_AUTO_BUILD */
private long lastPostBuildId = 0;
The state of the workspace at the end of the last POST_BUILD notification
/** * The state of the workspace at the end of the last POST_BUILD * notification */
private ElementTree lastPostBuildTree;
the marker change id at the end of the last POST_CHANGE
/** * the marker change id at the end of the last POST_CHANGE */
private long lastPostChangeId = 0;
The state of the workspace at the end of the last POST_CHANGE notification
/** * The state of the workspace at the end of the last POST_CHANGE * notification */
private ElementTree lastPostChangeTree; private ResourceChangeListenerList listeners; protected volatile boolean notificationRequested = false; private Job notifyJob; Workspace workspace; public NotificationManager(Workspace workspace) { this.workspace = workspace; listeners = new ResourceChangeListenerList(); notifyJob = new NotifyJob(); } public void addListener(IResourceChangeListener listener, int eventMask) { listeners.add(listener, eventMask); if (ResourceStats.TRACE_LISTENERS) ResourceStats.listenerAdded(listener); }
Indicates the beginning of a block where periodic notifications should be avoided. Returns true if notification avoidance really started, and false for nested operations.
/** * Indicates the beginning of a block where periodic notifications should be avoided. * Returns true if notification avoidance really started, and false for nested * operations. */
public boolean beginAvoidNotify() { return avoidNotify.add(Thread.currentThread()); }
Signals the beginning of the notification phase at the end of a top level operation.
/** * Signals the beginning of the notification phase at the end of a top level operation. */
public void beginNotify() { notifyJob.cancel(); notificationRequested = false; }
The main broadcast point for notification deltas
/** * The main broadcast point for notification deltas */
public void broadcastChanges(ElementTree lastState, ResourceChangeEvent event, boolean lockTree) { final int type = event.getType(); try { // Do the notification if there are listeners for events of the given type. if (!listeners.hasListenerFor(type)) return; isNotifying = true; ResourceDelta delta = getDelta(lastState, type); //don't broadcast POST_CHANGE or autobuild events if the delta is empty if (delta == null || delta.getKind() == 0) { int trigger = event.getBuildKind(); if (trigger == IncrementalProjectBuilder.AUTO_BUILD || trigger == 0) return; } event.setDelta(delta); long start = System.currentTimeMillis(); notify(getListeners(), event, lockTree); lastNotifyDuration = System.currentTimeMillis() - start; } finally { // Update the state regardless of whether people are listening. isNotifying = false; cleanUp(lastState, type); } }
Performs cleanup at the end of a resource change notification
/** * Performs cleanup at the end of a resource change notification */
private void cleanUp(ElementTree lastState, int type) { // Remember the current state as the last notified state if requested. // Be sure to clear out the old delta boolean postChange = type == IResourceChangeEvent.POST_CHANGE; if (postChange || type == IResourceChangeEvent.POST_BUILD) { long id = workspace.getMarkerManager().getChangeId(); lastState.immutable(); if (postChange) { lastPostChangeTree = lastState; lastPostChangeId = id; } else { lastPostBuildTree = lastState; lastPostBuildId = id; } workspace.getMarkerManager().resetMarkerDeltas(Math.min(lastPostBuildId, lastPostChangeId)); lastDelta = null; lastDeltaState = lastState; } }
Helper method for the save participant lifecycle computation.
/** * Helper method for the save participant lifecycle computation. */
public void broadcastChanges(IResourceChangeListener listener, int type, IResourceDelta delta) { ResourceChangeListenerList.ListenerEntry[] entries; entries = new ResourceChangeListenerList.ListenerEntry[] {new ResourceChangeListenerList.ListenerEntry(listener, type)}; notify(entries, new ResourceChangeEvent(workspace, type, 0, delta), false); }
Indicates the end of a block where periodic notifications should be avoided.
/** * Indicates the end of a block where periodic notifications should be avoided. */
public void endAvoidNotify() { avoidNotify.remove(Thread.currentThread()); }
Requests that a periodic notification be scheduled
/** * Requests that a periodic notification be scheduled */
public void requestNotify() { //don't do intermediate notifications if the current thread doesn't want them if (isNotifying || avoidNotify.contains(Thread.currentThread())) return; //notifications must never take more than one tenth of operation time long delay = Math.max(NOTIFICATION_DELAY, lastNotifyDuration * 10); if (notifyJob.getState() == Job.NONE) notifyJob.schedule(delay); }
Computes and returns the resource delta for the given event type and the given current tree state.
/** * Computes and returns the resource delta for the given event type and the * given current tree state. */
protected ResourceDelta getDelta(ElementTree tree, int type) { long id = workspace.getMarkerManager().getChangeId(); // If we have a delta from last time and no resources have changed // since then, we can reuse the delta structure. // However, be sure not to mix deltas from post_change with build events, because they use // a different reference point for delta computation. boolean postChange = type == IResourceChangeEvent.POST_CHANGE; if (!postChange && lastDelta != null && !ElementTree.hasChanges(tree, lastDeltaState, ResourceComparator.getNotificationComparator(), true)) { // Markers may have changed since the delta was generated. If so, get the new // marker state and insert it in to the delta which is being reused. if (id != lastDeltaId) { Map<IPath, MarkerSet> markerDeltas = workspace.getMarkerManager().getMarkerDeltas(lastPostBuildId); lastDelta.updateMarkers(markerDeltas); } } else { // We don't have a delta or something changed so recompute the whole deal. ElementTree oldTree = postChange ? lastPostChangeTree : lastPostBuildTree; long markerId = postChange ? lastPostChangeId : lastPostBuildId; lastDelta = ResourceDeltaFactory.computeDelta(workspace, oldTree, tree, Path.ROOT, markerId + 1); } // remember the state of the world when this delta was consistent lastDeltaState = tree; lastDeltaId = id; return lastDelta; } protected ResourceChangeListenerList.ListenerEntry[] getListeners() { return listeners.getListeners(); } @Override public void handleEvent(LifecycleEvent event) { switch (event.kind) { case LifecycleEvent.PRE_PROJECT_CLOSE : if (!listeners.hasListenerFor(IResourceChangeEvent.PRE_CLOSE)) return; IProject project = (IProject) event.resource; notify(getListeners(), new ResourceChangeEvent(workspace, IResourceChangeEvent.PRE_CLOSE, project), true); break; case LifecycleEvent.PRE_PROJECT_MOVE : //only notify deletion on move if old project handle is going // away if (event.resource.equals(event.newResource)) return; //fall through case LifecycleEvent.PRE_PROJECT_DELETE : if (!listeners.hasListenerFor(IResourceChangeEvent.PRE_DELETE)) return; project = (IProject) event.resource; notify(getListeners(), new ResourceChangeEvent(workspace, IResourceChangeEvent.PRE_DELETE, project), true); break; case LifecycleEvent.PRE_REFRESH : if (!listeners.hasListenerFor(IResourceChangeEvent.PRE_REFRESH)) return; if (event.resource.getType() == IResource.PROJECT) notify(getListeners(), new ResourceChangeEvent(event.resource, IResourceChangeEvent.PRE_REFRESH, event.resource), true); else if (event.resource.getType() == IResource.ROOT) notify(getListeners(), new ResourceChangeEvent(workspace, IResourceChangeEvent.PRE_REFRESH, null), true); break; } } private void notify(ResourceChangeListenerList.ListenerEntry[] resourceListeners, final ResourceChangeEvent event, final boolean lockTree) { int type = event.getType(); boolean oldLock = workspace.isTreeLocked(); if (lockTree) workspace.setTreeLocked(true); try { for (ListenerEntry resourceListener : resourceListeners) { if ((type & resourceListener.eventMask) != 0) { final IResourceChangeListener listener = resourceListener.listener; if (ResourceStats.TRACE_LISTENERS) ResourceStats.startNotify(listener); SafeRunner.run(new ISafeRunnable() { @Override public void handleException(Throwable e) { // exception logged in SafeRunner#run } @Override public void run() throws Exception { if (Policy.DEBUG_NOTIFICATIONS) Policy.debug("Notifying " + listener.getClass().getName() + " about resource change event" + event.toDebugString()); //$NON-NLS-1$ //$NON-NLS-2$ listener.resourceChanged(event); } }); if (ResourceStats.TRACE_LISTENERS) ResourceStats.endNotify(); } } } finally { if (lockTree) workspace.setTreeLocked(oldLock); } } public void removeListener(IResourceChangeListener listener) { listeners.remove(listener); if (ResourceStats.TRACE_LISTENERS) ResourceStats.listenerRemoved(listener); }
Returns true if a notification is needed. This happens if sufficient time has elapsed since the last notification
Returns:true if a notification is needed, and false otherwise
/** * Returns true if a notification is needed. This happens if * sufficient time has elapsed since the last notification * @return true if a notification is needed, and false otherwise */
public boolean shouldNotify() { return !isNotifying && notificationRequested; } @Override public void shutdown(IProgressMonitor monitor) { //wipe out any existing listeners listeners = new ResourceChangeListenerList(); } @Override public void startup(IProgressMonitor monitor) { // get the current state of the workspace as the starting point and // tell the workspace to track changes from there. This gives the // notification manager an initial basis for comparison. lastPostBuildTree = lastPostChangeTree = workspace.getElementTree(); workspace.addLifecycleListener(this); } }