Commits

Michael Ludwig  committed 05166e0

Add documentation to remaining task package classes.

  • Participants
  • Parent commits d46108b

Comments (0)

Files changed (3)

File src/main/java/com/lhkbob/entreri/task/Job.java

 
 import com.lhkbob.entreri.ComponentData;
 
+/**
+ * <p>
+ * Job represents a list of {@link Task tasks} that must be executed in a
+ * particular order so that they produce a meaningful computation over an entity
+ * system. Examples of a job might be to render a frame, which could then be
+ * decomposed into tasks for computing the visible objects, occluded objects,
+ * the optimal rendering order, and shadow computations, etc.
+ * <p>
+ * Jobs are created by first getting the {@link Scheduler} from a particular
+ * EntitySystem, and then calling {@link Scheduler#createJob(String, Task...)}.
+ * The name of a job is primarily used to for informational purposes and does
+ * not affect its behavior.
+ * 
+ * @author Michael Ludwig
+ * 
+ */
 public class Job implements Runnable {
     private final Task[] tasks;
     private final Map<Class<? extends Result>, List<ResultReporter>> resultMethods;
     private final Set<Class<? extends Result>> singletonResults;
     private int taskIndex;
 
+    /**
+     * Create a new job with the given name and tasks.
+     * 
+     * @param name The name of the job
+     * @param scheduler The owning scheduler
+     * @param tasks The tasks in order of execution
+     * @throws NullPointerException if name is null, tasks is null or contains
+     *             null elements
+     */
     Job(String name, Scheduler scheduler, Task... tasks) {
+        if (name == null) {
+            throw new NullPointerException("Name cannot be null");
+        }
         this.scheduler = scheduler;
         this.tasks = new Task[tasks.length];
         this.name = name;
 
         singletonResults = new HashSet<Class<? extends Result>>();
         resultMethods = new HashMap<Class<? extends Result>, List<ResultReporter>>();
+        taskIndex = -1;
 
         boolean exclusive = false;
         Set<Class<? extends ComponentData<?>>> typeLocks = new HashSet<Class<? extends ComponentData<?>>>();
         for (int i = 0; i < tasks.length; i++) {
+            if (tasks[i] == null) {
+                throw new NullPointerException("Task cannot be null");
+            }
+
             this.tasks[i] = tasks[i];
 
             // collect parallelization info (which should not change over
         }
     }
 
+    /**
+     * @return The designated name of this job
+     */
     public String getName() {
         return name;
     }
 
+    /**
+     * @return The Scheduler that created this job
+     */
     public Scheduler getScheduler() {
         return scheduler;
     }
 
+    /**
+     * <p>
+     * Invoke all tasks in this job. This method is thread-safe and will use its
+     * owning scheduler to coordinate the locks necessary to safely execute its
+     * tasks.
+     * <p>
+     * Although {@link Scheduler} has convenience methods to repeatedly invoke a
+     * job, this method can be called directly if a more controlled job
+     * execution scheme is required.
+     */
     @Override
     public void run() {
         // repeatedly run jobs until no task produces a post-process task
                 }
             }
 
+            // set this to negative so that report() can fail now that
+            // we're not executing tasks anymore
+            taskIndex = -1;
+
             if (postProcess.isEmpty()) {
                 // nothing to process afterwards
                 return null;
         }
     }
 
