1 /* Copyright (C) 2005-2011 Fabio Riccardi */
2 
3 /*
4  * $RCSfile: SunTileCache.java,v $
5  *
6  * Copyright (c) 2005 Sun Microsystems, Inc. All rights reserved.
7  *
8  * Use is subject to license terms.
9  *
10  * $Revision: 1.1 $
11  * $Date: 2005/02/11 04:57:02 $
12  * $State: Exp $
13  */
14 package com.lightcrafts.jai.utils;
15 
16 import com.lightcrafts.media.jai.util.CacheDiagnostics;
17 import com.lightcrafts.media.jai.util.ImageUtil;
18 import com.lightcrafts.utils.cache.*;
19 import com.lightcrafts.utils.MemoryLimits;
20 import com.lightcrafts.utils.LCArrays;
21 import com.lightcrafts.jai.JAIContext;
22 
23 import java.awt.RenderingHints;
24 import java.util.*;
25 import java.util.prefs.Preferences;
26 import java.awt.Point;
27 import java.awt.image.*;
28 import java.io.*;
29 import java.lang.ref.ReferenceQueue;
30 import java.lang.ref.WeakReference;
31 import java.lang.ref.Reference;
32 import java.nio.ByteBuffer;
33 import com.lightcrafts.mediax.jai.EnumeratedParameter;
34 import com.lightcrafts.mediax.jai.TileCache;
35 import com.lightcrafts.mediax.jai.util.ImagingListener;
36 
37 /**
38  * This is Sun Microsystems' reference implementation of the
39  * <code>com.lightcrafts.mediax.jai.TileCache</code> interface.  It provides a
40  * central location for images to cache computed tiles, and is used as
41  * the default tile cache mechanism when no other tile cache objects
42  * are specified.
43  *
44  * <p> In this implementation, the cache size is limited by the memory
45  * capacity, which may be set at construction time or using the
46  * <code>setMemoryCapacity(long)</code> method.  The tile capacity
47  * is not used.  Different images may have very different tile sizes.
48  * Therefore, the memory usage for a specific tile capacity may vary
49  * greatly depends on the type of images involved.  In fact, the tile
50  * capacity is rather meaningless.
51  *
52  * @see com.lightcrafts.mediax.jai.TileCache
53  *
54  */
55 
56 public final class LCTileCache extends Observable
57                                 implements TileCache,
58         CacheDiagnostics {
59 
60     /** The default memory capacity of the cache (16 MB). */
61     private static final long DEFAULT_MEMORY_CAPACITY = 16L * 1024L * 1024L;
62 
63     /** The default hashtable capacity (heuristic) */
64     private static final int DEFAULT_HASHTABLE_CAPACITY = 1009; // prime number
65 
66     /** The hashtable load factor */
67     private static final float LOAD_FACTOR = 0.5F;
68 
69     /** Listener for the flush() method, to detect low memory situations. */
70     private static LCTileCacheListener Listener;
71 
72     /**
73      * The tile cache.
74      * A Hashtable is used to cache the tiles.  The "key" is a
75      * <code>Object</code> determined based on tile owner's UID if any or
76      * hashCode if the UID doesn't exist, and tile index.  The
77      * "value" is a LCCachedTile.
78      */
79     private Hashtable cache;
80 
81     /**
82      * Sorted (Tree) Set used with tile metrics.
83      * Adds another level of metrics used to determine
84      * which tiles are removed during memoryControl().
85      */
86     private SortedSet cacheSortedSet;
87 
88     /** The memory capacity of the cache. */
89     private long memoryCapacity;
90 
91     /** The amount of memory currently being used by the cache. */
92     private long memoryUsage = 0;
93 
94     /** The amount of memory to keep after memory control */
95     private float memoryThreshold = 0.75F;
96 
97     /** A indicator for tile access time. */
98     private long timeStamp = 0;
99 
100     /** Custom comparator used to determine tile cost or
101      *  priority ordering in the tile cache.
102      */
103     private Comparator comparator = null;
104 
105     /** Pointer to the first (newest) tile of the linked LCCachedTile list. */
106     private LCCachedTile first = null;
107 
108     /** Pointer to the last (oldest) tile of the linked LCCachedTile list. */
109     private LCCachedTile last = null;
110 
111     /** Tile count used for diagnostics */
112     private long tileCount = 0;
113 
114     /** Cache hit count */
115     private long hitCount = 0;
116 
117     /** Cache miss count */
118     private long missCount = 0;
119 
120     /** Diagnostics enable/disable */
121     private boolean diagnostics;
122 
123     private Cache m_objectCache;
124 
125     // diagnostic actions
126     // !!! If actions are changed in any way (removal, modification, addition)
127     // then the getCachedTileActions() method below should be changed to match.
128     private static final int ADD                 = 0;
129     private static final int REMOVE              = 1;
130     private static final int REMOVE_FROM_FLUSH   = 2;
131     private static final int REMOVE_FROM_MEMCON  = 3;
132     private static final int UPDATE_FROM_ADD     = 4;
133     private static final int UPDATE_FROM_GETTILE = 5;
134     private static final int ABOUT_TO_REMOVE     = 6;
135     private static final int REMOVE_FROM_GCEVENT = 7;
136 
137     /**
138      * Returns an array of <code>EnumeratedParameter</code>s corresponding
139      * to the numeric values returned by the <code>getAction()</code>
140      * method of the <code>CachedTile</code> implementation used by
141      * <code>SunTileCache</code>.  The "name" of each
142      * <code>EnumeratedParameter</code> provides a brief string
143      * describing the numeric action value.
144      */
getCachedTileActions()145     public static EnumeratedParameter[] getCachedTileActions() {
146         return new EnumeratedParameter[] {
147             new EnumeratedParameter("add", ADD),
148             new EnumeratedParameter("remove", REMOVE),
149             new EnumeratedParameter("remove_by_flush", REMOVE_FROM_FLUSH),
150             new EnumeratedParameter("remove_by_memorycontrol",
151                                     REMOVE_FROM_MEMCON),
152             new EnumeratedParameter("remove_by_gcevent",
153                                     REMOVE_FROM_GCEVENT),
154             new EnumeratedParameter("timestamp_update_by_add", UPDATE_FROM_ADD),
155             new EnumeratedParameter("timestamp_update_by_gettile",
156                                     UPDATE_FROM_GETTILE),
157             new EnumeratedParameter("preremove", ABOUT_TO_REMOVE)
158         };
159     }
160 
161     /** Get callbacks about calls to flush(), to detect low memory. */
setListener(LCTileCacheListener listener)162     public static synchronized void setListener(LCTileCacheListener listener) {
163         Listener = listener;
164     }
165 
166     /**
167      * No args constructor. Use the DEFAULT_MEMORY_CAPACITY of 16 Megs.
168      */
LCTileCache(boolean useDisk)169     public LCTileCache(boolean useDisk) {
170         this(DEFAULT_MEMORY_CAPACITY, useDisk);
171     }
172 
173     /**
174      * Constructor.  The memory capacity should be explicitly specified.
175      *
176      * @param memoryCapacity  The maximum cache memory size in bytes.
177      *
178      * @throws IllegalArgumentException  If <code>memoryCapacity</code>
179      *         is less than 0.
180      */
LCTileCache(long memoryCapacity, boolean useDisk)181     public LCTileCache(long memoryCapacity, boolean useDisk) {
182         if (memoryCapacity < 0) {
183             throw new IllegalArgumentException("memory capacity must be >= 0");
184         }
185 
186         this.memoryCapacity = memoryCapacity;
187 
188         // try to get a prime number (more efficient?)
189         // lower values of LOAD_FACTOR increase speed, decrease space efficiency
190         cache = new Hashtable(DEFAULT_HASHTABLE_CAPACITY, LOAD_FACTOR);
191 
192         if (useDisk) {
193             m_objectCache = createDiskCache();
194 
195             m_tileReaper = new TileReaper( this );
196             m_tileReaper.start();
197         }
198     }
199 
200 
201     /**
202      * Adds a tile to the cache.
203      *
204      * <p> If the specified tile is already in the cache, it will not be
205      * cached again.  If by adding this tile, the cache exceeds the memory
206      * capacity, older tiles in the cache are removed to keep the cache
207      * memory usage under the specified limit.
208      *
209      * @param owner            The image the tile blongs to.
210      * @param tileX            The tile's X index within the image.
211      * @param tileY            The tile's Y index within the image.
212      * @param tile             The tile to be cached.
213      */
add(RenderedImage owner, int tileX, int tileY, Raster tile)214     public void add(RenderedImage owner,
215                     int tileX,
216                     int tileY,
217                     Raster tile) {
218         add(owner, tileX, tileY, tile, null);
219     }
220 
221     /**
222      * Adds a tile to the cache with an associated tile compute cost.
223      *
224      * <p> If the specified tile is already in the cache, it will not be
225      * cached again.  If by adding this tile, the cache exceeds the memory
226      * capacity, older tiles in the cache are removed to keep the cache
227      * memory usage under the specified limit.
228      *
229      * @param owner            The image the tile blongs to.
230      * @param tileX            The tile's X index within the image.
231      * @param tileY            The tile's Y index within the image.
232      * @param tile             The tile to be cached.
233      * @param tileCacheMetric  Metric for prioritizing tiles
234      */
add(RenderedImage owner, int tileX, int tileY, Raster tile, Object tileCacheMetric)235     public synchronized void add(RenderedImage owner,
236                                  int tileX,
237                                  int tileY,
238                                  Raster tile,
239                                  Object tileCacheMetric) {
240 
241         if ( memoryCapacity == 0 ) {
242             return;
243         }
244 
245         // This tile is not in the cache; create a new LCCachedTile.
246         // else just update.
247         Object key = LCCachedTile.hashKey(owner, tileX, tileY);
248         LCCachedTile ct = (LCCachedTile) cache.get(key);
249 
250         if ( ct != null ) {
251             updateTileList(ct, UPDATE_FROM_ADD);
252         } else {
253             // create a new tile
254             ct = new LCCachedTile(owner, tileX, tileY, tile, tileCacheMetric);
255             ct.timeStamp = timeStamp++;
256             ct.previous = null;
257             ct.next = first;
258 
259             if (first == null && last == null) {
260                 first = ct;
261                 last  = ct;
262             } else {
263                 first.previous = ct;
264                 first = ct;        // put this tile at the top of the list
265             }
266 
267             // add to tile cache
268             if ( cache.put(ct.key, ct) == null ) {
269                 memoryUsage += ct.memorySize;
270                 tileCount++;
271                 //missCount++;  Not necessary?
272 
273                 if ( cacheSortedSet != null ) {
274                     cacheSortedSet.add(ct);
275                 }
276 
277                 if ( diagnostics ) {
278                     ct.action = ADD;
279                     setChanged();
280                     notifyObservers(ct);
281                 }
282             }
283 
284             // Bring memory usage down to memoryThreshold % of memory capacity.
285             if (memoryUsage > memoryCapacity) {
286                 memoryControl();
287             }
288         }
289 
290         if (m_tileReaper != null) {
291             // TODO: do we need this?
292             synchronized ( this ) {
293                 WeakReference weakKey = null;
294 
295                 Set keySet = m_imageMap.keySet();
296                 Iterator it = keySet.iterator();
297                 while (it.hasNext()) {
298                     WeakReference ref = (WeakReference) it.next();
299 
300                     if (ref.get() == owner) {
301                         weakKey = ref;
302                         break;
303                     }
304                 }
305 
306                 // weakKey = (WeakReference) m_weakRefMap.get(owner);
307 
308                 if (weakKey == null) {
309                     weakKey = new WeakReference( owner, m_tileReaper.getRefQ() );
310                     // m_weakRefMap.put(owner, weakKey);
311                 }
312 
313                 Set hashKeys = (HashSet) m_imageMap.get(weakKey);
314 
315                 if (hashKeys == null) {
316                     hashKeys = new HashSet();
317                     m_imageMap.put(weakKey, hashKeys);
318                 }
319 
320                 hashKeys.add(key);
321             }
322         }
323     }
324 
removeFromTileList(Object key, int action)325     private boolean removeFromTileList(Object key, int action) {
326         LCCachedTile ct = (LCCachedTile) cache.remove(key);
327 
328         if (ct != null) {
329             memoryUsage -= ct.memorySize;
330             tileCount--;
331 
332             if ( cacheSortedSet != null ) {
333                 cacheSortedSet.remove(ct);
334             }
335 
336             if ( ct == first ) {
337                 if ( ct == last ) {
338                     first = null;  // only one tile in the list
339                     last  = null;
340                 } else {
341                     first = ct.next;
342                     first.previous = null;
343                 }
344             } else if ( ct == last ) {
345                 last = ct.previous;
346                 last.next = null;
347             } else {
348                 ct.previous.next = ct.next;
349                 ct.next.previous = ct.previous;
350             }
351 
352             // Notify observers that a tile has been removed.
353             if ( diagnostics ) {
354                 ct.action = action;
355                 setChanged();
356                 notifyObservers(ct);
357             }
358 
359             ct.previous = null;
360             ct.next = null;
361             ct = null;
362 
363             return true;
364         }
365         return false;
366     }
367 
updateTileList(LCCachedTile ct, int action)368     private void updateTileList(LCCachedTile ct, int action) {
369         ct.timeStamp = timeStamp++;
370 
371         if (ct != first) {
372             // Bring this tile to the beginning of the list.
373             if (ct == last) {
374                 last = ct.previous;
375                 last.next = null;
376             } else {
377                 ct.previous.next = ct.next;
378                 ct.next.previous = ct.previous;
379             }
380 
381             ct.previous = null;
382             ct.next = first;
383 
384             first.previous = ct;
385             first = ct;
386         }
387 
388         hitCount++;
389 
390         if ( diagnostics ) {
391             ct.action = action;
392             setChanged();
393             notifyObservers(ct);
394         }
395     }
396 
397     /**
398      * Removes a tile from the cache.
399      *
400      * <p> If the specified tile is not in the cache, this method
401      * does nothing.
402      */
remove(RenderedImage owner, int tileX, int tileY)403     public synchronized void remove(RenderedImage owner,
404                                     int tileX,
405                                     int tileY) {
406 
407         if ( memoryCapacity == 0 ) {
408             return;
409         }
410 
411         Object key = LCCachedTile.hashKey(owner, tileX, tileY);
412         LCCachedTile ct = (LCCachedTile) cache.get(key);
413 
414         if ( ct != null ) {
415             // Notify observers that a tile is about to be removed.
416             // It is possible that the tile will be removed from the
417             // cache before the observers get notified.  This should
418             // be ok, since a hard reference to the tile will be
419             // kept for the observers, so the garbage collector won't
420             // remove the tile until the observers release it.
421             ct.action = ABOUT_TO_REMOVE;
422             setChanged();
423             notifyObservers(ct);
424 
425             removeFromTileList(key, REMOVE);
426         } else {
427             // if the tile is not in the memory cache than it might be on disk...
428             if (m_objectCache != null && m_objectCache.contains(key)) {
429                 m_objectCache.remove(key);
430                 tilesOnDisk--;
431             }
432         }
433     }
434 
435     /**
436      * Retrieves a tile from the cache.
437      *
438      * <p> If the specified tile is not in the cache, this method
439      * returns <code>null</code>.  If the specified tile is in the
440      * cache, its last-access time is updated.
441      *
442      * @param owner  The image the tile blongs to.
443      * @param tileX  The tile's X index within the image.
444      * @param tileY  The tile's Y index within the image.
445      */
getTile(RenderedImage owner, int tileX, int tileY)446     public synchronized Raster getTile(RenderedImage owner,
447                                        int tileX,
448                                        int tileY) {
449         if ( memoryCapacity == 0 )
450             return null;
451 
452         Object key = LCCachedTile.hashKey(owner, tileX, tileY);
453         LCCachedTile ct = (LCCachedTile) cache.get(key);
454 
455         if (m_objectCache != null && ct == null) {
456             Raster raster = readTileFromDisk(owner, tileX, tileY, key);
457             if (raster != null) {
458                 add(owner, tileX, tileY, raster, null);
459                 ct = (LCCachedTile) cache.get(key);
460                 assert ct != null;
461             }
462         }
463 
464         Raster tile = null;
465 
466         if ( ct == null ) {
467             missCount++;
468         } else {    // found tile in cache
469             tile = (Raster) ct.getTile();
470 
471             updateTileList(ct, UPDATE_FROM_GETTILE);
472         }
473 
474         return tile;
475     }
476 
477     /**
478      * Retrieves a contiguous array of all tiles in the cache which are
479      * owned by the specified image.  May be <code>null</code> if there
480      * were no tiles in the cache.  The array contains no null entries.
481      *
482      * @param owner The <code>RenderedImage</code> to which the tiles belong.
483      * @return An array of all tiles owned by the specified image or
484      *         <code>null</code> if there are none currently in the cache.
485      */
getTiles(RenderedImage owner)486     public synchronized Raster[] getTiles(RenderedImage owner) {
487         Raster[] tiles = null;
488 
489         if ( memoryCapacity == 0 ) {
490             return null;
491         }
492 
493         int size = Math.min(owner.getNumXTiles() * owner.getNumYTiles(),
494                             (int)tileCount);
495 
496         if ( size > 0 ) {
497             int minTx = owner.getMinTileX();
498             int minTy = owner.getMinTileY();
499             int maxTx = minTx + owner.getNumXTiles();
500             int maxTy = minTy + owner.getNumYTiles();
501 
502             // arbitrarily set a temporary vector size
503             Vector temp = new Vector(10, 20);
504 
505             for (int y = minTy; y < maxTy; y++) {
506                 for (int x = minTx; x < maxTx; x++) {
507                     Raster raster = getTile(owner, x, y);
508 
509                     if ( raster != null ) {
510                         temp.add(raster);
511                     }
512                 }
513             }
514 
515             int tmpsize = temp.size();
516             if ( tmpsize > 0 ) {
517                 tiles = (Raster[])temp.toArray(new Raster[tmpsize]);
518             }
519         }
520 
521         return tiles;
522     }
523 
524     /**
525      * Removes all the tiles that belong to a <code>RenderedImage</code>
526      * from the cache.
527      *
528      * @param owner  The image whose tiles are to be removed from the cache.
529      */
removeTiles(RenderedImage owner)530     public void removeTiles(RenderedImage owner) {
531         if ( memoryCapacity > 0 ) {
532             int minTx = owner.getMinTileX();
533             int minTy = owner.getMinTileY();
534             int maxTx = minTx + owner.getNumXTiles();
535             int maxTy = minTy + owner.getNumYTiles();
536 
537             for (int y=minTy; y<maxTy; y++) {
538                 for (int x=minTx; x<maxTx; x++) {
539                     remove(owner, x, y);
540                 }
541             }
542         }
543     }
544 
545     /**
546      * Adds an array of tiles to the tile cache.
547      *
548      * @param owner The <code>RenderedImage</code> that the tile belongs to.
549      * @param tileIndices An array of <code>Point</code>s containing the
550      *        <code>tileX</code> and <code>tileY</code> indices for each tile.
551      * @param tiles The array of tile <code>Raster</code>s containing tile data.
552      * @param tileCacheMetric Object which provides an ordering metric
553      *        associated with the <code>RenderedImage</code> owner.
554      * @since 1.1
555      */
addTiles(RenderedImage owner, Point[] tileIndices, Raster[] tiles, Object tileCacheMetric)556     public synchronized void addTiles(RenderedImage owner,
557                                       Point[] tileIndices,
558                                       Raster[] tiles,
559                                       Object tileCacheMetric) {
560 
561         if ( memoryCapacity == 0 ) {
562             return;
563         }
564 
565         for ( int i = 0; i < tileIndices.length; i++ ) {
566             int tileX = tileIndices[i].x;
567             int tileY = tileIndices[i].y;
568             Raster tile = tiles[i];
569 
570             add(owner, tileX, tileY, tile, tileCacheMetric);
571         }
572     }
573 
574     /**
575      * Returns an array of tile <code>Raster</code>s from the cache.
576      * Any or all of the elements of the returned array may be <code>null</code>
577      * if the corresponding tile is not in the cache.
578      *
579      * @param owner The <code>RenderedImage</code> that the tile belongs to.
580      * @param tileIndices  An array of <code>Point</code>s containing the
581      *        <code>tileX</code> and <code>tileY</code> indices for each tile.
582      * @since 1.1
583      */
getTiles(RenderedImage owner, Point[] tileIndices)584     public synchronized Raster[] getTiles(RenderedImage owner, Point[] tileIndices) {
585 
586         if ( memoryCapacity == 0 ) {
587             return null;
588         }
589 
590         Raster[] tiles = new Raster[tileIndices.length];
591 
592         for ( int i = 0; i < tiles.length; i++ ) {
593             int tileX = tileIndices[i].x;
594             int tileY = tileIndices[i].y;
595 
596             tiles[i] = getTile(owner, tileX, tileY);
597         }
598 
599         return tiles;
600     }
601 
602     /** Removes -ALL- tiles from the cache. */
flush()603     public synchronized void flush() {
604         // Call the LCTileCacheListener, if one is defined.  This helps detect
605         // low memory conditions.
606         if (Listener != null) {
607             Listener.tileCacheFlushed();
608         }
609         // NOTE: we don't do flushing for disk caches, it wipes the persistent cache, rather spill half of the cache out
610         if (m_objectCache != null) {
611             System.err.println("flushing the cache");
612             float mt = memoryThreshold;
613             memoryThreshold = 0.1f;
614             memoryControl();
615             memoryThreshold = mt;
616             return;
617         }
618 
619         //
620         // It is necessary to clear all the elements
621         // from the old cache in order to remove dangling
622         // references, due to the linked list.  In other
623         // words, it is possible to reache the object
624         // through 2 paths so the object does not
625         // become weakly reachable until the reference
626         // to it in the hash map is null. It is not enough
627         // to just set the object to null.
628         //
629         Enumeration keys = cache.keys();    // all keys in Hashtable
630 
631         // reset counters before diagnostics
632         hitCount  = 0;
633         missCount = 0;
634 
635         while (keys.hasMoreElements()) {
636             Object key = keys.nextElement();
637 
638             removeFromTileList(key, REMOVE_FROM_FLUSH);
639         }
640 
641         if ( memoryCapacity > 0 ) {
642             cache = new Hashtable(DEFAULT_HASHTABLE_CAPACITY, LOAD_FACTOR);
643         }
644 
645         if ( cacheSortedSet != null ) {
646             cacheSortedSet.clear();
647             cacheSortedSet = Collections.synchronizedSortedSet( new TreeSet(comparator) );
648         }
649 
650         // force reset after diagnostics
651         tileCount   = 0;
652         timeStamp   = 0;
653         memoryUsage = 0;
654 
655         // no System.gc() here, it's too slow and may occur anyway.
656     }
657 
658     /**
659      * Returns the cache's tile capacity.
660      *
661      * <p> This implementation of <code>TileCache</code> does not use
662      * the tile capacity.  This method always returns 0.
663      */
getTileCapacity()664     public int getTileCapacity() { return 0; }
665 
666     /**
667      * Sets the cache's tile capacity to the desired number of tiles.
668      *
669      * <p> This implementation of <code>TileCache</code> does not use
670      * the tile capacity.  The cache size is limited by the memory
671      * capacity only.  This method does nothing and has no effect on
672      * the cache.
673      *
674      * @param tileCapacity  The desired tile capacity for this cache
675      *        in number of tiles.
676      */
setTileCapacity(int tileCapacity)677     public void setTileCapacity(int tileCapacity) { }
678 
679     /** Returns the cache's memory capacity in bytes. */
getMemoryCapacity()680     public long getMemoryCapacity() {
681         return memoryCapacity;
682     }
683 
684     /**
685      * Sets the cache's memory capacity to the desired number of bytes.
686      * If the new memory capacity is smaller than the amount of memory
687      * currently being used by this cache, tiles are removed from the
688      * cache until the memory usage is less than the specified memory
689      * capacity.
690      *
691      * @param memoryCapacity  The desired memory capacity for this cache
692      *        in bytes.
693      *
694      * @throws IllegalArgumentException  If <code>memoryCapacity</code>
695      *         is less than 0.
696      */
setMemoryCapacity(long memoryCapacity)697     public void setMemoryCapacity(long memoryCapacity) {
698         if (memoryCapacity < 0) {
699             throw new IllegalArgumentException("memory capacity must be >= 0");
700         } else if ( memoryCapacity == 0 ) {
701             flush();
702         }
703 
704         this.memoryCapacity = memoryCapacity;
705 
706         if ( memoryUsage > memoryCapacity ) {
707             memoryControl();
708         }
709     }
710 
711     /** Enable Tile Monitoring and Diagnostics */
enableDiagnostics()712     public void enableDiagnostics() {
713         diagnostics = true;
714     }
715 
716     /** Turn off diagnostic notification */
disableDiagnostics()717     public void disableDiagnostics() {
718         diagnostics = false;
719     }
720 
getCacheTileCount()721     public long getCacheTileCount() {
722         return tileCount;
723     }
724 
getCacheMemoryUsed()725     public long getCacheMemoryUsed() {
726         return memoryUsage;
727     }
728 
getCacheHitCount()729     public long getCacheHitCount() {
730         return hitCount;
731     }
732 
getCacheMissCount()733     public long getCacheMissCount() {
734         return missCount;
735     }
736 
737     /**
738      * Reset hit and miss counters.
739      *
740      * @since 1.1
741      */
resetCounts()742     public void resetCounts() {
743         hitCount  = 0;
744         missCount = 0;
745     }
746 
747     /**
748      * Set the memory threshold value.
749      *
750      * @since 1.1
751      */
setMemoryThreshold(float mt)752     public void setMemoryThreshold(float mt) {
753         if ( mt < 0.0F || mt > 1.0F ) {
754             throw new IllegalArgumentException("memory threshold must be between 0 and 1");
755         } else {
756             memoryThreshold = mt;
757             memoryControl();
758         }
759     }
760 
761     /**
762      * Returns the current <code>memoryThreshold</code>.
763      *
764      * @since 1.1
765      */
getMemoryThreshold()766     public float getMemoryThreshold() {
767         return memoryThreshold;
768     }
769 
770     /** Returns a string representation of the class object. */
toString()771     public String toString() {
772         return getClass().getName() + "@" + Integer.toHexString(hashCode()) +
773                ": memoryCapacity = " + Long.toHexString(memoryCapacity) +
774                " memoryUsage = " + Long.toHexString(memoryUsage) +
775                " #tilesInCache = " + Integer.toString(cache.size());
776     }
777 
778     /** Returns the <code>Object</code> that represents the actual cache. */
getCachedObject()779     public Object getCachedObject() {
780         return cache;
781     }
782 
783     /**
784      * Removes tiles from the cache based on their last-access time
785      * (old to new) until the memory usage is memoryThreshold % of that of the
786      * memory capacity.
787      */
memoryControl()788     public synchronized void memoryControl() {
789         if ( cacheSortedSet == null ) {
790             standard_memory_control();
791         } else {
792             custom_memory_control();
793         }
794     }
795 
796     // time stamp based memory control (LRU)
standard_memory_control()797     private final void standard_memory_control() {
798         long limit = (long)(memoryCapacity * memoryThreshold);
799 
800         while( memoryUsage > limit && last != null ) {
801             LCCachedTile ct = (LCCachedTile) cache.get(last.key);
802 
803             if ( ct != null ) {
804                 RenderedImage owner = ct.getOwner();
805                 if (owner != null && owner.getProperty(JAIContext.PERSISTENT_CACHE_TAG) == Boolean.TRUE)
806                     if (m_objectCache != null)
807                         writeTileToDisk(ct, last.key);
808 
809                 removeFromTileList(last.key, REMOVE_FROM_MEMCON);
810             }
811         }
812     }
813 
814     private static class TileCacheCacheObjectBroker implements CacheObjectBroker {
815 
getEncodedSizeOf( Object obj )816         public int getEncodedSizeOf( Object obj ) {
817             if ( obj instanceof byte[] ) {
818                 final byte[] ba = (byte[])obj;
819                 return ba.length;
820             } else if ( obj instanceof short[] ) {
821                 final short[] sa = (short[])obj;
822                 return sa.length * 2;
823             } else if ( obj instanceof int[] ) {
824                 final int[] ia = (int[])obj;
825                 return ia.length * 4;
826             } else
827                 throw new IllegalArgumentException(
828                     "can't get size of " + obj.getClass()
829                 );
830         }
831 
832         // TODO: cache the ByteBuffer with a soft reference
833 
decodeFromByteBuffer( ByteBuffer buf, Object obj )834         public Object decodeFromByteBuffer( ByteBuffer buf, Object obj ) {
835             if ( obj instanceof byte[] )
836                 if ( buf.hasArray() )
837                     LCArrays.copy( buf.array(), 0, (byte[])obj, 0, buf.capacity() );
838                 else
839                     buf.get( (byte[])obj );
840             else if ( obj instanceof short[] )
841                 if ( buf.hasArray() )
842                     LCArrays.copy( buf.array(), 0, (short[])obj, 0, buf.capacity() );
843                 else
844                     buf.asShortBuffer().get( (short[])obj );
845             else if ( obj instanceof int[] )
846                 if ( buf.hasArray() )
847                     LCArrays.copy( buf.array(), 0, (int[])obj, 0, buf.capacity() );
848                 else
849                     buf.asIntBuffer().get( (int[])obj );
850             else
851                 throw new IllegalArgumentException(
852                     "can't decode " + obj.getClass()
853                 );
854             return obj;
855         }
856 
encodeToByteBuffer( ByteBuffer buf, Object obj )857         public void encodeToByteBuffer( ByteBuffer buf, Object obj ) {
858             if ( obj instanceof byte[] )
859                 if ( buf.hasArray() )
860                     LCArrays.copy( (byte[])obj, 0, buf.array(), 0, buf.capacity() );
861                 else
862                     buf.put( (byte[])obj );
863             else if ( obj instanceof short[] )
864                 if ( buf.hasArray() )
865                     LCArrays.copy( (short[])obj, 0, buf.array(), 0, buf.capacity() );
866                 else
867                     buf.asShortBuffer().put( (short[])obj );
868             else if ( obj instanceof int[] )
869                 if ( buf.hasArray() )
870                     LCArrays.copy( (int[])obj, 0, buf.array(), 0, buf.capacity() );
871                 else
872                     buf.asIntBuffer().put( (int[])obj );
873             else
874                 throw new IllegalArgumentException(
875                     "can't encode " + obj.getClass()
876                 );
877         }
878     }
879 
880     static class CacheFileFilter implements FilenameFilter {
881         File goodFile;
882 
CacheFileFilter(File goodFile)883         CacheFileFilter(File goodFile) {
884             this.goodFile = goodFile;
885         }
886 
accept(File dir, String name)887         public boolean accept(File dir, String name) {
888             if (!name.equals(goodFile.getName()) && name.startsWith("LCCacheFile") && name.endsWith(".cce"))
889                 return true;
890             return false;
891         }
892     }
893 
894     private final static Preferences Prefs =
895         Preferences.userNodeForPackage(LCTileCache.class);
896 
897     private final static String CacheDirKey = "ScratchDirectory";
898 
899     private File tmpFile = null;
900 
createDiskCache()901     private Cache createDiskCache() {
902         try {
903             // Try creating the temp file in the user-specified location
904             String path = Prefs.get(CacheDirKey, null);
905             tmpFile = null;
906             if (path != null) {
907                 File tmpDir = new File(path);
908                 if (tmpDir.isDirectory() && tmpDir.canWrite()) {
909                     tmpFile = File.createTempFile("LCCacheFile", ".cce", tmpDir);
910                 }
911             }
912             // Fall back to the regular java temp directory
913             if (tmpFile == null) {
914                 tmpFile = File.createTempFile("LCCacheFile", ".cce");
915             }
916             tmpFile.deleteOnExit();
917 
918             // Try to delete old cache files, checking if the file is locked by some other instance of ourself
919             File[] oldCacheFiles = tmpFile.getParentFile().listFiles(new CacheFileFilter(tmpFile));
920             if ( oldCacheFiles != null )
921                 for ( int i = 0; i < oldCacheFiles.length; i++ )
922                     oldCacheFiles[i].delete();
923 
924             int defaultMemorySize = MemoryLimits.getDefault();
925             Preferences prefs = Preferences.userRoot().node("/com/lightcrafts/app");
926             long maxMemory = (long) prefs.getInt("MaxMemory", defaultMemorySize) * 1024 * 1024;
927             long maxHeap = Runtime.getRuntime().maxMemory();
928             long extraCacheSize = Math.max( maxMemory - maxHeap, 0 );
929 
930             System.out.println("Allocating " + (extraCacheSize / (1024 * 1024)) + "MB for the image cache.");
931 
932             return new Cache(
933                 new TileCacheCacheObjectBroker(),
934                 extraCacheSize < 128 * 1024 * 1024 ?
935                     new WriteThroughCacheObjectMap() :
936                     new LRUCacheObjectMap(
937                         new NativeByteBufferAllocator( CHUNK_SIZE ), extraCacheSize
938                     ),
939                 new DirectFileCacheStore( tmpFile ),
940                 new CoalescingFreeBlockManager()
941             );
942         }
943         catch ( IOException e ) {
944             e.printStackTrace();
945             return null;
946         }
947     }
948 
949     // private static final long CACHE_SIZE = (long) (1024 * 1024 * 1024);
950     private static final int CHUNK_SIZE = 16 * 1024 * 1024;
951 
dispose()952     public synchronized void dispose() throws IOException {
953         m_objectCache.dispose();
954 
955         // Close and delete the old cache file
956         if (m_tileReaper != null)
957             m_tileReaper.kill();
958 
959         if (tmpFile != null)
960             tmpFile.delete();
961     }
962 
963     /**
964      * Finalize an <code>LCTileCache</code>.
965      */
finalize()966     protected void finalize() throws Throwable {
967         dispose();
968         super.finalize();
969     }
970 
971     private long tilesWritten = 0;
972     private long tilesRead = 0;
973     private long tilesOnDisk = 0;
974 
tilesWritten()975     public long tilesWritten() {
976         return tilesWritten;
977     }
978 
tilesRead()979     public long tilesRead() {
980         return tilesRead;
981     }
982 
tilesOnDisk()983     public long tilesOnDisk() {
984         return tilesOnDisk;
985     }
986 
readTileFromDisk(RenderedImage owner, int tileX, int tileY, Object key)987     private Raster readTileFromDisk(RenderedImage owner, int tileX, int tileY, Object key) {
988         if (m_objectCache.contains(key)) {
989             SampleModel sm = owner.getSampleModel();
990             DataBuffer db = sm.createDataBuffer();
991 
992             try {
993                 switch (db.getDataType()) {
994                     case DataBuffer.TYPE_BYTE:
995                         m_objectCache.getOnce(key, ((DataBufferByte) db).getData());
996                         break;
997 
998                     case DataBuffer.TYPE_USHORT:
999                         m_objectCache.getOnce(key, ((DataBufferUShort) db).getData());
1000                         break;
1001 
1002                     case DataBuffer.TYPE_INT:
1003                         m_objectCache.getOnce(key, ((DataBufferInt) db).getData());
1004                         break;
1005 
1006                     default:
1007                         throw new IllegalArgumentException("unsupported image type " + db.getClass());
1008                 }
1009                 synchronized (this) {
1010                     tilesOnDisk--;
1011                 }
1012             } catch (IOException e) {
1013                 e.printStackTrace();
1014             }
1015 
1016             WritableRaster raster;
1017 
1018             if (true)
1019                 raster = Raster.createWritableRaster(sm, db, new Point(tileX * owner.getTileWidth(),
1020                                                                        tileY * owner.getTileHeight()));
1021             else {
1022                 int bands = sm.getNumBands();
1023                 int bandOffsets[] = ((PixelInterleavedSampleModel) sm).getBandOffsets();
1024 
1025                 raster = Raster.createInterleavedRaster(db, owner.getTileWidth(), owner.getTileHeight(),
1026                                                         bands * owner.getTileWidth(), bands, bandOffsets,
1027                                                         new Point(tileX * owner.getTileWidth(),
1028                                                                   tileY * owner.getTileHeight()));
1029             }
1030             synchronized (this) {
1031                 tilesRead++;
1032             }
1033             return raster;
1034         } else
1035             return null;
1036     }
1037 
writeTileToDisk(LCCachedTile ct, Object key)1038     private void writeTileToDisk(LCCachedTile ct, Object key) {
1039         Raster raster = ct.getTile();
1040         DataBuffer db = raster.getDataBuffer();
1041 
1042         try {
1043             switch (db.getDataType()) {
1044                 case DataBuffer.TYPE_BYTE:
1045                     m_objectCache.put(key, ((DataBufferByte) db).getData());
1046                     break;
1047 
1048                 case DataBuffer.TYPE_USHORT:
1049                     m_objectCache.put(key, ((DataBufferUShort) db).getData());
1050                     break;
1051 
1052                 case DataBuffer.TYPE_INT:
1053                     m_objectCache.put(key, ((DataBufferInt) db).getData());
1054                     break;
1055 
1056                 default:
1057                     throw new IllegalArgumentException("unsupported image type " + db.getClass());
1058             }
1059             synchronized (this) {
1060                 tilesOnDisk++;
1061             }
1062         } catch (IOException e) {
1063             e.printStackTrace();
1064         }
1065 
1066         synchronized (this) {
1067             tilesWritten++;
1068         }
1069     }
1070 
1071     // comparator based memory control (TreeSet)
custom_memory_control()1072     private final void custom_memory_control() {
1073         long limit = (long)(memoryCapacity * memoryThreshold);
1074         Iterator iter = cacheSortedSet.iterator();
1075         LCCachedTile ct;
1076 
1077         while( iter.hasNext() && (memoryUsage > limit) ) {
1078             ct = (LCCachedTile) iter.next();
1079 
1080             memoryUsage -= ct.memorySize;
1081             synchronized (this) {
1082                 tileCount--;
1083             }
1084 
1085             // remove from sorted set
1086             try {
1087                 iter.remove();
1088             } catch(ConcurrentModificationException e) {
1089                 ImagingListener listener =
1090                     ImageUtil.getImagingListener((RenderingHints)null);
1091                 listener.errorOccurred("something wrong with the TileCache",
1092                                        e, this, false);
1093 //                e.printStackTrace();
1094             }
1095 
1096             // remove tile from the linked list
1097             if ( ct == first ) {
1098                 if ( ct == last ) {
1099                     first = null;
1100                     last  = null;
1101                 } else {
1102                     first = ct.next;
1103 
1104                     if ( first != null ) {
1105                         first.previous = null;
1106                         first.next = ct.next.next;
1107                     }
1108                 }
1109             } else if ( ct == last ) {
1110                 last = ct.previous;
1111 
1112                 if ( last != null ) {
1113                     last.next = null;
1114                     last.previous = ct.previous.previous;
1115                 }
1116             } else {
1117                 LCCachedTile ptr = first.next;
1118 
1119                 while( ptr != null ) {
1120 
1121                     if ( ptr == ct ) {
1122                         if ( ptr.previous != null ) {
1123                             ptr.previous.next = ptr.next;
1124                         }
1125 
1126                         if ( ptr.next != null ) {
1127                             ptr.next.previous = ptr.previous;
1128                         }
1129 
1130                         break;
1131                     }
1132 
1133                     ptr = ptr.next;
1134                 }
1135             }
1136 
1137             // remove reference in the hashtable
1138             cache.remove(ct.key);
1139 
1140             // diagnostics
1141             if ( diagnostics ) {
1142                 ct.action = REMOVE_FROM_MEMCON;
1143                 setChanged();
1144                 notifyObservers(ct);
1145             }
1146         }
1147 
1148         // If the custom memory control didn't release sufficient
1149         // number of tiles to satisfy the memory limit, fallback
1150         // to the standard memory controller.
1151         if ( memoryUsage > limit ) {
1152             standard_memory_control();
1153         }
1154     }
1155 
1156     /**
1157      *  The <code>Comparator</code> is used to produce an
1158      *  ordered list of tiles based on a user defined
1159      *  compute cost or priority metric.  This determines
1160      *  which tiles are subject to "ordered" removal
1161      *  during a memory control operation.
1162      *
1163      *  @since 1.1
1164      */
setTileComparator(Comparator c)1165     public synchronized void setTileComparator(Comparator c) {
1166         if (comparator != null)
1167             throw new IllegalArgumentException("TileComparator not supported by LCTileCache");
1168 
1169         comparator = c;
1170 
1171         if ( comparator == null ) {
1172             // turn of comparator
1173             if ( cacheSortedSet != null ) {
1174                 cacheSortedSet.clear();
1175                 cacheSortedSet = null;
1176             }
1177         } else {
1178             // copy tiles from hashtable to sorted tree set
1179             cacheSortedSet = Collections.synchronizedSortedSet( new TreeSet(comparator) );
1180 
1181             Enumeration keys = cache.keys();
1182 
1183             while( keys.hasMoreElements() ) {
1184                 Object key = keys.nextElement();
1185                 Object ct = cache.get(key);
1186                 cacheSortedSet.add(ct);
1187             }
1188         }
1189     }
1190 
1191     /**
1192      * Return the current comparator
1193      *
1194      * @since 1.1
1195      */
getTileComparator()1196     public Comparator getTileComparator() {
1197         return comparator;
1198     }
1199 
1200     // test
dump()1201     public void dump() {
1202 
1203         System.out.println("first = " + first);
1204         System.out.println("last  = " + last);
1205 
1206         Iterator iter = cacheSortedSet.iterator();
1207         int k = 0;
1208 
1209         while( iter.hasNext() ) {
1210             LCCachedTile ct = (LCCachedTile) iter.next();
1211             System.out.println(k++);
1212             System.out.println(ct);
1213         }
1214     }
1215 
sendExceptionToListener(String message, Exception e)1216     void sendExceptionToListener(String message, Exception e) {
1217         ImagingListener listener =
1218             ImageUtil.getImagingListener((RenderingHints)null);
1219         listener.errorOccurred(message, e, this, false);
1220     }
1221 
1222     /**
1223      * A <code>TileReaper</code> is-a {@link Thread} that runs continuously and
1224      * asynchronously in the background waiting for {@link RenderedImage}s that
1225      * the Java garbage collector has determined are weakly reachable.  Once
1226      * that's the case, remove all of a {@link RenderedImage}'s associated
1227      * tiles from the disk cache.
1228      */
1229     private static final class TileReaper extends Thread {
1230 
1231         ////////// public /////////////////////////////////////////////////////
1232 
1233         /**
1234          * Run the thread: wait for a weakly reachable {@link RenderedImage} to
1235          * become available and remove all of its tiles from the disk cache
1236          * (if any).
1237          */
run()1238         public void run() {
1239             while ( !m_killed ) {
1240                 try {
1241                     final Reference weakKey = m_refQ.remove(); // Image to be garbage collected
1242 
1243                     final LCTileCache tileCache =
1244                         (LCTileCache) m_tileCacheRef.get();
1245 
1246                     if ( tileCache == null )
1247                         break;
1248 
1249                     synchronized ( tileCache ) {
1250                         // System.out.println( "Removing tiles from caches" );
1251 
1252                         Set hashKeys = (Set) tileCache.m_imageMap.remove(weakKey);
1253 
1254                         assert hashKeys != null;
1255 
1256                         for ( Iterator i = hashKeys.iterator(); i.hasNext(); ) {
1257                             Object o = i.next();
1258 
1259                             if (tileCache.removeFromTileList(o, REMOVE_FROM_GCEVENT)) {
1260                                 // System.out.println("removed entry from memory cache");
1261                             }
1262 
1263                             if (tileCache.m_objectCache.remove(o)) {
1264                                 synchronized (tileCache) {
1265                                     tileCache.tilesOnDisk--;
1266                                 }
1267                                 // System.out.println("removed entry from disk cache");
1268                             }
1269                         }
1270                     }
1271                 }
1272                 catch ( InterruptedException e ) {
1273                     // do nothing
1274                 }
1275             }
1276         }
1277 
1278         ////////// package ////////////////////////////////////////////////////
1279 
1280         /**
1281          * Construct a <code>TileReaper</code> and make it a daemon.
1282          */
TileReaper( LCTileCache tileCache )1283         TileReaper( LCTileCache tileCache ) {
1284             super("TileReaper");
1285             setDaemon( true );
1286             m_refQ = new ReferenceQueue();
1287             m_tileCacheRef = new WeakReference( tileCache );
1288         }
1289 
1290         /**
1291          * Gets the {@link ReferenceQueue} being used.
1292          *
1293          * @return Returns said {@link ReferenceQueue}.
1294          */
getRefQ()1295         ReferenceQueue getRefQ() {
1296             return m_refQ;
1297         }
1298 
1299         /**
1300          * Kill this thread.
1301          */
kill()1302         void kill() {
1303             m_killed = true;
1304             interrupt();
1305         }
1306 
1307         ////////// private ////////////////////////////////////////////////////
1308 
1309         /**
1310          * A flag to indicate whether this thread has been killed.
1311          */
1312         private boolean m_killed;
1313 
1314         /**
1315          * The {@link ReferenceQueue} wherein the Java garbage collector
1316          * deposits objects that have become weakly reachable.
1317          */
1318         private final ReferenceQueue m_refQ;
1319 
1320         /**
1321          * A {@link WeakReference} to the owning {@link LCTileCache}.
1322          * TODO: explain why this is needed instead of using an inner class.
1323          */
1324         private final WeakReference m_tileCacheRef;
1325     }
1326 
1327     /**
1328      * This is a set of {@link WeakReference}s to {@link RenderedImage}s.
1329      */
1330     private final Map m_imageMap = new HashMap();
1331 
1332     /**
1333      * The {@link TileReaper} associated with this  <code>LCTileCache</code>.
1334      */
1335     private TileReaper m_tileReaper;
1336 }
1337 /* vim:set et sw=4 ts=4: */
1338