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 }