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