Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
Job |
|
| 2.4482758620689653;2.448 | ||||
Job$PredictTask |
|
| 2.4482758620689653;2.448 |
1 | /** | |
2 | * Distribution License: | |
3 | * JSword is free software; you can redistribute it and/or modify it under | |
4 | * the terms of the GNU Lesser General Public License, version 2.1 or later | |
5 | * as published by the Free Software Foundation. This program is distributed | |
6 | * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even | |
7 | * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
8 | * See the GNU Lesser General Public License for more details. | |
9 | * | |
10 | * The License is available on the internet at: | |
11 | * http://www.gnu.org/copyleft/lgpl.html | |
12 | * or by writing to: | |
13 | * Free Software Foundation, Inc. | |
14 | * 59 Temple Place - Suite 330 | |
15 | * Boston, MA 02111-1307, USA | |
16 | * | |
17 | * © CrossWire Bible Society, 2005 - 2016 | |
18 | * | |
19 | */ | |
20 | package org.crosswire.common.progress; | |
21 | ||
22 | import java.io.IOException; | |
23 | import java.net.URI; | |
24 | import java.util.ArrayList; | |
25 | import java.util.HashMap; | |
26 | import java.util.List; | |
27 | import java.util.Map; | |
28 | import java.util.Timer; | |
29 | import java.util.TimerTask; | |
30 | ||
31 | import org.crosswire.common.util.NetUtil; | |
32 | import org.crosswire.common.util.PropertyMap; | |
33 | import org.crosswire.jsword.JSMsg; | |
34 | import org.slf4j.Logger; | |
35 | import org.slf4j.LoggerFactory; | |
36 | ||
37 | /** | |
38 | * A Generic method of keeping track of Threads and monitoring their progress. | |
39 | * | |
40 | * @see gnu.lgpl.License The GNU Lesser General Public License for details. | |
41 | * @author Joe Walker | |
42 | * @author DM Smith | |
43 | */ | |
44 | public final class Job implements Progress { | |
45 | /** | |
46 | * Create a new Job. This will automatically fire a workProgressed event to | |
47 | * all WorkListeners, with the work property of this job set to 0. | |
48 | * | |
49 | * @param jobID the job identifier | |
50 | * @param jobName | |
51 | * Short description of this job | |
52 | * @param worker | |
53 | * Optional thread to use in request to stop worker | |
54 | */ | |
55 | 0 | protected Job(String jobID, String jobName, Thread worker) { |
56 | 0 | this.jobName = jobName; |
57 | 0 | this.jobID = jobID; |
58 | 0 | this.workerThread = worker; |
59 | 0 | this.listeners = new ArrayList<WorkListener>(); |
60 | 0 | this.cancelable = workerThread != null; |
61 | 0 | this.jobMode = ProgressMode.PREDICTIVE; |
62 | 0 | } |
63 | ||
64 | /* (non-Javadoc) | |
65 | * @see org.crosswire.common.progress.Progress#beginJob(java.lang.String) | |
66 | */ | |
67 | public void beginJob(String sectionName) { | |
68 | 0 | beginJob(sectionName, 100); |
69 | 0 | } |
70 | ||
71 | /* (non-Javadoc) | |
72 | * @see org.crosswire.common.progress.Progress#beginJob(java.lang.String, int) | |
73 | */ | |
74 | public void beginJob(String sectionName, int totalWork) { | |
75 | 0 | if (this.finished) { |
76 | 0 | return; |
77 | } | |
78 | ||
79 | 0 | synchronized (this) { |
80 | 0 | finished = false; |
81 | 0 | currentSectionName = sectionName; |
82 | 0 | totalUnits = totalWork; |
83 | 0 | jobMode = totalUnits == 100 ? ProgressMode.PERCENT : ProgressMode.UNITS; |
84 | 0 | } |
85 | ||
86 | // Report that the Job has started. | |
87 | 0 | JobManager.fireWorkProgressed(this); |
88 | 0 | } |
89 | ||
90 | /* (non-Javadoc) | |
91 | * @see org.crosswire.common.progress.Progress#beginJob(java.lang.String, java.net.URI) | |
92 | */ | |
93 | public void beginJob(String sectionName, URI predictURI) { | |
94 | 0 | if (finished) { |
95 | 0 | return; |
96 | } | |
97 | ||
98 | 0 | synchronized (this) { |
99 | 0 | currentSectionName = sectionName; |
100 | 0 | predictionMapURI = predictURI; |
101 | 0 | jobMode = ProgressMode.PREDICTIVE; |
102 | 0 | startTime = System.currentTimeMillis(); |
103 | ||
104 | 0 | fakingTimer = new Timer(); |
105 | 0 | fakingTimer.schedule(new PredictTask(), 0, REPORTING_INTERVAL); |
106 | ||
107 | // Load currentPredictionMap. It's not a disaster if it doesn't load | |
108 | 0 | totalUnits = loadPredictions(); |
109 | ||
110 | // There were no prior predictions so punt. | |
111 | 0 | if (totalUnits == Progress.UNKNOWN) { |
112 | // if we have nothing to go on use our assumption | |
113 | 0 | totalUnits = EXTRA_TIME; |
114 | 0 | jobMode = ProgressMode.UNKNOWN; |
115 | } | |
116 | ||
117 | // And the predictions for next time | |
118 | 0 | nextPredictionMap = new HashMap<String, Integer>(); |
119 | 0 | } |
120 | ||
121 | // Report that the Job has started. | |
122 | 0 | JobManager.fireWorkProgressed(this); |
123 | 0 | } |
124 | ||
125 | /* (non-Javadoc) | |
126 | * @see org.crosswire.common.progress.Progress#getJobName() | |
127 | */ | |
128 | public synchronized String getJobName() { | |
129 | 0 | return jobName; |
130 | } | |
131 | ||
132 | /* (non-Javadoc) | |
133 | * @see org.crosswire.common.progress.Progress#getProgressMode() | |
134 | */ | |
135 | public ProgressMode getProgressMode() { | |
136 | 0 | return jobMode; |
137 | } | |
138 | ||
139 | /* (non-Javadoc) | |
140 | * @see org.crosswire.common.progress.Progress#getTotalWork() | |
141 | */ | |
142 | public synchronized int getTotalWork() { | |
143 | 0 | return totalUnits; |
144 | } | |
145 | ||
146 | /* (non-Javadoc) | |
147 | * @see org.crosswire.common.progress.Progress#setTotalWork(int) | |
148 | */ | |
149 | public void setTotalWork(int totalWork) { | |
150 | 0 | this.totalUnits = totalWork; |
151 | 0 | } |
152 | ||
153 | /* (non-Javadoc) | |
154 | * @see org.crosswire.common.progress.Progress#getWork() | |
155 | */ | |
156 | public int getWork() { | |
157 | 0 | return percent; |
158 | } | |
159 | ||
160 | /* (non-Javadoc) | |
161 | * @see org.crosswire.common.progress.Progress#setWork(int) | |
162 | */ | |
163 | public void setWork(int work) { | |
164 | 0 | setWorkDone(work); |
165 | 0 | } |
166 | ||
167 | /* (non-Javadoc) | |
168 | * @see org.crosswire.common.progress.Progress#getWorkDone() | |
169 | */ | |
170 | public int getWorkDone() { | |
171 | 0 | return workUnits; |
172 | } | |
173 | ||
174 | /* (non-Javadoc) | |
175 | * @see org.crosswire.common.progress.Progress#setWork(int) | |
176 | */ | |
177 | public void setWorkDone(int work) { | |
178 | 0 | if (finished) { |
179 | 0 | return; |
180 | } | |
181 | ||
182 | 0 | synchronized (this) { |
183 | 0 | if (workUnits == work) { |
184 | 0 | return; |
185 | } | |
186 | ||
187 | 0 | workUnits = work; |
188 | ||
189 | 0 | int oldPercent = percent; |
190 | 0 | percent = 100 * workUnits / totalUnits; |
191 | 0 | if (oldPercent == percent) { |
192 | 0 | return; |
193 | } | |
194 | 0 | } |
195 | ||
196 | 0 | JobManager.fireWorkProgressed(this); |
197 | 0 | } |
198 | ||
199 | /* (non-Javadoc) | |
200 | * @see org.crosswire.common.progress.Progress#incrementWorkDone(int) | |
201 | */ | |
202 | public void incrementWorkDone(int step) { | |
203 | 0 | if (finished) { |
204 | 0 | return; |
205 | } | |
206 | ||
207 | 0 | synchronized (this) { |
208 | 0 | workUnits += step; |
209 | ||
210 | 0 | int oldPercent = percent; |
211 | // use long in arithmetic to avoid integer overflow | |
212 | 0 | percent = (int) (100L * workUnits / totalUnits); |
213 | 0 | if (oldPercent == percent) { |
214 | 0 | return; |
215 | } | |
216 | 0 | } |
217 | ||
218 | 0 | JobManager.fireWorkProgressed(this); |
219 | 0 | } |
220 | ||
221 | /* (non-Javadoc) | |
222 | * @see org.crosswire.common.progress.Progress#getSectionName() | |
223 | */ | |
224 | public String getSectionName() { | |
225 | 0 | return currentSectionName; |
226 | } | |
227 | ||
228 | /* (non-Javadoc) | |
229 | * @see org.crosswire.common.progress.Progress#setSectionName(java.lang.String) | |
230 | */ | |
231 | public void setSectionName(String sectionName) { | |
232 | 0 | if (finished) { |
233 | 0 | return; |
234 | } | |
235 | ||
236 | 0 | boolean doUpdate = false; |
237 | 0 | synchronized (this) { |
238 | // If we are in some kind of predictive mode, then measure progress toward the expected end. | |
239 | 0 | if (jobMode == ProgressMode.PREDICTIVE || jobMode == ProgressMode.UNKNOWN) { |
240 | 0 | doUpdate = updateProgress(System.currentTimeMillis()); |
241 | ||
242 | // We are done with the current section and are starting another | |
243 | // So record the length of the last section | |
244 | 0 | if (nextPredictionMap != null) { |
245 | 0 | nextPredictionMap.put(currentSectionName, Integer.valueOf(workUnits)); |
246 | } | |
247 | } | |
248 | ||
249 | 0 | currentSectionName = sectionName; |
250 | 0 | } |
251 | ||
252 | // Don't automatically tell listeners that the label changed. | |
253 | // Only do so if it is time to do an update. | |
254 | 0 | if (doUpdate) { |
255 | 0 | JobManager.fireWorkProgressed(this); |
256 | } | |
257 | 0 | } |
258 | ||
259 | /* (non-Javadoc) | |
260 | * @see org.crosswire.common.progress.Progress#done() | |
261 | */ | |
262 | public void done() { | |
263 | // TRANSLATOR: This shows up in a progress bar when progress is finished. | |
264 | 0 | String sectionName = JSMsg.gettext("Done"); |
265 | ||
266 | 0 | synchronized (this) { |
267 | 0 | finished = true; |
268 | ||
269 | 0 | currentSectionName = sectionName; |
270 | ||
271 | // Turn off the timer | |
272 | 0 | if (fakingTimer != null) { |
273 | 0 | fakingTimer.cancel(); |
274 | 0 | fakingTimer = null; |
275 | } | |
276 | ||
277 | 0 | workUnits = totalUnits; |
278 | 0 | percent = 100; |
279 | ||
280 | 0 | if (nextPredictionMap != null) { |
281 | 0 | nextPredictionMap.put(currentSectionName, Integer.valueOf((int) (System.currentTimeMillis() - startTime))); |
282 | } | |
283 | 0 | } |
284 | ||
285 | // Report that the job is done. | |
286 | 0 | JobManager.fireWorkProgressed(this); |
287 | ||
288 | 0 | synchronized (this) { |
289 | 0 | if (predictionMapURI != null) { |
290 | 0 | savePredictions(); |
291 | } | |
292 | 0 | } |
293 | 0 | } |
294 | ||
295 | /* (non-Javadoc) | |
296 | * @see org.crosswire.common.progress.Progress#cancel() | |
297 | */ | |
298 | public void cancel() { | |
299 | 0 | if (!finished) { |
300 | 0 | ignoreTimings(); |
301 | 0 | done(); |
302 | 0 | if (workerThread != null) { |
303 | 0 | workerThread.interrupt(); |
304 | } | |
305 | } | |
306 | 0 | } |
307 | ||
308 | /* (non-Javadoc) | |
309 | * @see org.crosswire.common.progress.Progress#isFinished() | |
310 | */ | |
311 | public boolean isFinished() { | |
312 | 0 | return finished; |
313 | } | |
314 | ||
315 | /* (non-Javadoc) | |
316 | * @see org.crosswire.common.progress.Progress#isCancelable() | |
317 | */ | |
318 | public boolean isCancelable() { | |
319 | 0 | return cancelable; |
320 | } | |
321 | ||
322 | /* (non-Javadoc) | |
323 | * @see org.crosswire.common.progress.Progress#setCancelable(boolean) | |
324 | */ | |
325 | public void setCancelable(boolean newInterruptable) { | |
326 | 0 | if (workerThread == null || finished) { |
327 | 0 | return; |
328 | } | |
329 | 0 | cancelable = newInterruptable; |
330 | 0 | fireStateChanged(); |
331 | 0 | } |
332 | ||
333 | /** | |
334 | * Add a listener to the list | |
335 | * | |
336 | * @param li the interested listener | |
337 | */ | |
338 | public synchronized void addWorkListener(WorkListener li) { | |
339 | 0 | List<WorkListener> temp = new ArrayList<WorkListener>(); |
340 | 0 | temp.addAll(listeners); |
341 | ||
342 | 0 | if (!temp.contains(li)) { |
343 | 0 | temp.add(li); |
344 | 0 | listeners = temp; |
345 | } | |
346 | 0 | } |
347 | ||
348 | /** | |
349 | * Remote a listener from the list | |
350 | * | |
351 | * @param li the disinterested listener | |
352 | */ | |
353 | public synchronized void removeWorkListener(WorkListener li) { | |
354 | 0 | if (listeners.contains(li)) { |
355 | 0 | List<WorkListener> temp = new ArrayList<WorkListener>(); |
356 | 0 | temp.addAll(listeners); |
357 | 0 | temp.remove(li); |
358 | 0 | listeners = temp; |
359 | } | |
360 | 0 | } |
361 | ||
362 | protected void fireStateChanged() { | |
363 | 0 | final WorkEvent ev = new WorkEvent(this); |
364 | ||
365 | // we need to keep the synchronized section very small to avoid deadlock | |
366 | // certainly keep the event dispatch clear of the synchronized block or | |
367 | // there will be a deadlock | |
368 | 0 | final List<WorkListener> temp = new ArrayList<WorkListener>(); |
369 | 0 | synchronized (this) { |
370 | 0 | if (listeners != null) { |
371 | 0 | temp.addAll(listeners); |
372 | } | |
373 | 0 | } |
374 | ||
375 | // We ought only to tell listeners about jobs that are in our | |
376 | // list of jobs so we need to fire before delete. | |
377 | 0 | int count = temp.size(); |
378 | 0 | for (int i = 0; i < count; i++) { |
379 | 0 | temp.get(i).workStateChanged(ev); |
380 | } | |
381 | 0 | } |
382 | ||
383 | /** | |
384 | * Get estimated the percent progress | |
385 | * | |
386 | * @param now the current point in progress | |
387 | * @return true if there is an update to progress. | |
388 | */ | |
389 | protected synchronized boolean updateProgress(long now) { | |
390 | 0 | int oldPercent = percent; |
391 | 0 | workUnits = (int) (now - startTime); |
392 | ||
393 | // Are we taking more time than expected? | |
394 | // Then we are at 100% | |
395 | 0 | if (workUnits > totalUnits) { |
396 | 0 | workUnits = totalUnits; |
397 | 0 | percent = 100; |
398 | } else { | |
399 | 0 | percent = 100 * workUnits / totalUnits; |
400 | } | |
401 | 0 | return oldPercent != percent; |
402 | } | |
403 | ||
404 | /** | |
405 | * Load the predictive timings if any | |
406 | * | |
407 | * @return the length of progress | |
408 | */ | |
409 | private int loadPredictions() { | |
410 | 0 | int maxAge = UNKNOWN; |
411 | try { | |
412 | 0 | currentPredictionMap = new HashMap<String, Integer>(); |
413 | 0 | PropertyMap temp = NetUtil.loadProperties(predictionMapURI); |
414 | ||
415 | // Determine the predicted time from the current prediction map | |
416 | 0 | for (String title : temp.keySet()) { |
417 | 0 | String timestr = temp.get(title); |
418 | ||
419 | try { | |
420 | 0 | Integer time = Integer.valueOf(timestr); |
421 | 0 | currentPredictionMap.put(title, time); |
422 | ||
423 | // if this time is later than the latest | |
424 | 0 | int age = time.intValue(); |
425 | 0 | if (maxAge < age) { |
426 | 0 | maxAge = age; |
427 | } | |
428 | 0 | } catch (NumberFormatException ex) { |
429 | 0 | log.error("Time format error", ex); |
430 | 0 | } |
431 | 0 | } |
432 | 0 | } catch (IOException ex) { |
433 | 0 | log.debug("Failed to load prediction times - guessing"); |
434 | 0 | } |
435 | ||
436 | 0 | return maxAge; |
437 | } | |
438 | ||
439 | /** | |
440 | * Save the known timings to a properties file. | |
441 | */ | |
442 | private void savePredictions() { | |
443 | // Now we know the start and the end we can convert all times to | |
444 | // percents | |
445 | 0 | PropertyMap predictions = new PropertyMap(); |
446 | 0 | for (String sectionName : nextPredictionMap.keySet()) { |
447 | 0 | Integer age = nextPredictionMap.get(sectionName); |
448 | 0 | predictions.put(sectionName, age.toString()); |
449 | 0 | } |
450 | ||
451 | // And save. It's not a disaster if this goes wrong | |
452 | try { | |
453 | 0 | NetUtil.storeProperties(predictions, predictionMapURI, "Predicted Startup Times"); |
454 | 0 | } catch (IOException ex) { |
455 | 0 | log.error("Failed to save prediction times", ex); |
456 | 0 | } |
457 | 0 | } |
458 | ||
459 | /** | |
460 | * Typically called from in a catch block, this ensures that we don't save | |
461 | * the timing file because we have a messed up run. | |
462 | */ | |
463 | private synchronized void ignoreTimings() { | |
464 | 0 | predictionMapURI = null; |
465 | 0 | } |
466 | ||
467 | private static final int REPORTING_INTERVAL = 100; | |
468 | ||
469 | /** | |
470 | * The amount of extra time if the predicted time was off and more time is needed. | |
471 | */ | |
472 | private static final int EXTRA_TIME = 2 * REPORTING_INTERVAL; | |
473 | ||
474 | /** | |
475 | * The type of job being performed. This is used to simplify code. | |
476 | */ | |
477 | private ProgressMode jobMode; | |
478 | ||
479 | /** | |
480 | * Total amount of work to do. | |
481 | */ | |
482 | private int totalUnits; | |
483 | ||
484 | /** | |
485 | * Does this job allow interruptions? | |
486 | */ | |
487 | private boolean cancelable; | |
488 | ||
489 | /** | |
490 | * Have we just finished? | |
491 | */ | |
492 | private boolean finished; | |
493 | ||
494 | /** | |
495 | * The amount of work done against the total. | |
496 | */ | |
497 | private int workUnits; | |
498 | ||
499 | /** | |
500 | * The officially reported progress | |
501 | */ | |
502 | private int percent; | |
503 | ||
504 | /** | |
505 | * A short descriptive phrase | |
506 | */ | |
507 | private String jobName; | |
508 | ||
509 | private final String jobID; | |
510 | /** | |
511 | * Optional thread to monitor progress | |
512 | */ | |
513 | private Thread workerThread; | |
514 | ||
515 | /** | |
516 | * Description of what we are doing | |
517 | */ | |
518 | private String currentSectionName; | |
519 | ||
520 | /** | |
521 | * The URI to which we load and save timings | |
522 | */ | |
523 | private URI predictionMapURI; | |
524 | ||
525 | /** | |
526 | * The timings loaded from where they were saved after the last run | |
527 | */ | |
528 | private Map<String, Integer> currentPredictionMap; | |
529 | ||
530 | /** | |
531 | * The timings as measured this time | |
532 | */ | |
533 | private Map<String, Integer> nextPredictionMap; | |
534 | ||
535 | /** | |
536 | * When did this job start? Measured in milliseconds since beginning of epoch. | |
537 | */ | |
538 | private long startTime; | |
539 | ||
540 | /** | |
541 | * The timer that lets us post fake progress events. | |
542 | */ | |
543 | private Timer fakingTimer; | |
544 | ||
545 | /** | |
546 | * People that want to know about "cancelable" changes | |
547 | */ | |
548 | private List<WorkListener> listeners; | |
549 | ||
550 | /** | |
551 | * The Job ID associated with this job | |
552 | * @return the job ID | |
553 | */ | |
554 | public String getJobID() { | |
555 | 0 | return jobID; |
556 | } | |
557 | ||
558 | /** | |
559 | * So we can fake progress for Jobs that don't tell us how they are doing | |
560 | */ | |
561 | 0 | final class PredictTask extends TimerTask { |
562 | /* (non-Javadoc) | |
563 | * @see java.util.TimerTask#run() | |
564 | */ | |
565 | @Override | |
566 | public void run() { | |
567 | 0 | if (updateProgress(System.currentTimeMillis())) { |
568 | 0 | JobManager.fireWorkProgressed(Job.this); |
569 | } | |
570 | 0 | } |
571 | } | |
572 | ||
573 | /** | |
574 | * The log stream | |
575 | */ | |
576 | 0 | private static final Logger log = LoggerFactory.getLogger(Job.class); |
577 | } |