1 |
/* |
2 |
* Written by Doug Lea and Bill Scherer with assistance from members |
3 |
* of JCP JSR-166 Expert Group and released to the public domain, as |
4 |
* explained at http://creativecommons.org/licenses/publicdomain |
5 |
*/ |
6 |
|
7 |
import java.util.*; |
8 |
import java.util.concurrent.*; |
9 |
import java.util.concurrent.atomic.*; |
10 |
import java.util.concurrent.locks.*; |
11 |
|
12 |
/** |
13 |
* A parallel Traveling Salesperson Problem (TSP) program based on a |
14 |
* genetic algorithm using an Exchanger. A population of chromosomes |
15 |
* is distributed among "pools". The chromosomes represent tours, and |
16 |
* their fitness is the total tour length. Each chromosome is |
17 |
* initialized as a random tour. A Task is associated with each pool. |
18 |
* Each task repeatedly does, for a fixed number of iterations |
19 |
* (generations): |
20 |
* |
21 |
* <ol> |
22 |
* <li> Select a breeder b from the pool |
23 |
* <li> Create a strand of its tour with a random starting point and length |
24 |
* <li> Offer the strand to the exchanger, receiving a strand from |
25 |
* another pool |
26 |
* <li> Combine b and the received strand using crossing function to |
27 |
* create new chromosome c. |
28 |
* <li> Replace a chromosome in the pool with c. |
29 |
* </ol> |
30 |
* |
31 |
* See below for more details. |
32 |
* <p> |
33 |
* |
34 |
*/ |
35 |
public class TSPExchangerTest { |
36 |
static final int DEFAULT_MAX_THREADS = |
37 |
(Runtime.getRuntime().availableProcessors() + 2); |
38 |
|
39 |
/** |
40 |
* The problem size. Each city is a random point. The goal is to |
41 |
* find a tour among them with smallest total Euclidean distance. |
42 |
*/ |
43 |
static final int DEFAULT_CITIES = 144; |
44 |
|
45 |
// Tuning parameters. |
46 |
|
47 |
/** |
48 |
* The number of chromosomes per pool. Must be a power of two. |
49 |
* |
50 |
* Smaller values lead to faster iterations but poorer quality |
51 |
* results |
52 |
*/ |
53 |
static final int DEFAULT_POOL_SIZE = 32; |
54 |
|
55 |
/** |
56 |
* The number of iterations per task. Convergence appears |
57 |
* to be roughly proportional to #cities-squared |
58 |
*/ |
59 |
static final int DEFAULT_GENERATIONS = DEFAULT_CITIES * DEFAULT_CITIES; |
60 |
|
61 |
/** |
62 |
* The number of pools. The total population is #pools * poolSize, |
63 |
* which should be roughly on the order of #cities-squared |
64 |
* |
65 |
* Smaller values lead to faster total runs but poorer quality |
66 |
* results |
67 |
*/ |
68 |
static final int DEFAULT_NPOOLS = DEFAULT_GENERATIONS / DEFAULT_POOL_SIZE; |
69 |
|
70 |
/** |
71 |
* The minimum length for a random chromosome strand. |
72 |
* Must be at least 1. |
73 |
*/ |
74 |
static final int MIN_STRAND_LENGTH = 3; |
75 |
|
76 |
/** |
77 |
* The probablility mask value for creating random strands, |
78 |
* that have lengths at least MIN_STRAND_LENGTH, and grow |
79 |
* with exposnential decay 2^(-(1/(RANDOM_STRAND_MASK + 1) |
80 |
* Must be 1 less than a power of two. |
81 |
*/ |
82 |
static final int RANDOM_STRAND_MASK = 7; |
83 |
|
84 |
/** |
85 |
* Probablility control for selecting breeders. |
86 |
* Breeders are selected starting at the best-fitness chromosome, |
87 |
* with exponentially decaying probablility |
88 |
* 1 / (poolSize >>> BREEDER_DECAY). |
89 |
* |
90 |
* Larger values usually cause faster convergence but poorer |
91 |
* quality results |
92 |
*/ |
93 |
static final int BREEDER_DECAY = 1; |
94 |
|
95 |
/** |
96 |
* Probablility control for selecting dyers. |
97 |
* Dyers are selected starting at the worst-fitness chromosome, |
98 |
* with exponentially decaying probablility |
99 |
* 1 / (poolSize >>> DYER_DECAY) |
100 |
* |
101 |
* Larger values usually cause faster convergence but poorer |
102 |
* quality results |
103 |
*/ |
104 |
static final int DYER_DECAY = 1; |
105 |
|
106 |
/** |
107 |
* The probability mask for a task to give up running and |
108 |
* resubmit itself. On each iteration, a task stops iterating |
109 |
* and resubmits itself with probability 1 / (RESUBMIT_MASK+1). |
110 |
* This avoids some tasks running to completion before others |
111 |
* even start when there are more pools than threads. |
112 |
* |
113 |
* Must be 1 less than a power of two. |
114 |
*/ |
115 |
static final int RESUBMIT_MASK = 63; |
116 |
|
117 |
static final boolean verbose = true; |
118 |
static final long SNAPSHOT_RATE = 10000; // in milliseconds |
119 |
|
120 |
/** |
121 |
* The set of cities. Created once per program run, to |
122 |
* make it easier to compare solutions across different runs. |
123 |
*/ |
124 |
static CitySet cities; |
125 |
|
126 |
public static void main(String[] args) throws Exception { |
127 |
int maxThreads = DEFAULT_MAX_THREADS; |
128 |
int nCities = DEFAULT_CITIES; |
129 |
int poolSize = DEFAULT_POOL_SIZE; |
130 |
int nGen = nCities * nCities; |
131 |
int nPools = nCities * nCities / poolSize; |
132 |
|
133 |
try { |
134 |
int argc = 0; |
135 |
while (argc < args.length) { |
136 |
String option = args[argc++]; |
137 |
if (option.equals("-c")) { |
138 |
nCities = Integer.parseInt(args[argc]); |
139 |
nGen = nCities * nCities; |
140 |
nPools = nCities * nCities / poolSize; |
141 |
} |
142 |
else if (option.equals("-p")) |
143 |
poolSize = Integer.parseInt(args[argc]); |
144 |
else if (option.equals("-g")) |
145 |
nGen = Integer.parseInt(args[argc]); |
146 |
else if (option.equals("-n")) |
147 |
nPools = Integer.parseInt(args[argc]); |
148 |
else |
149 |
maxThreads = Integer.parseInt(option); |
150 |
argc++; |
151 |
} |
152 |
} |
153 |
catch (Exception e) { |
154 |
reportUsageErrorAndDie(); |
155 |
} |
156 |
|
157 |
System.out.print("TSPExchangerTest"); |
158 |
System.out.print(" -c " + nCities); |
159 |
System.out.print(" -g " + nGen); |
160 |
System.out.print(" -p " + poolSize); |
161 |
System.out.print(" -n " + nPools); |
162 |
System.out.print(" max threads " + maxThreads); |
163 |
System.out.println(); |
164 |
|
165 |
cities = new CitySet(nCities); |
166 |
|
167 |
for (int i = 2; i <= maxThreads; i += 2) |
168 |
oneRun(i, nPools, poolSize, nGen); |
169 |
} |
170 |
|
171 |
static void reportUsageErrorAndDie() { |
172 |
System.out.print("usage: TSPExchangerTest"); |
173 |
System.out.print(" [-c #cities]"); |
174 |
System.out.print(" [-p #poolSize]"); |
175 |
System.out.print(" [-g #generations]"); |
176 |
System.out.print(" [-n #pools]"); |
177 |
System.out.print(" #threads]"); |
178 |
System.out.println(); |
179 |
System.exit(0); |
180 |
} |
181 |
|
182 |
/** |
183 |
* Perform one run with the given parameters. Each run completes |
184 |
* when all but one of the tasks has finished. The last remaining |
185 |
* task may have no one left to exchange with, so the pool is |
186 |
* abruptly terminated. |
187 |
*/ |
188 |
static void oneRun(int nThreads, int nPools, int poolSize, int nGen) |
189 |
throws InterruptedException { |
190 |
Population p = new Population(nThreads, nPools, poolSize, nGen); |
191 |
ProgressMonitor mon = null; |
192 |
if (verbose) { |
193 |
mon = new ProgressMonitor(p); |
194 |
mon.start(); |
195 |
} |
196 |
p.printSnapshot(0); |
197 |
long startTime = System.nanoTime(); |
198 |
p.start(); |
199 |
p.awaitTasks(); |
200 |
long stopTime = System.nanoTime(); |
201 |
if (mon != null) |
202 |
mon.interrupt(); |
203 |
p.shutdown(); |
204 |
Thread.sleep(100); |
205 |
|
206 |
long elapsed = stopTime - startTime; |
207 |
long rate = elapsed / (nPools * nGen); |
208 |
double secs = (double)elapsed / 1000000000.0; |
209 |
p.printSnapshot(secs); |
210 |
System.out.printf("%10d ns per transfer\n", rate); |
211 |
} |
212 |
|
213 |
|
214 |
/** |
215 |
* A Population creates the pools, tasks, and threads for a run |
216 |
* and has control methods to start, stop, and report progress. |
217 |
*/ |
218 |
static final class Population { |
219 |
final Task[] tasks; |
220 |
final Exchanger<Strand> exchanger; |
221 |
final ThreadPoolExecutor exec; |
222 |
final CountDownLatch done; |
223 |
final int nGen; |
224 |
final int poolSize; |
225 |
final int nThreads; |
226 |
|
227 |
Population(int nThreads, int nPools, int poolSize, int nGen) { |
228 |
this.nThreads = nThreads; |
229 |
this.nGen = nGen; |
230 |
this.poolSize = poolSize; |
231 |
this.exchanger = new Exchanger<Strand>(); |
232 |
this.done = new CountDownLatch(nPools-1); |
233 |
this.tasks = new Task[nPools]; |
234 |
for (int i = 0; i < nPools; i++) |
235 |
tasks[i] = new Task(this); |
236 |
BlockingQueue<Runnable> tq = |
237 |
new LinkedBlockingQueue<Runnable>(); |
238 |
this.exec = new ThreadPoolExecutor(nThreads, nThreads, |
239 |
0L, TimeUnit.MILLISECONDS, |
240 |
tq); |
241 |
exec.prestartAllCoreThreads(); |
242 |
} |
243 |
|
244 |
/** Start the tasks */ |
245 |
void start() { |
246 |
for (int i = 0; i < tasks.length; i++) |
247 |
exec.execute(tasks[i]); |
248 |
} |
249 |
|
250 |
/** Stop the tasks */ |
251 |
void shutdown() { |
252 |
exec.shutdownNow(); |
253 |
} |
254 |
|
255 |
/** Called by task upon terminations */ |
256 |
void taskDone() { |
257 |
done.countDown(); |
258 |
} |
259 |
|
260 |
/** Wait for (all but one) task to complete */ |
261 |
void awaitTasks() throws InterruptedException { |
262 |
done.await(); |
263 |
} |
264 |
|
265 |
/** |
266 |
* Called by a task to resubmit itself after completing |
267 |
* fewer than nGen iterations. |
268 |
*/ |
269 |
void resubmit(Task task) { |
270 |
exec.execute(task); |
271 |
} |
272 |
|
273 |
void printSnapshot(double secs) { |
274 |
int gens = 0; |
275 |
double best = Double.MAX_VALUE; |
276 |
double worst = 0; |
277 |
for (int k = 0; k < tasks.length; ++k) { |
278 |
gens += tasks[k].gen; |
279 |
Chromosome[] cs = tasks[k].chromosomes; |
280 |
float b = cs[0].fitness; |
281 |
if (b < best) |
282 |
best = b; |
283 |
float w = cs[cs.length-1].fitness; |
284 |
if (w > worst) |
285 |
worst = w; |
286 |
} |
287 |
int avegen = (done.getCount() == 0)? nGen : gens / tasks.length; |
288 |
System.out.printf("Time:%9.3f Best:%9.3f Worst:%9.3f Gen:%6d Threads:%4d\n", |
289 |
secs, best, worst, avegen, nThreads); |
290 |
} |
291 |
|
292 |
float averageFitness() { // currently unused |
293 |
float total = 0; |
294 |
int count = 0; |
295 |
for (int k = 0; k < tasks.length; ++k) { |
296 |
Chromosome[] cs = tasks[k].chromosomes; |
297 |
for (int i = 0; i < cs.length; i++) |
298 |
total += cs[i].fitness; |
299 |
count += cs.length; |
300 |
} |
301 |
return total/(float)count; |
302 |
} |
303 |
} |
304 |
|
305 |
/** |
306 |
* A Task updates its pool of chromosomes.. |
307 |
*/ |
308 |
static final class Task implements Runnable { |
309 |
/** The pool of chromosomes, kept in sorted order */ |
310 |
final Chromosome[] chromosomes; |
311 |
final Population pop; |
312 |
/** The common exchanger, same for all tasks */ |
313 |
final Exchanger<Strand> exchanger; |
314 |
/** The current strand being exchanged */ |
315 |
Strand strand; |
316 |
/** Bitset used in cross */ |
317 |
final int[] inTour; |
318 |
final RNG rng; |
319 |
final int poolSize; |
320 |
final int nGen; |
321 |
int gen; |
322 |
|
323 |
Task(Population pop) { |
324 |
this.pop = pop; |
325 |
this.nGen = pop.nGen; |
326 |
this.gen = 0; |
327 |
this.poolSize = pop.poolSize; |
328 |
this.exchanger = pop.exchanger; |
329 |
this.rng = new RNG(); |
330 |
int length = cities.length; |
331 |
this.strand = new Strand(length); |
332 |
this.inTour = new int[(length >>> 5) + 1]; |
333 |
this.chromosomes = new Chromosome[poolSize]; |
334 |
for (int j = 0; j < poolSize; ++j) |
335 |
chromosomes[j] = new Chromosome(length, rng); |
336 |
Arrays.sort(chromosomes); |
337 |
} |
338 |
|
339 |
/** |
340 |
* Run one or more update cycles. |
341 |
*/ |
342 |
public void run() { |
343 |
try { |
344 |
for (;;) { |
345 |
update(); |
346 |
if (++gen >= nGen) { |
347 |
pop.taskDone(); |
348 |
return; |
349 |
} |
350 |
if ((rng.next() & RESUBMIT_MASK) == 1) { |
351 |
pop.resubmit(this); |
352 |
return; |
353 |
} |
354 |
} |
355 |
} catch (InterruptedException ie) { |
356 |
pop.taskDone(); |
357 |
} |
358 |
} |
359 |
|
360 |
/** |
361 |
* Choose a breeder, exchange strand with another pool, and |
362 |
* cross them to create new chromosome to replace a chosen |
363 |
* dyer. |
364 |
*/ |
365 |
void update() throws InterruptedException { |
366 |
int b = chooseBreeder(); |
367 |
int d = chooseDyer(b); |
368 |
Chromosome breeder = chromosomes[b]; |
369 |
Chromosome child = chromosomes[d]; |
370 |
chooseStrand(breeder); |
371 |
strand = exchanger.exchange(strand); |
372 |
cross(breeder, child); |
373 |
fixOrder(child, d); |
374 |
} |
375 |
|
376 |
/** |
377 |
* Choose a breeder, with exponentially decreasing probability |
378 |
* starting at best. |
379 |
* @return index of selected breeder |
380 |
*/ |
381 |
int chooseBreeder() { |
382 |
int mask = (poolSize >>> BREEDER_DECAY) - 1; |
383 |
int b = 0; |
384 |
while ((rng.next() & mask) != mask) { |
385 |
if (++b >= poolSize) |
386 |
b = 0; |
387 |
} |
388 |
return b; |
389 |
} |
390 |
|
391 |
/** |
392 |
* Choose a chromosome that will be replaced, with |
393 |
* exponentially decreasing probablility starting at |
394 |
* worst, ignoring the excluded index |
395 |
* @param exclude index to ignore; use -1 to not exclude any |
396 |
* @return index of selected dyer |
397 |
*/ |
398 |
int chooseDyer(int exclude) { |
399 |
int mask = (poolSize >>> DYER_DECAY) - 1; |
400 |
int d = poolSize - 1; |
401 |
while (d == exclude || (rng.next() & mask) != mask) { |
402 |
if (--d < 0) |
403 |
d = poolSize - 1; |
404 |
} |
405 |
return d; |
406 |
} |
407 |
|
408 |
/** |
409 |
* Select a random strand of b's. |
410 |
* @param breeder the breeder |
411 |
*/ |
412 |
void chooseStrand(Chromosome breeder) { |
413 |
int[] bs = breeder.alleles; |
414 |
int length = bs.length; |
415 |
int strandLength = MIN_STRAND_LENGTH; |
416 |
while (strandLength < length && |
417 |
(rng.next() & RANDOM_STRAND_MASK) != RANDOM_STRAND_MASK) |
418 |
strandLength++; |
419 |
strand.strandLength = strandLength; |
420 |
int[] ss = strand.alleles; |
421 |
int k = (rng.next() & 0x7FFFFFFF) % length; |
422 |
for (int i = 0; i < strandLength; ++i) { |
423 |
ss[i] = bs[k]; |
424 |
if (++k >= length) k = 0; |
425 |
} |
426 |
} |
427 |
|
428 |
/** |
429 |
* Copy current strand to start of c's, and then append all |
430 |
* remaining b's that aren't in the strand. |
431 |
* @param breeder the breeder |
432 |
* @param child the child |
433 |
*/ |
434 |
void cross(Chromosome breeder, Chromosome child) { |
435 |
for (int k = 0; k < inTour.length; ++k) // clear bitset |
436 |
inTour[k] = 0; |
437 |
|
438 |
// Copy current strand to c |
439 |
int[] cs = child.alleles; |
440 |
int ssize = strand.strandLength; |
441 |
int[] ss = strand.alleles; |
442 |
int i; |
443 |
for (i = 0; i < ssize; ++i) { |
444 |
int x = ss[i]; |
445 |
cs[i] = x; |
446 |
inTour[x >>> 5] |= 1 << (x & 31); // record in bit set |
447 |
} |
448 |
|
449 |
// Find index of matching origin in b |
450 |
int first = cs[0]; |
451 |
int j = 0; |
452 |
int[] bs = breeder.alleles; |
453 |
while (bs[j] != first) |
454 |
++j; |
455 |
|
456 |
// Append remaining b's that aren't already in tour |
457 |
while (i < cs.length) { |
458 |
if (++j >= bs.length) j = 0; |
459 |
int x = bs[j]; |
460 |
if ((inTour[x >>> 5] & (1 << (x & 31))) == 0) |
461 |
cs[i++] = x; |
462 |
} |
463 |
|
464 |
} |
465 |
|
466 |
/** |
467 |
* Fix the sort order of a changed Chromosome c at position k |
468 |
* @param c the chromosome |
469 |
* @param k the index |
470 |
*/ |
471 |
void fixOrder(Chromosome c, int k) { |
472 |
Chromosome[] cs = chromosomes; |
473 |
float oldFitness = c.fitness; |
474 |
c.recalcFitness(); |
475 |
float newFitness = c.fitness; |
476 |
if (newFitness < oldFitness) { |
477 |
int j = k; |
478 |
int p = j - 1; |
479 |
while (p >= 0 && cs[p].fitness > newFitness) { |
480 |
cs[j] = cs[p]; |
481 |
j = p--; |
482 |
} |
483 |
cs[j] = c; |
484 |
} else if (newFitness > oldFitness) { |
485 |
int j = k; |
486 |
int n = j + 1; |
487 |
while (n < cs.length && cs[n].fitness < newFitness) { |
488 |
cs[j] = cs[n]; |
489 |
j = n++; |
490 |
} |
491 |
cs[j] = c; |
492 |
} |
493 |
} |
494 |
} |
495 |
|
496 |
/** |
497 |
* A Chromosome is a candidate TSP tour. |
498 |
*/ |
499 |
static final class Chromosome implements Comparable { |
500 |
/** Index of cities in tour order */ |
501 |
final int[] alleles; |
502 |
/** Total tour length */ |
503 |
float fitness; |
504 |
|
505 |
/** |
506 |
* Initialize to random tour |
507 |
*/ |
508 |
Chromosome(int length, RNG random) { |
509 |
alleles = new int[length]; |
510 |
for (int i = 0; i < length; i++) |
511 |
alleles[i] = i; |
512 |
for (int i = length - 1; i > 0; i--) { |
513 |
int idx = (random.next() & 0x7FFFFFFF) % alleles.length; |
514 |
int tmp = alleles[i]; |
515 |
alleles[i] = alleles[idx]; |
516 |
alleles[idx] = tmp; |
517 |
} |
518 |
recalcFitness(); |
519 |
} |
520 |
|
521 |
public int compareTo(Object x) { // to enable sorting |
522 |
float xf = ((Chromosome)x).fitness; |
523 |
float f = fitness; |
524 |
return ((f == xf)? 0 :((f < xf)? -1 : 1)); |
525 |
} |
526 |
|
527 |
void recalcFitness() { |
528 |
int[] a = alleles; |
529 |
int len = a.length; |
530 |
int p = a[0]; |
531 |
float f = cities.distanceBetween(a[len-1], p); |
532 |
for (int i = 1; i < len; i++) { |
533 |
int n = a[i]; |
534 |
f += cities.distanceBetween(p, n); |
535 |
p = n; |
536 |
} |
537 |
fitness = f; |
538 |
} |
539 |
|
540 |
void validate() { // Ensure that this is a valid tour. |
541 |
int len = alleles.length; |
542 |
boolean[] used = new boolean[len]; |
543 |
for (int i = 0; i < len; ++i) |
544 |
used[alleles[i]] = true; |
545 |
for (int i = 0; i < len; ++i) |
546 |
if (!used[i]) |
547 |
throw new Error("Bad tour"); |
548 |
} |
549 |
|
550 |
} |
551 |
|
552 |
/** |
553 |
* A Strand is a random sub-sequence of a Chromosome. Each task |
554 |
* creates only one strand, and then trades it with others, |
555 |
* refilling it on each iteration. |
556 |
*/ |
557 |
static final class Strand { |
558 |
final int[] alleles; |
559 |
int strandLength; |
560 |
Strand(int length) { alleles = new int[length]; } |
561 |
} |
562 |
|
563 |
/** |
564 |
* A collection of (x,y) points that represent cities. Distances |
565 |
* are scaled in [0,1) to simply checking results. The expected |
566 |
* optimal TSP for random points is believed to be around 0.76 * |
567 |
* sqrt(N). For papers discussing this, see |
568 |
* http://www.densis.fee.unicamp.br/~moscato/TSPBIB_home.html for |
569 |
*/ |
570 |
static final class CitySet { |
571 |
// Scale ints to doubles in [0,1) |
572 |
static final double PSCALE = (double)0x80000000L; |
573 |
|
574 |
final int length; |
575 |
final int[] xPts; |
576 |
final int[] yPts; |
577 |
final float[][] distances; |
578 |
|
579 |
CitySet(int n) { |
580 |
this.length = n; |
581 |
this.xPts = new int[n]; |
582 |
this.yPts = new int[n]; |
583 |
this.distances = new float[n][n]; |
584 |
|
585 |
RNG random = new RNG(); |
586 |
for (int i = 0; i < n; i++) { |
587 |
xPts[i] = (random.next() & 0x7FFFFFFF); |
588 |
yPts[i] = (random.next() & 0x7FFFFFFF); |
589 |
} |
590 |
|
591 |
for (int i = 0; i < n; i++) { |
592 |
for (int j = 0; j < n; j++) { |
593 |
double dX = (xPts[i] - xPts[j]) / PSCALE; |
594 |
double dY = (yPts[i] - yPts[j]) / PSCALE; |
595 |
distances[i][j] = (float)Math.hypot(dX, dY); |
596 |
} |
597 |
} |
598 |
} |
599 |
|
600 |
// Retrieve the cached distance between a pair of cities |
601 |
float distanceBetween(int i, int j) { |
602 |
return distances[i][j]; |
603 |
} |
604 |
} |
605 |
|
606 |
/** |
607 |
* Cheap XorShift random number generator |
608 |
*/ |
609 |
static final class RNG { |
610 |
/** Seed generator for XorShift RNGs */ |
611 |
static final Random seedGenerator = new Random(); |
612 |
|
613 |
int seed; |
614 |
RNG(int seed) { this.seed = seed; } |
615 |
RNG() { this.seed = seedGenerator.nextInt(); } |
616 |
|
617 |
int next() { |
618 |
int x = seed; |
619 |
x ^= x << 6; |
620 |
x ^= x >>> 21; |
621 |
x ^= x << 7; |
622 |
seed = x; |
623 |
return x; |
624 |
} |
625 |
} |
626 |
|
627 |
static final class ProgressMonitor extends Thread { |
628 |
final Population pop; |
629 |
ProgressMonitor(Population p) { pop = p; } |
630 |
public void run() { |
631 |
double time = 0; |
632 |
try { |
633 |
while (!Thread.interrupted()) { |
634 |
sleep(SNAPSHOT_RATE); |
635 |
time += SNAPSHOT_RATE; |
636 |
pop.printSnapshot(time / 1000.0); |
637 |
} |
638 |
} catch (InterruptedException ie) {} |
639 |
} |
640 |
} |
641 |
} |