View Javadoc

1   /*
2    * Entreri, an entity-component framework in Java
3    *
4    * Copyright (c) 2013, Michael Ludwig
5    * All rights reserved.
6    *
7    * Redistribution and use in source and binary forms, with or without modification,
8    * are permitted provided that the following conditions are met:
9    *
10   *     Redistributions of source code must retain the above copyright notice,
11   *         this list of conditions and the following disclaimer.
12   *     Redistributions in binary form must reproduce the above copyright notice,
13   *         this list of conditions and the following disclaimer in the
14   *         documentation and/or other materials provided with the distribution.
15   *
16   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19   * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
20   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23   * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26   */
27  package com.lhkbob.entreri.task;
28  
29  import com.lhkbob.entreri.Component;
30  
31  import java.lang.reflect.InvocationTargetException;
32  import java.lang.reflect.Method;
33  import java.util.*;
34  
35  /**
36   * <p/>
37   * Job represents a list of {@link Task tasks} that must be executed in a particular order
38   * so that they produce a meaningful computation over an entity system. Examples of a job
39   * might be to render a frame, which could then be decomposed into tasks for computing the
40   * visible objects, occluded objects, the optimal rendering order, and shadow
41   * computations, etc.
42   * <p/>
43   * Jobs are created by first getting the {@link Scheduler} from a particular EntitySystem,
44   * and then calling {@link Scheduler#createJob(String, Task...)}. The name of a job is
45   * primarily used to for informational purposes and does not affect its behavior.
46   *
47   * @author Michael Ludwig
48   */
49  public class Job implements Runnable {
50      private final Task[] tasks;
51      private final Map<Class<? extends Result>, List<ResultReporter>> resultMethods;
52  
53      private final boolean needsExclusiveLock;
54      private final List<Class<? extends Component>> locks;
55  
56      private final Scheduler scheduler;
57      private final String name;
58  
59      private final Set<Class<? extends Result>> singletonResults;
60      private int taskIndex;
61  
62      /**
63       * Create a new job with the given name and tasks.
64       *
65       * @param name      The name of the job
66       * @param scheduler The owning scheduler
67       * @param tasks     The tasks in order of execution
68       *
69       * @throws NullPointerException if name is null, tasks is null or contains null
70       *                              elements
71       */
72      Job(String name, Scheduler scheduler, Task... tasks) {
73          if (name == null) {
74              throw new NullPointerException("Name cannot be null");
75          }
76          this.scheduler = scheduler;
77          this.tasks = new Task[tasks.length];
78          this.name = name;
79  
80          singletonResults = new HashSet<>();
81          resultMethods = new HashMap<>();
82          taskIndex = -1;
83  
84          boolean exclusive = false;
85          Set<Class<? extends Component>> typeLocks = new HashSet<>();
86          for (int i = 0; i < tasks.length; i++) {
87              if (tasks[i] == null) {
88                  throw new NullPointerException("Task cannot be null");
89              }
90  
91              this.tasks[i] = tasks[i];
92  
93              // collect parallelization info (which should not change over
94              // a task's lifetime)
95              if (tasks[i] instanceof ParallelAware) {
96                  ParallelAware pa = (ParallelAware) tasks[i];
97                  exclusive |= pa.isEntitySetModified();
98                  typeLocks.addAll(pa.getAccessedComponents());
99              } else {
100                 // must assume it could touch anything
101                 exclusive = true;
102             }
103 
104             // record all result report methods exposed by this task
105             for (Method m : tasks[i].getClass().getMethods()) {
106                 if (m.getName().equals("report")) {
107                     if (m.getReturnType().equals(void.class) &&
108                         m.getParameterTypes().length == 1 &&
109                         Result.class.isAssignableFrom(m.getParameterTypes()[0])) {
110                         // found a valid report method
111                         m.setAccessible(true);
112                         ResultReporter reporter = new ResultReporter(m, i);
113                         Class<? extends Result> type = reporter.getResultType();
114 
115                         List<ResultReporter> all = resultMethods.get(type);
116                         if (all == null) {
117                             all = new ArrayList<>();
118                             resultMethods.put(type, all);
119                         }
120 
121                         all.add(reporter);
122                     }
123                 }
124             }
125         }
126 
127         if (exclusive) {
128             needsExclusiveLock = true;
129             locks = null;
130         } else {
131             needsExclusiveLock = false;
132             locks = new ArrayList<>(typeLocks);
133             // give locks a consistent ordering
134             Collections.sort(locks, new Comparator<Class<? extends Component>>() {
135                 @Override
136                 public int compare(Class<? extends Component> o1,
137                                    Class<? extends Component> o2) {
138                     return o1.getName().compareTo(o2.getName());
139                 }
140             });
141         }
142     }
143 
144     /**
145      * @return The designated name of this job
146      */
147     public String getName() {
148         return name;
149     }
150 
151     /**
152      * @return The Scheduler that created this job
153      */
154     public Scheduler getScheduler() {
155         return scheduler;
156     }
157 
158     /**
159      * <p/>
160      * Invoke all tasks in this job. This method is thread-safe and will use its owning
161      * scheduler to coordinate the locks necessary to safely execute its tasks.
162      * <p/>
163      * Although {@link Scheduler} has convenience methods to repeatedly invoke a job, this
164      * method can be called directly if a more controlled job execution scheme is
165      * required.
166      */
167     @Override
168     public void run() {
169         // repeatedly run jobs until no task produces a post-process task
170         Job toInvoke = this;
171         while (toInvoke != null) {
172             toInvoke = toInvoke.runJob();
173         }
174     }
175 
176     private Job runJob() {
177         // acquire locks (either exclusive or per type in order)
178         if (needsExclusiveLock) {
179             scheduler.getEntitySystemLock().writeLock().lock();
180         } else {
181             scheduler.getEntitySystemLock().readLock().lock();
182             for (int i = 0; i < locks.size(); i++) {
183                 scheduler.getTypeLock(locks.get(i)).lock();
184             }
185         }
186 
187         try {
188             // reset all tasks and the job
189             taskIndex = 0;
190             singletonResults.clear();
191             for (int i = 0; i < tasks.length; i++) {
192                 tasks[i].reset(scheduler.getEntitySystem());
193             }
194 
195             // process all tasks and collect all returned tasks, in order
196             List<Task> postProcess = new ArrayList<>();
197             for (int i = 0; i < tasks.length; i++) {
198                 taskIndex = i;
199                 Task after = tasks[i].process(scheduler.getEntitySystem(), this);
200                 if (after != null) {
201                     postProcess.add(after);
202                 }
203             }
204 
205             // set this to negative so that report() can fail now that
206             // we're not executing tasks anymore
207             taskIndex = -1;
208 
209             if (postProcess.isEmpty()) {
210                 // nothing to process afterwards
211                 return null;
212             } else {
213                 Task[] tasks = postProcess.toArray(new Task[postProcess.size()]);
214                 return new Job(name + "-postprocess", scheduler, tasks);
215             }
216         } finally {
217             // unlock
218             if (needsExclusiveLock) {
219                 scheduler.getEntitySystemLock().writeLock().unlock();
220             } else {
221                 for (int i = locks.size() - 1; i >= 0; i--) {
222                     scheduler.getTypeLock(locks.get(i)).unlock();
223                 }
224                 scheduler.getEntitySystemLock().readLock().unlock();
225             }
226         }
227     }
228 
229     /**
230      * Report the given result instance to all tasks yet to be executed by this job, that
231      * have declared a public method named 'report' that takes a Result sub-type that is
232      * compatible with <var>r</var>'s type.
233      *
234      * @param r The result to report
235      *
236      * @throws NullPointerException  if r is null
237      * @throws IllegalStateException if r is a singleton result whose type has already
238      *                               been reported by another task in this job, or if the
239      *                               job is not currently executing tasks
240      */
241     public void report(Result r) {
242         if (r == null) {
243             throw new NullPointerException("Cannot report null results");
244         }
245         if (taskIndex < 0) {
246             throw new IllegalStateException(
247                     "Can only be invoked by a task from within run()");
248         }
249 
250         if (r.isSingleton()) {
251             // make sure this is the first we've seen the result
252             if (!singletonResults.add(r.getClass())) {
253                 throw new IllegalStateException(
254                         "Singleton result of type: " + r.getClass() +
255                         " has already been reported during " + name + "'s execution");
256             }
257         }
258 
259         Class<?> type = r.getClass();
260         while (Result.class.isAssignableFrom(type)) {
261             // report to all methods that receive the type
262             List<ResultReporter> all = resultMethods.get(type);
263             if (all != null) {
264                 int ct = all.size();
265                 for (int i = 0; i < ct; i++) {
266                     // this will filter on the current task index to only report
267                     // results to future tasks
268                     all.get(i).report(r);
269                 }
270             }
271 
272             type = type.getSuperclass();
273         }
274     }
275 
276     @Override
277     public String toString() {
278         return "Job(" + name + ", # tasks=" + tasks.length + ")";
279     }
280 
281     private class ResultReporter {
282         private final Method reportMethod;
283         private final int taskIndex;
284 
285         public ResultReporter(Method reportMethod, int taskIndex) {
286             this.reportMethod = reportMethod;
287             this.taskIndex = taskIndex;
288         }
289 
290         public void report(Result r) {
291             try {
292                 if (taskIndex > Job.this.taskIndex) {
293                     reportMethod.invoke(Job.this.tasks[taskIndex], r);
294                 }
295             } catch (IllegalArgumentException | IllegalAccessException e) {
296                 // shouldn't happen, since we check the type before invoking
297                 throw new RuntimeException(e);
298             } catch (InvocationTargetException e) {
299                 throw new RuntimeException("Error reporting result", e.getCause());
300             }
301         }
302 
303         @SuppressWarnings("unchecked")
304         public Class<? extends Result> getResultType() {
305             return (Class<? extends Result>) reportMethod.getParameterTypes()[0];
306         }
307     }
308 }