org.eclipse.cdt.dsf.debug.ui.viewmodel.SteppingController.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.cdt.dsf.debug.ui.viewmodel.SteppingController.java

Source

/*******************************************************************************
 * Copyright (c) 2006, 2010 Wind River Systems 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:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.debug.ui.viewmodel;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ThreadSafe;
import org.eclipse.cdt.dsf.datamodel.AbstractDMEvent;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.debug.service.IRunControl;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExecutionDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IResumedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.ISuspendedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.StateChangeReason;
import org.eclipse.cdt.dsf.debug.service.IRunControl.StepType;
import org.eclipse.cdt.dsf.debug.ui.IDsfDebugUIConstants;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

/**
 * This class builds on top of standard run control service to provide
 * functionality for step queuing and delaying. Step queuing essentially allows
 * user to press and hold the step key and achieve maximum stepping speed. If
 * this class is used, other service implementations, such as stack and
 * expressions, can use it to avoid requesting data from debugger back end if
 * another step is about to be executed.
 * 
 * @since 1.1
 */
@ConfinedToDsfExecutor("#getExecutor()")
public final class SteppingController {
    /**
     * Default delay in milliseconds, that it takes the SteppingTimedOutEvent 
     * event to be issued after a step is started. 
     * @see SteppingTimedOutEvent
     * @see #setStepTimeout(int)
     * @see #getStepTimeout()
     */
    public final static int STEPPING_TIMEOUT = 500;

    /**
     * The depth of the step queue.  In other words, the maximum number of steps 
     * that are queued before the step queue manager is throwing them away. 
     */
    public final static int STEP_QUEUE_DEPTH = 2;

    /**
     * The maximum delay (in milliseconds) between steps when synchronized
     * stepping is enabled. This also serves as a safeguard in the case stepping
     * control participants fail to indicate completion of event processing.
     */
    public final static int MAX_STEP_DELAY = 5000;

    private final static boolean DEBUG = "true" //$NON-NLS-1$
            .equals(Platform.getDebugOption("org.eclipse.cdt.dsf.ui/debug/stepping")); //$NON-NLS-1$

    /**
     * Indicates that the given context has been stepping for some time, 
     * and the UI (views and actions) may need to be updated accordingly. 
     */
    public static final class SteppingTimedOutEvent extends AbstractDMEvent<IExecutionDMContext> {
        private SteppingTimedOutEvent(IExecutionDMContext execCtx) {
            super(execCtx);
        }
    }

    /**
     * Interface for clients interested in stepping control. When a stepping
     * control participant is registered with the stepping controller, it is
     * expected to call
     * {@link SteppingController#doneStepping(IExecutionDMContext, ISteppingControlParticipant)
     * doneStepping} as soon as a "step", i.e. a suspended event has been
     * processed. If synchronized stepping is enabled, further stepping is
     * blocked until all stepping control participants have indicated completion
     * of event processing or the maximum timeout
     * {@link SteppingController#MAX_STEP_DELAY} has been reached.
     * 
     * @see SteppingController#addSteppingControlParticipant(ISteppingControlParticipant)
     * @see SteppingController#removeSteppingControlParticipant(ISteppingControlParticipant)
     */
    public interface ISteppingControlParticipant {
    }

    private static class StepRequest {
        IExecutionDMContext fContext;
        StepType fStepType;
        boolean inProgress = false;

        StepRequest(IExecutionDMContext execCtx, StepType type) {
            fContext = execCtx;
            fStepType = type;
        }
    }

    private class TimeOutRunnable extends DsfRunnable {

        TimeOutRunnable(IExecutionDMContext dmc) {
            fDmc = dmc;
        }

        private final IExecutionDMContext fDmc;

        @Override
        public void run() {
            fTimedOutFutures.remove(fDmc);

            if (getSession().isActive()) {
                fTimedOutFlags.put(fDmc, Boolean.TRUE);
                enableStepping(fDmc);
                // Issue the stepping time-out event.
                getSession().dispatchEvent(new SteppingTimedOutEvent(fDmc), null);
            }
        }
    }

