diff --git a/src/main/java/com/team766/framework/Context.java b/src/main/java/com/team766/framework/Context.java index 6221ccde1..3502acf5c 100644 --- a/src/main/java/com/team766/framework/Context.java +++ b/src/main/java/com/team766/framework/Context.java @@ -1,15 +1,5 @@ package com.team766.framework; -import com.team766.hal.Clock; -import com.team766.hal.RobotProvider; -import com.team766.logging.Category; -import com.team766.logging.Logger; -import com.team766.logging.LoggerExceptionUtils; -import com.team766.logging.Severity; -import java.lang.StackWalker.StackFrame; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; import java.util.function.BooleanSupplier; /** @@ -45,331 +35,8 @@ * model execution of the program as a sequence of imperative commands"), rather * than as state machines or in continuation-passing style, which can be much * more complicated to reason about, especially for new programmers. - * - * Currently, threads of execution are implemented using OS threads, but this - * should be considered an implementation detail and may change in the future. - * Even though the framework creates multiple OS threads, it uses Java's - * monitors to implement a "baton passing" pattern in order to ensure that only - * one of threads is actually running at once (the others will be sleeping, - * waiting for the baton to be passed to them). */ -public class Context implements Runnable, LaunchedContext { - /** - * Represents the baton-passing state (see class comments). Instead of - * passing a baton directly from one Context's thread to the next, each - * Context has its own baton that gets passed from the program's main thread - * to the Context's thread and back. While this is less efficient (double - * the number of OS context switches required), it makes the code simpler - * and more modular. - */ - private enum ControlOwner { - MAIN_THREAD, - SUBROUTINE, - } - - /** - * Indicates the lifetime state of this Context. - */ - private enum State { - /** - * The Context has been started (a Context is started immediately upon - * construction). - */ - RUNNING, - /** - * stop() has been called on this Context (but it has not been allowed - * to respond to the stop request yet). - */ - CANCELED, - /** - * The Context's execution has come to an end. - */ - DONE, - } - - // package visible for testing - /* package */ static class TimedPredicate implements BooleanSupplier { - private final Clock clock; - private final BooleanSupplier predicate; - private final double deadlineSeconds; - private boolean succeeded = false; - - // package visible for testing - /* package */ TimedPredicate( - Clock clock, BooleanSupplier predicate, double timeoutSeconds) { - this.clock = clock; - this.deadlineSeconds = clock.getTime() + timeoutSeconds; - this.predicate = predicate; - } - - public TimedPredicate(BooleanSupplier predicate, double timeoutSeconds) { - this(RobotProvider.instance.getClock(), predicate, timeoutSeconds); - } - - public boolean getAsBoolean() { - if (predicate.getAsBoolean()) { - succeeded = true; - return true; - } - if (clock.getTime() >= deadlineSeconds) { - succeeded = false; - return true; - } else { - return false; - } - } - - public boolean succeeded() { - return succeeded; - } - } - - private static Context c_currentContext = null; - - /** - * Returns the currently-executing Context. - * - * This is maintained for things like checking Mechanism ownership, but - * intentionally only has package-private visibility - code outside of the - * framework should ideally pass around references to the current context - * object rather than cheating with this static accessor. - */ - static Context currentContext() { - return c_currentContext; - } - - /** - * The top-level procedure being run by this Context. - */ - private final RunnableWithContext m_func; - - /** - * If this Context was created by another context using - * {@link #startAsync}, this will contain a reference to that originating - * Context. - */ - private final Context m_parentContext; - - /** - * The OS thread that this Context is executing on. - */ - private final Thread m_thread; - - /** - * Used to synchronize access to this Context's state variable. - */ - private final Object m_threadSync; - - /** - * This Context's lifetime state. - */ - private State m_state; - - /** - * If one of the wait* methods has been called on this Context, this - * contains the predicate which should be checked to determine whether - * the Context's execution should be resumed. This makes it more efficient - * to poll completion criteria without needing to context-switch between - * threads. - */ - private BooleanSupplier m_blockingPredicate; - - /** - * Set to SUBROUTINE when this Context is executing and MAIN_THREAD - * otherwise. - */ - private ControlOwner m_controlOwner; - - /** - * Contains the method name and line number at which this Context most - * recently yielded. - */ - private String m_previousWaitPoint; - - /** - * The mechanisms that have been claimed by this Context using - * takeOwnership. These will be automatically released when the Context - * finishes executing. - */ - private Set m_ownedMechanisms = new HashSet(); - - /* - * Constructors are intentionally private or package-private. New contexts - * should be created with {@link Context#startAsync} or - * {@link Scheduler#startAsync}. - */ - - Context(final RunnableWithContext func, final Context parentContext) { - m_func = func; - m_parentContext = parentContext; - Logger.get(Category.FRAMEWORK) - .logRaw( - Severity.DEBUG, - "Starting context " + getContextName() + " for " + func.toString()); - m_threadSync = new Object(); - m_previousWaitPoint = null; - m_controlOwner = ControlOwner.MAIN_THREAD; - m_state = State.RUNNING; - m_thread = new Thread(this::threadFunction, getContextName()); - m_thread.start(); - Scheduler.getInstance().add(this); - } - - Context(final RunnableWithContext func) { - this(func, null); - } - - Context(final Runnable func, final Context parentContext) { - this((context) -> func.run()); - } - - Context(final Runnable func) { - this(func, null); - } - - /** - * Returns a string meant to uniquely identify this Context (e.g. for use in logging). - */ - public String getContextName() { - return "Context/" + Integer.toHexString(hashCode()) + "/" + m_func.toString(); - } - - @Override - public String toString() { - String repr = getContextName(); - if (currentContext() == this) { - repr += " running"; - } - repr += "\n"; - repr += StackTraceUtils.getStackTrace(m_thread); - return repr; - } - - /** - * Walks up the call stack until it reaches a frame that isn't from the Context class, then - * returns a string representation of that frame. This is used to generate a concise string - * representation of from where the user called into framework code. - */ - private String getExecutionPoint() { - StackWalker walker = StackWalker.getInstance(); - return walker.walk( - s -> - s.dropWhile(f -> f.getClassName() != Context.this.getClass().getName()) - .filter(f -> f.getClassName() != Context.this.getClass().getName()) - .findFirst() - .map(StackFrame::toString) - .orElse(null)); - } - - /** - * Wait until the baton (see the class comments) has been passed to this thread. - * - * @param thisOwner the thread from which this function is being called (and thus the - * baton-passing state that should be waited for) - * @throws ContextStoppedException if stop() is called on this Context while waiting. - */ - private void waitForControl(final ControlOwner thisOwner) { - // If this is being called from the worker thread, log from where in the - // user's code that the context is waiting. This is provided as a - // convenience so the user can track the progress of execution through - // their procedures. - if (thisOwner == ControlOwner.SUBROUTINE) { - String waitPointTrace = getExecutionPoint(); - if (waitPointTrace != null && !waitPointTrace.equals(m_previousWaitPoint)) { - Logger.get(Category.FRAMEWORK) - .logRaw( - Severity.DEBUG, - getContextName() + " is waiting at " + waitPointTrace); - m_previousWaitPoint = waitPointTrace; - } - } - // Wait for the baton to be passed to us. - synchronized (m_threadSync) { - while (m_controlOwner != thisOwner && m_state != State.DONE) { - try { - m_threadSync.wait(); - } catch (InterruptedException e) { - } - } - m_controlOwner = thisOwner; - if (m_state != State.RUNNING && m_controlOwner == ControlOwner.SUBROUTINE) { - throw new ContextStoppedException(); - } - } - } - - /** - * Pass the baton (see the class comments) to the other thread and then wait for it to be passed - * back. - * - * @param thisOwner the thread from which this function is being called (and thus the - * baton-passing state that should be waited for) - * @param desiredOwner the thread to which the baton should be passed - * @throws ContextStoppedException if stop() is called on this Context while waiting. - */ - private void transferControl(final ControlOwner thisOwner, final ControlOwner desiredOwner) { - synchronized (m_threadSync) { - // Make sure we currently have the baton before trying to give it to - // someone else. - if (m_controlOwner != thisOwner) { - throw new IllegalStateException( - "Subroutine had control owner " - + m_controlOwner - + " but assumed control owner " - + thisOwner); - } - // Pass the baton. - m_controlOwner = desiredOwner; - if (m_controlOwner == ControlOwner.SUBROUTINE) { - c_currentContext = this; - } else { - c_currentContext = null; - } - m_threadSync.notifyAll(); - // Wait for the baton to be passed back. - waitForControl(thisOwner); - } - } - - /** - * This is the entry point for this Context's worker thread. - */ - private void threadFunction() { - try { - // OS threads run independently of one another, so we need to wait until - // the baton is passed to us before we can start running the user's code - waitForControl(ControlOwner.SUBROUTINE); - // Call into the user's code. - m_func.run(this); - Logger.get(Category.FRAMEWORK) - .logRaw(Severity.DEBUG, "Context " + getContextName() + " finished"); - } catch (ContextStoppedException ex) { - Logger.get(Category.FRAMEWORK) - .logRaw(Severity.WARNING, getContextName() + " was stopped"); - } catch (Exception ex) { - ex.printStackTrace(); - LoggerExceptionUtils.logException(ex); - Logger.get(Category.FRAMEWORK) - .logRaw(Severity.WARNING, "Context " + getContextName() + " died"); - } finally { - for (Mechanism m : m_ownedMechanisms) { - // Don't use this.releaseOwnership here, because that would cause a - // ConcurrentModificationException since we're iterating over m_ownedMechanisms - try { - m.releaseOwnership(this); - } catch (Exception ex) { - LoggerExceptionUtils.logException(ex); - } - } - synchronized (m_threadSync) { - m_state = State.DONE; - c_currentContext = null; - m_threadSync.notifyAll(); - } - m_ownedMechanisms.clear(); - } - } - +public interface Context { /** * Pauses the execution of this Context until the given predicate returns true or until * the timeout has elapsed. Yields to other Contexts in the meantime. @@ -379,12 +46,7 @@ private void threadFunction() { * * @return True if the predicate succeeded, false if the wait timed out. */ - public boolean waitForConditionOrTimeout( - final BooleanSupplier predicate, double timeoutSeconds) { - TimedPredicate timedPredicate = new TimedPredicate(predicate, timeoutSeconds); - waitFor(timedPredicate); - return timedPredicate.succeeded(); - } + boolean waitForConditionOrTimeout(final BooleanSupplier predicate, double timeoutSeconds); /** * Pauses the execution of this Context until the given predicate returns true. Yields to other @@ -393,27 +55,18 @@ public boolean waitForConditionOrTimeout( * Note that the predicate will be evaluated repeatedly (possibly on a different thread) while * the Context is paused to determine whether it should continue waiting. */ - public void waitFor(final BooleanSupplier predicate) { - if (!predicate.getAsBoolean()) { - m_blockingPredicate = predicate; - transferControl(ControlOwner.SUBROUTINE, ControlOwner.MAIN_THREAD); - } - } + void waitFor(final BooleanSupplier predicate); /** * Pauses the execution of this Context until the given LaunchedContext has finished running. */ - public void waitFor(final LaunchedContext otherContext) { - waitFor(otherContext::isDone); - } + void waitFor(final LaunchedContext otherContext); /** * Pauses the execution of this Context until all of the given LaunchedContexts have finished * running. */ - public void waitFor(final LaunchedContext... otherContexts) { - waitFor(() -> Arrays.stream(otherContexts).allMatch(LaunchedContext::isDone)); - } + void waitFor(final LaunchedContext... otherContexts); /** * Momentarily pause execution of this Context to allow other Contexts to execute. Execution of @@ -423,86 +76,27 @@ public void waitFor(final LaunchedContext... otherContexts) { * Procedures should call this periodically if they wouldn't otherwise call one of the wait* * methods for a while. */ - public void yield() { - m_blockingPredicate = null; - transferControl(ControlOwner.SUBROUTINE, ControlOwner.MAIN_THREAD); - } + void yield(); /** * Pauses the execution of this Context for the given length of time. */ - public void waitForSeconds(final double seconds) { - double startTime = RobotProvider.instance.getClock().getTime(); - waitFor(() -> RobotProvider.instance.getClock().getTime() - startTime > seconds); - } + void waitForSeconds(final double seconds); /** * Start running a new Context so the given procedure can run in parallel. */ - public LaunchedContext startAsync(final RunnableWithContext func) { - return new Context(func, this); - } + LaunchedContext startAsync(final RunnableWithContext func); /** * Start running a new Context so the given procedure can run in parallel. */ - public LaunchedContextWithValue startAsync(final RunnableWithContextWithValue func) { - return new ContextWithValue(func, this); - } + LaunchedContextWithValue startAsync(final RunnableWithContextWithValue func); /** * Start running a new Context so the given procedure can run in parallel. */ - public LaunchedContext startAsync(final Runnable func) { - return new Context(func, this); - } - - /** - * Interrupt the running of this Context and force it to terminate. - * - * A ContextStoppedException will be raised on this Context at the point where the Context most - * recently waited or yielded -- if this Context is currently executing, a - * ContextStoppedException will be raised immediately. - */ - @Override - public void stop() { - Logger.get(Category.FRAMEWORK) - .logRaw(Severity.DEBUG, "Stopping requested of " + getContextName()); - synchronized (m_threadSync) { - if (m_state != State.DONE) { - m_state = State.CANCELED; - } - if (m_controlOwner == ControlOwner.SUBROUTINE) { - throw new ContextStoppedException(); - } - } - } - - /** - * Entry point for the Scheduler to execute this Context. - * - * This should only be called from framework code; it is public only as an implementation - * detail. - */ - @Override - public void run() { - if (m_state == State.DONE) { - Scheduler.getInstance().cancel(this); - return; - } - if (m_state == State.CANCELED - || m_blockingPredicate == null - || m_blockingPredicate.getAsBoolean()) { - transferControl(ControlOwner.MAIN_THREAD, ControlOwner.SUBROUTINE); - } - } - - /** - * Returns true if this Context has finished running, false otherwise. - */ - public boolean isDone() { - return m_state == State.DONE; - } + LaunchedContext startAsync(final Runnable func); /** * Take ownership of the given Mechanism with this Context. @@ -513,10 +107,7 @@ public boolean isDone() { * * @see Mechanism#takeOwnership(Context, Context) */ - public void takeOwnership(final Mechanism mechanism) { - mechanism.takeOwnership(this, m_parentContext); - m_ownedMechanisms.add(mechanism); - } + void takeOwnership(final Mechanism mechanism); /** * Release ownership of the given Mechanism. @@ -527,8 +118,5 @@ public void takeOwnership(final Mechanism mechanism) { * @see #takeOwnership(Mechanism) * @see Mechanism#releaseOwnership(Context) */ - public void releaseOwnership(final Mechanism mechanism) { - mechanism.releaseOwnership(this); - m_ownedMechanisms.remove(mechanism); - } + void releaseOwnership(final Mechanism mechanism); } diff --git a/src/main/java/com/team766/framework/ContextImpl.java b/src/main/java/com/team766/framework/ContextImpl.java new file mode 100644 index 000000000..1711c5afa --- /dev/null +++ b/src/main/java/com/team766/framework/ContextImpl.java @@ -0,0 +1,480 @@ +package com.team766.framework; + +import com.team766.hal.Clock; +import com.team766.hal.RobotProvider; +import com.team766.logging.Category; +import com.team766.logging.Logger; +import com.team766.logging.LoggerExceptionUtils; +import com.team766.logging.Severity; +import java.lang.StackWalker.StackFrame; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.function.BooleanSupplier; + +/** + * See {@link Context} for a general description of the Context concept. + * + * Currently, threads of execution are implemented using OS threads, but this + * should be considered an implementation detail and may change in the future. + * Even though the framework creates multiple OS threads, it uses Java's + * monitors to implement a "baton passing" pattern in order to ensure that only + * one of threads is actually running at once (the others will be sleeping, + * waiting for the baton to be passed to them). + */ +class ContextImpl implements Runnable, ContextWithValue, LaunchedContextWithValue { + private Optional m_lastYieldedValue = Optional.empty(); + + /** + * Represents the baton-passing state (see class comments). Instead of + * passing a baton directly from one Context's thread to the next, each + * Context has its own baton that gets passed from the program's main thread + * to the Context's thread and back. While this is less efficient (double + * the number of OS context switches required), it makes the code simpler + * and more modular. + */ + private enum ControlOwner { + MAIN_THREAD, + SUBROUTINE, + } + + /** + * Indicates the lifetime state of this Context. + */ + private enum State { + /** + * The Context has been started (a Context is started immediately upon + * construction). + */ + RUNNING, + /** + * stop() has been called on this Context (but it has not been allowed + * to respond to the stop request yet). + */ + CANCELED, + /** + * The Context's execution has come to an end. + */ + DONE, + } + + // package visible for testing + /* package */ static class TimedPredicate implements BooleanSupplier { + private final Clock clock; + private final BooleanSupplier predicate; + private final double deadlineSeconds; + private boolean succeeded = false; + + // package visible for testing + /* package */ TimedPredicate( + Clock clock, BooleanSupplier predicate, double timeoutSeconds) { + this.clock = clock; + this.deadlineSeconds = clock.getTime() + timeoutSeconds; + this.predicate = predicate; + } + + public TimedPredicate(BooleanSupplier predicate, double timeoutSeconds) { + this(RobotProvider.instance.getClock(), predicate, timeoutSeconds); + } + + public boolean getAsBoolean() { + if (predicate.getAsBoolean()) { + succeeded = true; + return true; + } + if (clock.getTime() >= deadlineSeconds) { + succeeded = false; + return true; + } else { + return false; + } + } + + public boolean succeeded() { + return succeeded; + } + } + + private static ContextImpl c_currentContext = null; + + /** + * Returns the currently-executing Context. + * + * This is maintained for things like checking Mechanism ownership, but + * intentionally only has package-private visibility - code outside of the + * framework should ideally pass around references to the current context + * object rather than cheating with this static accessor. + */ + static ContextImpl currentContext() { + return c_currentContext; + } + + /** + * The top-level procedure being run by this Context. + */ + private final RunnableWithContextWithValue m_func; + + /** + * If this Context was created by another context using + * {@link #startAsync}, this will contain a reference to that originating + * Context. + */ + private final ContextImpl m_parentContext; + + /** + * The OS thread that this Context is executing on. + */ + private final Thread m_thread; + + /** + * Used to synchronize access to this Context's state variable. + */ + private final Object m_threadSync; + + /** + * This Context's lifetime state. + */ + private State m_state; + + /** + * If one of the wait* methods has been called on this Context, this + * contains the predicate which should be checked to determine whether + * the Context's execution should be resumed. This makes it more efficient + * to poll completion criteria without needing to context-switch between + * threads. + */ + private BooleanSupplier m_blockingPredicate; + + /** + * Set to SUBROUTINE when this Context is executing and MAIN_THREAD + * otherwise. + */ + private ControlOwner m_controlOwner; + + /** + * Contains the method name and line number at which this Context most + * recently yielded. + */ + private String m_previousWaitPoint; + + /** + * The mechanisms that have been claimed by this Context using + * takeOwnership. These will be automatically released when the Context + * finishes executing. + */ + private Set m_ownedMechanisms = new HashSet(); + + /* + * Constructors are intentionally private or package-private. New contexts + * should be created with {@link Context#startAsync} or + * {@link Scheduler#startAsync}. + */ + + ContextImpl(final RunnableWithContextWithValue func, final ContextImpl parentContext) { + m_func = func; + m_parentContext = parentContext; + Logger.get(Category.FRAMEWORK) + .logRaw( + Severity.DEBUG, + "Starting context " + getContextName() + " for " + func.toString()); + m_threadSync = new Object(); + m_previousWaitPoint = null; + m_controlOwner = ControlOwner.MAIN_THREAD; + m_state = State.RUNNING; + m_thread = new Thread(this::threadFunction, getContextName()); + m_thread.start(); + Scheduler.getInstance().add(this); + } + + ContextImpl(final RunnableWithContextWithValue func) { + this(func, null); + } + + ContextImpl(final Runnable func, final ContextImpl parentContext) { + this((context) -> func.run()); + } + + ContextImpl(final Runnable func) { + this(func, null); + } + + /** + * Returns a string meant to uniquely identify this Context (e.g. for use in logging). + */ + public String getContextName() { + return "Context/" + Integer.toHexString(hashCode()) + "/" + m_func.toString(); + } + + @Override + public String toString() { + String repr = getContextName(); + if (currentContext() == this) { + repr += " running"; + } + repr += "\n"; + repr += StackTraceUtils.getStackTrace(m_thread); + return repr; + } + + /** + * Walks up the call stack until it reaches a frame that isn't from the Context class, then + * returns a string representation of that frame. This is used to generate a concise string + * representation of from where the user called into framework code. + */ + private String getExecutionPoint() { + StackWalker walker = StackWalker.getInstance(); + return walker.walk( + s -> + s.dropWhile(f -> f.getClassName() != ContextImpl.this.getClass().getName()) + .filter( + f -> + f.getClassName() + != ContextImpl.this.getClass().getName()) + .findFirst() + .map(StackFrame::toString) + .orElse(null)); + } + + /** + * Wait until the baton (see the class comments) has been passed to this thread. + * + * @param thisOwner the thread from which this function is being called (and thus the + * baton-passing state that should be waited for) + * @throws ContextStoppedException if stop() is called on this Context while waiting. + */ + private void waitForControl(final ControlOwner thisOwner) { + // If this is being called from the worker thread, log from where in the + // user's code that the context is waiting. This is provided as a + // convenience so the user can track the progress of execution through + // their procedures. + if (thisOwner == ControlOwner.SUBROUTINE) { + String waitPointTrace = getExecutionPoint(); + if (waitPointTrace != null && !waitPointTrace.equals(m_previousWaitPoint)) { + Logger.get(Category.FRAMEWORK) + .logRaw( + Severity.DEBUG, + getContextName() + " is waiting at " + waitPointTrace); + m_previousWaitPoint = waitPointTrace; + } + } + // Wait for the baton to be passed to us. + synchronized (m_threadSync) { + while (m_controlOwner != thisOwner && m_state != State.DONE) { + try { + m_threadSync.wait(); + } catch (InterruptedException e) { + } + } + m_controlOwner = thisOwner; + if (m_state != State.RUNNING && m_controlOwner == ControlOwner.SUBROUTINE) { + throw new ContextStoppedException(); + } + } + } + + /** + * Pass the baton (see the class comments) to the other thread and then wait for it to be passed + * back. + * + * @param thisOwner the thread from which this function is being called (and thus the + * baton-passing state that should be waited for) + * @param desiredOwner the thread to which the baton should be passed + * @throws ContextStoppedException if stop() is called on this Context while waiting. + */ + private void transferControl(final ControlOwner thisOwner, final ControlOwner desiredOwner) { + synchronized (m_threadSync) { + // Make sure we currently have the baton before trying to give it to + // someone else. + if (m_controlOwner != thisOwner) { + throw new IllegalStateException( + "Subroutine had control owner " + + m_controlOwner + + " but assumed control owner " + + thisOwner); + } + // Pass the baton. + m_controlOwner = desiredOwner; + if (m_controlOwner == ControlOwner.SUBROUTINE) { + c_currentContext = this; + } else { + c_currentContext = null; + } + m_threadSync.notifyAll(); + // Wait for the baton to be passed back. + waitForControl(thisOwner); + } + } + + /** + * This is the entry point for this Context's worker thread. + */ + private void threadFunction() { + try { + // OS threads run independently of one another, so we need to wait until + // the baton is passed to us before we can start running the user's code + waitForControl(ControlOwner.SUBROUTINE); + // Call into the user's code. + m_func.run(this); + Logger.get(Category.FRAMEWORK) + .logRaw(Severity.DEBUG, "Context " + getContextName() + " finished"); + } catch (ContextStoppedException ex) { + Logger.get(Category.FRAMEWORK) + .logRaw(Severity.WARNING, getContextName() + " was stopped"); + } catch (Exception ex) { + ex.printStackTrace(); + LoggerExceptionUtils.logException(ex); + Logger.get(Category.FRAMEWORK) + .logRaw(Severity.WARNING, "Context " + getContextName() + " died"); + } finally { + for (Mechanism m : m_ownedMechanisms) { + // Don't use this.releaseOwnership here, because that would cause a + // ConcurrentModificationException since we're iterating over m_ownedMechanisms + try { + m.releaseOwnership(this); + } catch (Exception ex) { + LoggerExceptionUtils.logException(ex); + } + } + synchronized (m_threadSync) { + m_state = State.DONE; + c_currentContext = null; + m_threadSync.notifyAll(); + } + m_ownedMechanisms.clear(); + } + } + + @Override + public boolean waitForConditionOrTimeout( + final BooleanSupplier predicate, double timeoutSeconds) { + TimedPredicate timedPredicate = new TimedPredicate(predicate, timeoutSeconds); + waitFor(timedPredicate); + return timedPredicate.succeeded(); + } + + @Override + public void waitFor(final BooleanSupplier predicate) { + if (!predicate.getAsBoolean()) { + m_blockingPredicate = predicate; + transferControl(ControlOwner.SUBROUTINE, ControlOwner.MAIN_THREAD); + } + } + + @Override + public void waitFor(final LaunchedContext otherContext) { + waitFor(otherContext::isDone); + } + + @Override + public void waitFor(final LaunchedContext... otherContexts) { + waitFor(() -> Arrays.stream(otherContexts).allMatch(LaunchedContext::isDone)); + } + + @Override + public void waitForSeconds(final double seconds) { + double startTime = RobotProvider.instance.getClock().getTime(); + waitFor(() -> RobotProvider.instance.getClock().getTime() - startTime > seconds); + } + + @Override + public void yield() { + m_blockingPredicate = null; + transferControl(ControlOwner.SUBROUTINE, ControlOwner.MAIN_THREAD); + } + + @Override + public void yield(final T valueToYield) { + m_lastYieldedValue = Optional.of(valueToYield); + this.yield(); + } + + @Override + public T lastYieldedValue() { + return m_lastYieldedValue.orElse(null); + } + + @Override + public boolean hasYieldedValue() { + return m_lastYieldedValue.isPresent(); + } + + @Override + public T getAndClearLastYieldedValue() { + final var result = m_lastYieldedValue; + m_lastYieldedValue = Optional.empty(); + return result.orElse(null); + } + + @Override + public LaunchedContext startAsync(final RunnableWithContext func) { + return new ContextImpl<>(func::run, this); + } + + @Override + public LaunchedContextWithValue startAsync(final RunnableWithContextWithValue func) { + return new ContextImpl(func, this); + } + + @Override + public LaunchedContext startAsync(final Runnable func) { + return new ContextImpl<>(func, this); + } + + /** + * Interrupt the running of this Context and force it to terminate. + * + * A ContextStoppedException will be raised on this Context at the point where the Context most + * recently waited or yielded -- if this Context is currently executing, a + * ContextStoppedException will be raised immediately. + */ + @Override + public void stop() { + Logger.get(Category.FRAMEWORK) + .logRaw(Severity.DEBUG, "Stopping requested of " + getContextName()); + synchronized (m_threadSync) { + if (m_state != State.DONE) { + m_state = State.CANCELED; + } + if (m_controlOwner == ControlOwner.SUBROUTINE) { + throw new ContextStoppedException(); + } + } + } + + /** + * Entry point for the Scheduler to execute this Context. + * + * This should only be called from framework code; it is public only as an implementation + * detail. + */ + @Override + public void run() { + if (m_state == State.DONE) { + Scheduler.getInstance().cancel(this); + return; + } + if (m_state == State.CANCELED + || m_blockingPredicate == null + || m_blockingPredicate.getAsBoolean()) { + transferControl(ControlOwner.MAIN_THREAD, ControlOwner.SUBROUTINE); + } + } + + @Override + public boolean isDone() { + return m_state == State.DONE; + } + + @Override + public void takeOwnership(final Mechanism mechanism) { + mechanism.takeOwnership(this, m_parentContext); + m_ownedMechanisms.add(mechanism); + } + + @Override + public void releaseOwnership(final Mechanism mechanism) { + mechanism.releaseOwnership(this); + m_ownedMechanisms.remove(mechanism); + } +} diff --git a/src/main/java/com/team766/framework/ContextWithValue.java b/src/main/java/com/team766/framework/ContextWithValue.java index 6810a4dc2..14950ce1a 100644 --- a/src/main/java/com/team766/framework/ContextWithValue.java +++ b/src/main/java/com/team766/framework/ContextWithValue.java @@ -4,53 +4,14 @@ * A {@link Context} that also allows the ProcedureWithValues running on it to yield values when * running asynchronously. */ -public class ContextWithValue extends Context implements LaunchedContextWithValue { - - private T m_lastYieldedValue; - - @SuppressWarnings("unchecked") - ContextWithValue(final RunnableWithContextWithValue func, final Context parentContext) { - super((context) -> func.run((ContextWithValue) context), parentContext); - } - - @SuppressWarnings("unchecked") - ContextWithValue(final RunnableWithContextWithValue func) { - super((context) -> func.run((ContextWithValue) context)); - } - - /** - * Return the most recent value passed to yield(T). - * - * Implements LaunchedContextWithValue - */ - @Override - public T lastYieldedValue() { - return m_lastYieldedValue; - } - - /** - * Return the most recent value passed to yield(T), and clear the recorded last yielded value - * such that subsequent calls to lastYieldedValue() will return null. - * - * Implements LaunchedContextWithValue - */ - @Override - public T getAndClearLastYieldedValue() { - final var result = m_lastYieldedValue; - m_lastYieldedValue = null; - return result; - } - +public interface ContextWithValue extends Context { /** * Momentarily pause execution of this Context to allow other Contexts to execute. Execution of * this Context will resume as soon as possible after the other Contexts have been given a * chance to run. * * The most recent value passed to this yield(T) method will be returned by subsequent calls to - * lastYieldedValue(). + * LaunchedContextWithValue.lastYieldedValue(). */ - public void yield(final T valueToYield) { - m_lastYieldedValue = valueToYield; - this.yield(); - } + void yield(final T valueToYield); } diff --git a/src/main/java/com/team766/framework/LaunchedContextWithValue.java b/src/main/java/com/team766/framework/LaunchedContextWithValue.java index 753fce034..5539d0522 100644 --- a/src/main/java/com/team766/framework/LaunchedContextWithValue.java +++ b/src/main/java/com/team766/framework/LaunchedContextWithValue.java @@ -7,17 +7,21 @@ public interface LaunchedContextWithValue extends LaunchedContext { /** * Return the most recent value that the Procedure passed to Context.yield(T). - * - * Implements LaunchedContextWithValue + * Return null if a value has never been yielded, or a value has not been yielded since the last + * call to getAndClearLastYieldedValue. */ T lastYieldedValue(); + /** + * Return true if a has been yielded by the Procedure since since the last call to + * getAndClearLastYieldedValue. Return false otherwise. + */ + boolean hasYieldedValue(); + /** * Return the most recent value that the Procedure passed to Context.yield(T), and clear the - * recorded last yielded value such that subsequent calls to lastYieldedValue() will return - * null. - * - * Implements LaunchedContextWithValue + * recorded last yielded value such that subsequent calls to hasYieldedValue() will return + * false. */ T getAndClearLastYieldedValue(); } diff --git a/src/main/java/com/team766/framework/Mechanism.java b/src/main/java/com/team766/framework/Mechanism.java index 8761b29af..285117863 100644 --- a/src/main/java/com/team766/framework/Mechanism.java +++ b/src/main/java/com/team766/framework/Mechanism.java @@ -6,7 +6,7 @@ import com.team766.logging.Severity; public abstract class Mechanism extends LoggingBase { - private Context m_owningContext = null; + private ContextImpl m_owningContext = null; private Thread m_runningPeriodic = null; public Mechanism() { @@ -44,9 +44,11 @@ public String getName() { } protected void checkContextOwnership() { - if (Context.currentContext() != m_owningContext && m_runningPeriodic == null) { + if (ContextImpl.currentContext() != m_owningContext && m_runningPeriodic == null) { String message = - getName() + " tried to be used by " + Context.currentContext().getContextName(); + getName() + + " tried to be used by " + + ContextImpl.currentContext().getContextName(); if (m_owningContext != null) { message += " while owned by " + m_owningContext.getContextName(); } else { @@ -57,7 +59,7 @@ protected void checkContextOwnership() { } } - void takeOwnership(final Context context, final Context parentContext) { + void takeOwnership(final ContextImpl context, final ContextImpl parentContext) { if (m_owningContext != null && m_owningContext == parentContext) { Logger.get(Category.FRAMEWORK) .logRaw( @@ -102,7 +104,7 @@ void takeOwnership(final Context context, final Context parentContext) { m_owningContext = context; } - void releaseOwnership(final Context context) { + void releaseOwnership(final ContextImpl context) { if (m_owningContext != context) { LoggerExceptionUtils.logException( new Exception( diff --git a/src/main/java/com/team766/framework/Scheduler.java b/src/main/java/com/team766/framework/Scheduler.java index 37e544c6a..68413a934 100644 --- a/src/main/java/com/team766/framework/Scheduler.java +++ b/src/main/java/com/team766/framework/Scheduler.java @@ -75,15 +75,15 @@ public void reset() { } public LaunchedContext startAsync(final RunnableWithContext func) { - return new Context(func); + return new ContextImpl<>(func::run); } - public LaunchedContext startAsync(final RunnableWithContextWithValue func) { - return new ContextWithValue(func); + public LaunchedContextWithValue startAsync(final RunnableWithContextWithValue func) { + return new ContextImpl(func); } public LaunchedContext startAsync(final Runnable func) { - return new Context(func); + return new ContextImpl<>(func); } public void run() { diff --git a/src/test/java/com/team766/framework/TimedPredicateTest.java b/src/test/java/com/team766/framework/TimedPredicateTest.java index 42c527f78..90b9a0de0 100644 --- a/src/test/java/com/team766/framework/TimedPredicateTest.java +++ b/src/test/java/com/team766/framework/TimedPredicateTest.java @@ -11,8 +11,8 @@ public class TimedPredicateTest { @Test public void testTimedPredicateTimedOut() { MockClock testClock = new MockClock(1710411240.0); - Context.TimedPredicate predicate = - new Context.TimedPredicate(testClock, () -> false, 1.766); + ContextImpl.TimedPredicate predicate = + new ContextImpl.TimedPredicate(testClock, () -> false, 1.766); assertFalse(predicate.getAsBoolean()); testClock.tick(1.0); assertFalse(predicate.getAsBoolean()); @@ -24,8 +24,8 @@ public void testTimedPredicateTimedOut() { @Test public void testTimedPredicateCondition() { MockClock testClock = new MockClock(1710411240.0); - Context.TimedPredicate predicate = - new Context.TimedPredicate( + ContextImpl.TimedPredicate predicate = + new ContextImpl.TimedPredicate( testClock, new BooleanSupplier() { private int counter = 0; diff --git a/src/test/java/com/team766/framework/YieldWithValueTest.java b/src/test/java/com/team766/framework/YieldWithValueTest.java index 528649029..ef484f546 100644 --- a/src/test/java/com/team766/framework/YieldWithValueTest.java +++ b/src/test/java/com/team766/framework/YieldWithValueTest.java @@ -14,6 +14,11 @@ private static class ValueConsumer extends Procedure { @Override public void run(Context context) { var generator = context.startAsync(new ValueGenerator()); + + assertNull( + generator.lastYieldedValue(), + "lastYieldedValue should be null before the procedure yields a value"); + while (generator.lastYieldedValue() == null || generator.lastYieldedValue() < 10) { var value = generator.lastYieldedValue(); if (value != null) {