+    /**
+     * Report the given result instance to all tasks yet to be executed by this
+     * job, that have declared a public method named 'report' that takes a
+     * Result sub-type that is compatible with <tt>r</tt>'s type.
+     * 
+     * @param r The result to report
+     * @throws NullPointerException if r is null
+     * @throws IllegalStateException if r is a singleton result whose type has
+     *             already been reported by another task in this job, or if the
+     *             job is not currently executing tasks
+     */
     public void report(Result r) {
+        if (r == null) {
+            throw new NullPointerException("Cannot report null results");
+        }
+        if (taskIndex < 0) {
+            throw new IllegalStateException("Can only be invoked by a task from within run()");
+        }
+
         if (r.isSingleton()) {
             // make sure this is the first we've seen the result
             if (!singletonResults.add(r.getClass())) {
         }
     }
 
+    @Override
+    public String toString() {
+        return "Job(" + name + ", # tasks=" + tasks.length + ")";
+    }
+
     private class ResultReporter {
         private final Method reportMethod;
         private final int taskIndex;

File src/main/java/com/lhkbob/entreri/task/ParallelAware.java

 import java.util.Set;
 
 import com.lhkbob.entreri.ComponentData;
+import com.lhkbob.entreri.Entity;
+import com.lhkbob.entreri.EntitySystem;
 
+/**
+ * <p>
+ * ParallelAware is an interface that {@link Task} implementations can
+ * implement. Tasks that are parallel aware hold to the contract that they will
+ * only modify a limited and knowable set of component types, or that they will
+ * or will not add or remove entities from the entity system.
+ * <p>
+ * With that assumption based on the what is returned by
+ * {@link #getAccessedComponents()} and {@link #isEntitySetModified()}, jobs
+ * will automatically guarantee thread safe execution of their tasks.
+ * <p>
+ * It is highly recommended to implement ParallelAware if it's known what types
+ * of components or entities will be modified at compile time.
+ * 
+ * @author Michael Ludwig
+ * 
+ */
 public interface ParallelAware {
-    // FIXME make sure to document that these cannot change for an instance
+    /**
+     * <p>
+     * Get the set of all component data types that might have their data
+     * mutated, be added to, or removed from an entity. This must always return
+     * the same types for a given instance, it cannot change based on state of
+     * the task. Instead, it must return the maximal set of types that might be
+     * added, removed, or modified by the task.
+     * <p>
+     * If a task's component access is determined at runtime, then it should not
+     * be parallel aware, or it should return true from
+     * {@link #isEntitySetModified()}.
+     * <p>
+     * Jobs that do not share any access to the same component types (i.e. their
+     * intersection of the returned set is empty), can be run in parallel if
+     * they both return false from {@link #isEntitySetModified()}.
+     * 
+     * @return The set of all component types that might be added, removed, or
+     *         modified by the task
+     */
     public Set<Class<? extends ComponentData<?>>> getAccessedComponents();
 
+    /**
+     * <p>
+     * Return whether or not {@link Entity entities} are added or removed from
+     * an EntitySystem. Note that this refers to using
+     * {@link EntitySystem#addEntity()} or
+     * {@link EntitySystem#removeEntity(Entity)}. When true is returned, the job
+     * will be forced to execute with an exclusive lock that blocks other jobs.
+     * <p>
+     * Thus it is a best practice to limit the run-time of tasks that require
+     * exclusive locks. It is better to have a parallel aware task that accesses
+     * only specific component types to determine if an entity must be added or
+     * removed.
+     * <p>
+     * Once these are determined, it keeps track and returns a new task from
+     * {@link Task#process(EntitySystem, Job)} that will get the exclusive lock
+     * and perform the determined additions or removals.
+     * <p>
+     * Like {@link #getAccessedComponents()}, this must always return the same
+     * value for a given instance, and cannot change during its lifetime.
+     * 
+     * @return True if the task might add or remove entities from the system
+     */
     public boolean isEntitySetModified();
 }

File src/main/java/com/lhkbob/entreri/task/Scheduler.java

 import com.lhkbob.entreri.ComponentData;
 import com.lhkbob.entreri.EntitySystem;
 
+/**
+ * <p>
+ * Scheduler coordinates the multi-threaded execution of jobs that process an
+ * EntitySystem. It is the factory that creates jobs and contains convenience
+ * methods to schedule the execution of jobs.
+ * <p>
+ * As an example, here's how you can set up a 'rendering' job, assuming that all
+ * tasks necessary to perform rendering are in an array called
+ * <tt>renderTasks</tt>:
+ * 
+ * <pre>
+ * Job renderJob = system.getScheduler().createJob(&quot;rendering&quot;, renderTasks);
+ * ExecutorService service = system.getScheduler().runEvery(1.0 / 60.0, renderJob);
+ * 
+ * // ... perform game logic, and wait for exit request
+ * service.shutdown();
+ * </pre>
+ * 
+ * @author Michael Ludwig
+ * 
+ */
 public class Scheduler {
     private final ThreadGroup schedulerGroup;
 
 
     private final EntitySystem system;
 
+    /**
+     * Create a new Scheduler for the given EntitySystem. It is recommended to
+     * use the scheduler provided by the system. If multiple schedulers exist
+     * for the same entity system, they cannot guarantee thread safety between
+     * each other, only within their own jobs.
+     * 
+     * @see EntitySystem#getScheduler()
+     * @param system The EntitySystem accessed by jobs created by this scheduler
+     * @throws NullPointerException if system is null
+     */
     public Scheduler(EntitySystem system) {
+        if (system == null) {
+            throw new NullPointerException("EntitySystem cannot be null");
+        }
         this.system = system;
 
         schedulerGroup = new ThreadGroup("job-scheduler");
         typeLocks = new ConcurrentHashMap<Class<? extends ComponentData<?>>, ReentrantLock>();
     }
 
+    /**
+     * @return The EntitySystem accessed by this scheduler
+     */
     public EntitySystem getEntitySystem() {
         return system;
     }
 
+    /**
+     * @return The read-write lock used to coordinate entity data access
+     */
     ReentrantReadWriteLock getEntitySystemLock() {
         return exclusiveLock;
     }
 
+    /**
+     * @param id The component type to lock
+     * @return The lock used to coordinate access to the particular componen
+     *         type
+     */
     ReentrantLock getTypeLock(Class<? extends ComponentData<?>> id) {
         ReentrantLock lock = typeLocks.get(id);
         if (lock == null) {
         return lock;
     }
 
+    /**
+     * Create a new job with the given <tt>name</tt>, that will execute the
+     * provided tasks in order.
+     * 
+     * @param name The name of the new job
+     * @param tasks The tasks of the job
+     * @return The new job
+     * @throws NullPointerException if name is null, tasks is null or contains
+     *             null elements
+     */
     public Job createJob(String name, Task... tasks) {
         return new Job(name, this, tasks);
     }
 
+    /**
+     * Execute the given job on the current thread. This will not return until
+     * after the job has completed invoking all of its tasks, and any
+     * subsequently produced tasks.
+     * <p>
+     * This is a convenience for invoking {@link Job#run()}, and exists
+     * primarily to parallel the other runX(Job) methods.
+     * 
+     * @param job The job to run
+     * @throws NullPointerException if job is null
+     * @throws IllegalArgumentException if job was not created by this scheduler
+     */
     public void runOnCurrentThread(Job job) {
         if (job == null) {
             throw new NullPointerException("Job cannot be null");
         job.run();
     }
 
+    /**
+     * <p>
+     * Execute the given job once on a new thread. This will create a new thread
+     * that will invoke the job once and then terminate once the job returns.
+     * This method will return after the thread starts and will not block the
+     * calling thread while the job is executed.
+     * <p>
+     * This should be used as a convenience to invoke one-off jobs that should
+     * not block a performance sensitive thread.
+     * 
+     * @param job The job to run
+     * @throws NullPointerException if job is null
+     * @throws IllegalArgumentException if job was not created by this scheduler
+     */
     public void runOnSeparateThread(Job job) {
         if (job == null) {
             throw new NullPointerException("Job cannot be null");
         jobThread.start();
     }
 
+    /**
+     * <p>
+     * Create an ExecutorService that is configured to execute the given job
+     * every <tt>dt</tt> seconds. Assuming that the job terminates in under
+     * <tt>dt</tt> seconds, it will not be invoked until <tt>dt</tt> seconds
+     * after it was first started.
+     * <p>
+     * To schedule a rendering job to run at 60 FPS, you could call
+     * <code>runEvery(1.0 / 60.0, renderJob)</code>.
+     * <p>
+     * The returned ExecutorService should have its
+     * {@link ExecutorService#shutdown() shutdown()} method called when the job
+     * no longer needs to be invoked. Scheduling timing is undefined if new
+     * Runnables or Callables are submitted to the returned service.
+     * 
+     * @param dt The amount of time between the start of each job execution
+     * @param job The job to be repeatedly executed
+     * @return An unconfigurable executor service that performs the scheduling,
+     *         and owns the execution thread
+     * @throws NullPointerException if job is null
+     * @throws IllegalArgumentException if job was not created by this
+     *             scheduler, or if dt is negative
+     */
     public ExecutorService runEvery(double dt, Job job) {
+        if (job == null) {
+            throw new NullPointerException("Job cannot be null");
+        }
+        if (job.getScheduler() != this) {
+            throw new IllegalArgumentException("Job was created by a different scheduler");
+        }
+        if (dt < 0) {
+            throw new IllegalArgumentException("Time between jobs cannot be negative: " + dt);
+        }
+
         final String name = String.format("job-%s-every-%.2fs", job.getName(), dt);
         ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
             @Override
         return Executors.unconfigurableExecutorService(service);
     }
 
+    /**
+     * <p>
+     * Create an ExecutorService that is configured to execute the given job
+     * back to back as fast as the job executes.
+     * <p>
+     * This effectively performs the following logic on a separate thread:
+     * 
+     * <pre>
+     * while (true) {
+     *     job.run();
+     * }
+     * </pre>
+     * <p>
+     * The returned ExecutorService should have its
+     * {@link ExecutorService#shutdown() shutdown()} method called when the job
+     * no longer needs to be invoked. Scheduling timing is undefined if new
+     * Runnables or Callables are submitted to the returned service.
+     * 
+     * @param job The job to be repeatedly executed
+     * @return An unconfigurable executor service that performs the scheduling,
+     *         and owns the execution thread
+     * @throws NullPointerException if job is null
+     * @throws IllegalArgumentException if job was not created by this scheduler
+     */
     public ExecutorService runContinuously(Job job) {
+        if (job == null) {
+            throw new NullPointerException("Job cannot be null");
+        }
+        if (job.getScheduler() != this) {
+            throw new IllegalArgumentException("Job was created by a different scheduler");
+        }
+
         final String name = String.format("job-%s-as-fast-as-possible", job.getName());
         ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
             @Override