    private final DsfSession fSession;
    private final DsfServicesTracker fServicesTracker;

    private IRunControl fRunControl;
    private int fQueueDepth = STEP_QUEUE_DEPTH;

    private final Map<IExecutionDMContext, List<StepRequest>> fStepQueues = new HashMap<IExecutionDMContext, List<StepRequest>>();
    private final Map<IExecutionDMContext, Boolean> fTimedOutFlags = new HashMap<IExecutionDMContext, Boolean>();
    private final Map<IExecutionDMContext, ScheduledFuture<?>> fTimedOutFutures = new HashMap<IExecutionDMContext, ScheduledFuture<?>>();

    /**
     * Records the time of the last step for an execution context.
     */
    private final Map<IExecutionDMContext, Long> fLastStepTimes = new HashMap<IExecutionDMContext, Long>();

    /**
     * Minimum step interval in milliseconds.
     */
    private volatile int fMinStepInterval;

    /**
     * Step timeout in milliseconds.
     */
    private volatile int fStepTimeout = STEPPING_TIMEOUT;

    /**
     * Map of execution contexts for which a step is in progress.
     */
    private final Map<IExecutionDMContext, List<ISteppingControlParticipant>> fStepInProgress = new HashMap<IExecutionDMContext, List<ISteppingControlParticipant>>();

    /**
     * List of registered stepping control participants.
     */
    private final List<ISteppingControlParticipant> fParticipants = Collections
            .synchronizedList(new ArrayList<ISteppingControlParticipant>());

    /**
     * Property change listener.  It updates the stepping control settings.
     */
    private IPropertyChangeListener fPreferencesListener;

    public SteppingController(DsfSession session) {
        fSession = session;
        fServicesTracker = new DsfServicesTracker(DsfUIPlugin.getBundleContext(), session.getId());

        final IPreferenceStore store = DsfUIPlugin.getDefault().getPreferenceStore();

        fPreferencesListener = new IPropertyChangeListener() {
            @Override
            public void propertyChange(final PropertyChangeEvent event) {
                handlePropertyChanged(store, event);
            }
        };
        store.addPropertyChangeListener(fPreferencesListener);

        setMinimumStepInterval(store.getInt(IDsfDebugUIConstants.PREF_MIN_STEP_INTERVAL));
    }

    @ThreadSafe
    public void dispose() {
        try {
            fSession.getExecutor().execute(new DsfRunnable() {
                @Override
                public void run() {
                    if (fRunControl != null) {
                        getSession().removeServiceEventListener(SteppingController.this);
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            // Session already gone.
        }

        IPreferenceStore store = DsfUIPlugin.getDefault().getPreferenceStore();
        store.removePropertyChangeListener(fPreferencesListener);

        fServicesTracker.dispose();
    }

    /**
     * Configure the minimum time (in milliseconds) to wait between steps.
     * 
     * @param interval
     */
    @ThreadSafe
    public void setMinimumStepInterval(int interval) {
        fMinStepInterval = interval;
    }

    /**
     * Configure the step timeout value. This controls the delay how long it takes
     * to send out the {@link SteppingTimedOutEvent} after a step has been issued.
     * 
     * @param timeout  The timeout value in milliseconds (must be non-negative).
     * @since 2.2
     */
    @ThreadSafe
    public void setStepTimeout(int timeout) {
        assert timeout >= 0;
        fStepTimeout = timeout;
    }

    /**
     * @return the currently configured step timeout value
     * @since 2.2
     */
    @ThreadSafe
    public int getStepTimeout() {
        return fStepTimeout;
    }

    /**
     * Register given stepping control participant.
     * <p>
     * Participants are obliged to call
     * {@link #doneStepping(IExecutionDMContext, ISteppingControlParticipant)}
     * when they have received and completed processing an
     * {@link ISuspendedDMEvent}. If synchronized stepping is enabled, further
     * stepping is disabled until all participants have indicated completion of
     * processing the event.
     * </p>
     * 
     * @param participant
     */
    @ThreadSafe
    public void addSteppingControlParticipant(ISteppingControlParticipant participant) {
        fParticipants.add(participant);
    }

    /**
     * Unregister given stepping control participant.
     * 
     * @param participant
     */
    @ThreadSafe
    public void removeSteppingControlParticipant(final ISteppingControlParticipant participant) {
        fParticipants.remove(participant);
    }

    /**
     * Indicate that participant has completed processing of the last step.
     * 
     * @param execCtx
     */
    public void doneStepping(final IExecutionDMContext execCtx, final ISteppingControlParticipant participant) {
        if (DEBUG)
            System.out.println(
                    "[SteppingController] doneStepping participant=" + participant.getClass().getSimpleName()); //$NON-NLS-1$
        List<ISteppingControlParticipant> participants = fStepInProgress.get(execCtx);
        if (participants != null) {
            participants.remove(participant);
            if (participants.isEmpty()) {
                doneStepping(execCtx);
            }
        } else {
            for (IExecutionDMContext disabledCtx : fStepInProgress.keySet()) {
                if (DMContexts.isAncestorOf(disabledCtx, execCtx)) {
                    participants = fStepInProgress.get(disabledCtx);
                    if (participants != null) {
                        participants.remove(participant);
                        if (participants.isEmpty()) {
                            doneStepping(disabledCtx);
                        }
                    }
                }
            }
        }
    }

    @ThreadSafe
    public DsfSession getSession() {
        return fSession;
    }

    /**
     * All access to this class should happen through this executor.
     * @return the executor this class is confined to
     */
    @ThreadSafe
    public DsfExecutor getExecutor() {
        return getSession().getExecutor();
    }

    private DsfServicesTracker getServicesTracker() {
        return fServicesTracker;
    }

    private IRunControl getRunControl() {
        if (fRunControl == null) {
            fRunControl = getServicesTracker().getService(IRunControl.class);
            getSession().addServiceEventListener(this, null);
        }
        return fRunControl;
    }

    /**
      * Checks whether a step command can be queued up for given context.
      */
    public void canEnqueueStep(IExecutionDMContext execCtx, StepType stepType, DataRequestMonitor<Boolean> rm) {
        if (doCanEnqueueStep(execCtx, stepType)) {
            rm.setData(true);
            rm.done();
        } else {
            getRunControl().canStep(execCtx, stepType, rm);
        }
    }

    private boolean doCanEnqueueStep(IExecutionDMContext execCtx, StepType stepType) {
        return getRunControl().isStepping(execCtx) && !isSteppingTimedOut(execCtx);
    }

    /**
     * Check whether the next step on the given execution context should be delayed
     * based on the configured step delay.
     * 
    * @param execCtx
    * @return <code>true</code> if the step should be delayed
    */
    private boolean shouldDelayStep(IExecutionDMContext execCtx) {
        final int stepDelay = getStepDelay(execCtx);
        if (DEBUG)
            System.out.println("[SteppingController] shouldDelayStep delay=" + stepDelay); //$NON-NLS-1$
        return stepDelay > 0;
    }

    /**
     * Compute the delay in milliseconds before the next step for the given context may be executed.
     * 
     * @param execCtx
     * @return  the number of milliseconds before the next possible step
     */
    private int getStepDelay(IExecutionDMContext execCtx) {
        int minStepInterval = fMinStepInterval;
        if (minStepInterval > 0) {
            for (IExecutionDMContext lastStepCtx : fLastStepTimes.keySet()) {
                if (execCtx.equals(lastStepCtx) || DMContexts.isAncestorOf(execCtx, lastStepCtx)) {
                    long now = System.currentTimeMillis();
                    int delay = (int) (fLastStepTimes.get(lastStepCtx) + minStepInterval - now);
                    return Math.max(delay, 0);
                }
            }
        }
        return 0;
    }

    private void updateLastStepTime(IExecutionDMContext execCtx) {
        long now = System.currentTimeMillis();
        fLastStepTimes.put(execCtx, now);
        for (IExecutionDMContext lastStepCtx : fLastStepTimes.keySet()) {
            if (!execCtx.equals(lastStepCtx) && DMContexts.isAncestorOf(execCtx, lastStepCtx)) {
                fLastStepTimes.put(lastStepCtx, now);
            }
        }
    }

    private long getLastStepTime(IExecutionDMContext execCtx) {
        if (fLastStepTimes.containsKey(execCtx)) {
            return fLastStepTimes.get(execCtx);
        }
        for (IExecutionDMContext lastStepCtx : fLastStepTimes.keySet()) {
            if (DMContexts.isAncestorOf(execCtx, lastStepCtx)) {
                return fLastStepTimes.get(lastStepCtx);
            }
        }
        return 0;
    }

    /**
      * Returns the number of step commands that are queued for given execution
      * context.
      */
    public int getPendingStepCount(IExecutionDMContext execCtx) {
        List<StepRequest> stepQueue = getStepQueue(execCtx);
        if (stepQueue == null)
            return 0;
        return stepQueue.size();
    }

    /**
     * Adds a step command to the execution queue for given context.
     * @param execCtx Execution context that should perform the step. 
     * @param stepType Type of step to execute.
     */
    public void enqueueStep(final IExecutionDMContext execCtx, final StepType stepType) {
        if (DEBUG)
            System.out.println("[SteppingController] enqueueStep ctx=" + execCtx); //$NON-NLS-1$
        if (!shouldDelayStep(execCtx) || doCanEnqueueStep(execCtx, stepType)) {
            doEnqueueStep(execCtx, stepType);
            processStepQueue(execCtx);
        }
    }

    private void doStep(final IExecutionDMContext execCtx, final StepType stepType) {
        if (DEBUG)
            System.out.println("[SteppingController] doStep ctx=" + execCtx); //$NON-NLS-1$
        disableStepping(execCtx);
        updateLastStepTime(execCtx);

        getRunControl().step(execCtx, stepType, new RequestMonitor(getExecutor(), null) {
            @Override
            protected void handleSuccess() {
                fTimedOutFlags.remove(execCtx);
                ScheduledFuture<?> currentTimeOutFuture = fTimedOutFutures.get(execCtx);
                if (currentTimeOutFuture != null) {
                    currentTimeOutFuture.cancel(false);
                }
                fTimedOutFutures.put(execCtx,
                        getExecutor().schedule(new TimeOutRunnable(execCtx), fStepTimeout, TimeUnit.MILLISECONDS));
            }

            @Override
            protected void handleFailure() {
                // in case of a failed step - enable stepping again (bug 265267)
                enableStepping(execCtx);
                if (getStatus().getCode() == IDsfStatusConstants.INVALID_STATE) {
                    // Ignore errors.  During fast stepping there can be expected race
                    // conditions leading to stepping errors.
                    return;
                }
                super.handleFailure();
            }
        });
    }

    /**
     * Enqueue the given step for later execution.
     * 
     * @param execCtx
     * @param stepType
     */
    private void doEnqueueStep(final IExecutionDMContext execCtx, final StepType stepType) {
        List<StepRequest> stepQueue = fStepQueues.get(execCtx);
        if (stepQueue == null) {
            stepQueue = new LinkedList<StepRequest>();
            fStepQueues.put(execCtx, stepQueue);
        }
        if (stepQueue.size() < fQueueDepth) {
            stepQueue.add(new StepRequest(execCtx, stepType));
        }
    }

    /**
     * Returns whether the step instruction for the given context has timed out.
     */
    public boolean isSteppingTimedOut(IExecutionDMContext execCtx) {
        for (IExecutionDMContext timedOutCtx : fTimedOutFlags.keySet()) {
            if (execCtx.equals(timedOutCtx) || DMContexts.isAncestorOf(execCtx, timedOutCtx)) {
                return fTimedOutFlags.get(timedOutCtx);
            }
        }
        return false;
    }

    /**
     * Process next step on queue if any.
     * @param execCtx
     */
    private void processStepQueue(final IExecutionDMContext execCtx) {
        final List<StepRequest> queue = getStepQueue(execCtx);
        if (queue != null) {
            final int stepDelay = getStepDelay(execCtx);
            if (stepDelay > 0) {
                getExecutor().schedule(new DsfRunnable() {
                    @Override
                    public void run() {
                        processStepQueue(execCtx);
                    }
                }, stepDelay, TimeUnit.MILLISECONDS);
                return;
            }
            final StepRequest request = queue.get(0);
            if (DEBUG)
                System.out
                        .println("[SteppingController] processStepQueue request-in-progress=" + request.inProgress); //$NON-NLS-1$
            if (!request.inProgress) {
                if (isSteppingDisabled(request.fContext)) {
                    return;
                }
                request.inProgress = true;
                getRunControl().canStep(request.fContext, request.fStepType,
                        new DataRequestMonitor<Boolean>(getExecutor(), null) {
                            @Override
                            protected void handleCompleted() {
                                if (isSuccess() && getData()) {
                                    queue.remove(0);
                                    if (queue.isEmpty())
                                        fStepQueues.remove(request.fContext);
                                    doStep(request.fContext, request.fStepType);
                                } else {
                                    // For whatever reason we can't step anymore, so clear out
                                    // the step queue.
                                    fStepQueues.remove(request.fContext);
                                }
                            }
                        });
            }
        }
    }

    private List<StepRequest> getStepQueue(IExecutionDMContext execCtx) {
        List<StepRequest> queue = fStepQueues.get(execCtx);
        if (queue == null) {
            for (IExecutionDMContext stepCtx : fStepQueues.keySet()) {
                if (DMContexts.isAncestorOf(stepCtx, execCtx)) {
                    queue = fStepQueues.get(stepCtx);
                    break;
                }
            }
        }
        return queue;
    }

    /**
     * Disable stepping for the given execution context.
     * 
     * @param execCtx
     */
    private void disableStepping(IExecutionDMContext execCtx) {
        if (!fParticipants.isEmpty()) {
            fStepInProgress.put(execCtx, new ArrayList<ISteppingControlParticipant>(fParticipants));
        }
    }

    /**
     * Indicate that processing of the last step has completed and
     * the next step can be issued.
     * 
     * @param execCtx
     */
    private void doneStepping(final IExecutionDMContext execCtx) {
        if (DEBUG)
            System.out.println("[SteppingController] doneStepping ctx=" + execCtx); //$NON-NLS-1$
        enableStepping(execCtx);
        processStepQueue(execCtx);
    }

    /**
     * Enable stepping for the given execution context.
     * 
     * @param execCtx
     */
    public void enableStepping(final IExecutionDMContext execCtx) {
        fStepInProgress.remove(execCtx);
        for (IExecutionDMContext disabledCtx : fStepInProgress.keySet()) {
            if (DMContexts.isAncestorOf(disabledCtx, execCtx)) {
                fStepInProgress.remove(disabledCtx);
            }
        }
    }

    private boolean isSteppingDisabled(IExecutionDMContext execCtx) {
        boolean disabled = fStepInProgress.containsKey(execCtx);
        if (!disabled) {
            for (IExecutionDMContext disabledCtx : fStepInProgress.keySet()) {
                if (DMContexts.isAncestorOf(execCtx, disabledCtx)) {
                    disabled = true;
                    break;
                }
            }
        }
        if (disabled) {
            long now = System.currentTimeMillis();
            long lastStepTime = getLastStepTime(execCtx);
            if (now - lastStepTime > MAX_STEP_DELAY) {
                if (DEBUG)
                    System.out.println("[SteppingController] stepping control participant(s) timed out"); //$NON-NLS-1$
                enableStepping(execCtx);
                disabled = false;
            }
        }
        return disabled;
    }

    protected void handlePropertyChanged(final IPreferenceStore store, final PropertyChangeEvent event) {
        String property = event.getProperty();
        if (IDsfDebugUIConstants.PREF_MIN_STEP_INTERVAL.equals(property)) {
            setMinimumStepInterval(store.getInt(property));
        }
    }

    ///////////////////////////////////////////////////////////////////////////

    @DsfServiceEventHandler
    public void eventDispatched(final ISuspendedDMEvent e) {
        // Take care of the stepping time out
        boolean timedOut = false;
        IExecutionDMContext dmc = e.getDMContext();
        for (Iterator<Map.Entry<IExecutionDMContext, Boolean>> itr = fTimedOutFlags.entrySet().iterator(); itr
                .hasNext();) {
            Map.Entry<IExecutionDMContext, Boolean> entry = itr.next();
            IExecutionDMContext nextDmc = entry.getKey();
            if (nextDmc.equals(dmc) || DMContexts.isAncestorOf(nextDmc, dmc)) {
                if (entry.getValue()) {
                    // after step timeout do not process queued steps
                    fStepQueues.remove(dmc);
                    timedOut = true;
                }
                itr.remove();
            }
        }

        // Cancel all time-out futures related to the event context.  I.e.
        // - If event is on a container, all child threads are suspended and 
        // should not issue a stepping time-out event.
        // - If event is on a thread, and resumed event was on a container then 
        // stepping timeout for container should be canceled as it would affect
        // suspended thread.
        for (Iterator<Map.Entry<IExecutionDMContext, ScheduledFuture<?>>> itr = fTimedOutFutures.entrySet()
                .iterator(); itr.hasNext();) {
            Map.Entry<IExecutionDMContext, ScheduledFuture<?>> entry = itr.next();
            IExecutionDMContext nextDmc = entry.getKey();
            if (nextDmc.equals(dmc) || DMContexts.isAncestorOf(nextDmc, dmc)
                    || DMContexts.isAncestorOf(dmc, nextDmc)) {
                entry.getValue().cancel(false);
                itr.remove();
            }
        }

        if (e.getReason() != StateChangeReason.STEP) {
            // after any non-step suspend reason do not process queued steps for given context
            fStepQueues.remove(dmc);
        } else if (!timedOut) {
            // Check if there's a step pending, if so execute it
            processStepQueue(dmc);
        }
    }

    @DsfServiceEventHandler
    public void eventDispatched(final IResumedDMEvent e) {
        if (e.getReason().equals(StateChangeReason.STEP)) {
            final IExecutionDMContext dmc = e.getDMContext();
            fTimedOutFlags.remove(dmc);

            // Find any time-out futures for contexts that are children of the 
            // resumed context, and cancel them as they'll be replaced.
            if (!fTimedOutFutures.containsKey(dmc)) {
                for (Iterator<Map.Entry<IExecutionDMContext, ScheduledFuture<?>>> itr = fTimedOutFutures.entrySet()
                        .iterator(); itr.hasNext();) {
                    Map.Entry<IExecutionDMContext, ScheduledFuture<?>> entry = itr.next();
                    if (DMContexts.isAncestorOf(entry.getKey(), dmc)) {
                        entry.getValue().cancel(false);
                        itr.remove();
                    }
                }

                fTimedOutFutures.put(dmc,
                        getExecutor().schedule(new TimeOutRunnable(dmc), fStepTimeout, TimeUnit.MILLISECONDS));
            }
        }
    }
}