Ticket #13120: patch-memory-manager.patch
File patch-memory-manager.patch, 22.1 KB (added by , 8 years ago) |
---|
-
src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java index b04e819..4374aca 100644
a b import org.openstreetmap.josm.gui.progress.ProgressMonitor; 85 85 import org.openstreetmap.josm.gui.util.GuiHelper; 86 86 import org.openstreetmap.josm.io.WMSLayerImporter; 87 87 import org.openstreetmap.josm.tools.GBC; 88 import org.openstreetmap.josm.tools.MemoryManager; 89 import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle; 90 import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException; 88 91 89 92 /** 90 93 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS … … implements ImageObserver, TileLoaderListener, ZoomChangeListener { 644 647 if (tileSource == null) { 645 648 throw new IllegalArgumentException(tr("Failed to create tile source")); 646 649 } 647 checkLayerMemoryDoesNotExceedMaximum();648 650 // check if projection is supported 649 651 projectionChanged(null, Main.getProjection()); 650 652 initTileSource(this.tileSource); … … implements ImageObserver, TileLoaderListener, ZoomChangeListener { 653 655 654 656 @Override 655 657 protected LayerPainter createMapViewPainter(MapViewEvent event) { 656 return new CompatibilityModeLayerPainter() { 657 @Override 658 public void detachFromMapView(MapViewEvent event) { 659 event.getMapView().removeMouseListener(adapter); 660 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this); 661 super.detachFromMapView(event); 662 } 663 }; 658 return new TileSourcePainter(); 664 659 } 665 660 666 661 /** … … implements ImageObserver, TileLoaderListener, ZoomChangeListener { 684 679 } 685 680 } 686 681 687 @Override688 protected long estimateMemoryUsage() {689 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();690 }691 692 682 protected int estimateTileCacheSize() { 693 683 Dimension screenSize = GuiHelper.getMaximumScreenSize(); 694 684 int height = screenSize.height; … … implements ImageObserver, TileLoaderListener, ZoomChangeListener { 1823 1813 public File createAndOpenSaveFileChooser() { 1824 1814 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1825 1815 } 1816 1817 private class TileSourcePainter extends CompatibilityModeLayerPainter { 1818 /** 1819 * The memory handle that will hold our tile source. 1820 */ 1821 private MemoryHandle<?> memory; 1822 1823 @Override 1824 public void paint(MapViewGraphics graphics) { 1825 allocateCacheMemory(); 1826 if (memory != null) { 1827 super.paint(graphics); 1828 } 1829 } 1830 1831 private void allocateCacheMemory() { 1832 if (memory == null) { 1833 MemoryManager manager = MemoryManager.getInstance(); 1834 if (manager.isAvailable(getEstimatedCacheSize())) { 1835 try { 1836 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), () -> new Object()); 1837 } catch (NotEnoughMemoryException e) { 1838 Main.warn("Could not allocate tile source memory", e); 1839 } 1840 } 1841 } 1842 } 1843 1844 protected long getEstimatedCacheSize() { 1845 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize(); 1846 } 1847 1848 @Override 1849 public void detachFromMapView(MapViewEvent event) { 1850 event.getMapView().removeMouseListener(adapter); 1851 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this); 1852 super.detachFromMapView(event); 1853 if (memory != null) { 1854 memory.free(); 1855 } 1856 } 1857 } 1826 1858 } -
src/org/openstreetmap/josm/gui/layer/Layer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/Layer.java b/src/org/openstreetmap/josm/gui/layer/Layer.java index b4f5797..2bab2e4 100644
a b public abstract class Layer extends AbstractMapViewPaintable implements Destroya 158 158 * It is always called in the event dispatching thread. 159 159 * Note that Main.map is null as long as no layer has been added, so do 160 160 * not execute code in the constructor, that assumes Main.map.mapView is 161 * not null. Instead override this method.161 * not null. 162 162 * 163 * This implementation provides check, if JOSM will be able to use Layer. Layers 164 * using a lot of memory, which do know in advance, how much memory they use, should 165 * override {@link #estimateMemoryUsage() estimateMemoryUsage} method and give a hint. 166 * 167 * This allows for preemptive warning message for user, instead of failing later on 168 * 169 * Remember to call {@code super.hookUpMapView()} when overriding this method 163 * If you need to execute code when this layer is added to the map view, use 164 * {@link #attachToMapView(org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent)} 170 165 */ 171 166 public void hookUpMapView() { 172 checkLayerMemoryDoesNotExceedMaximum();173 }174 175 /**176 * Checks that the memory required for the layers is no greather than the max memory.177 */178 protected static void checkLayerMemoryDoesNotExceedMaximum() {179 // calculate total memory needed for all layers180 long memoryBytesRequired = 50L * 1024L * 1024L; // assumed minimum JOSM memory footprint181 for (Layer layer: Main.getLayerManager().getLayers()) {182 memoryBytesRequired += layer.estimateMemoryUsage();183 }184 if (memoryBytesRequired > Runtime.getRuntime().maxMemory()) {185 throw new IllegalArgumentException(186 tr("To add another layer you need to allocate at least {0,number,#}MB memory to JOSM using -Xmx{0,number,#}M "187 + "option (see http://forum.openstreetmap.org/viewtopic.php?id=25677).\n"188 + "Currently you have {1,number,#}MB memory allocated for JOSM",189 memoryBytesRequired / 1024 / 1024, Runtime.getRuntime().maxMemory() / 1024 / 1024));190 }191 167 } 192 168 193 169 /** … … public abstract class Layer extends AbstractMapViewPaintable implements Destroya 588 564 589 565 /** 590 566 * @return bytes that the tile will use. Needed for resource management 567 * @deprecated Not used any more. 591 568 */ 569 @Deprecated 592 570 protected long estimateMemoryUsage() { 593 571 return 0; 594 572 } -
new file src/org/openstreetmap/josm/tools/MemoryManager.java
diff --git a/src/org/openstreetmap/josm/tools/MemoryManager.java b/src/org/openstreetmap/josm/tools/MemoryManager.java new file mode 100644 index 0000000..0cd2e5d
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.tools; 3 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 import java.text.MessageFormat; 6 import java.util.ArrayList; 7 import java.util.List; 8 import java.util.function.Supplier; 9 10 import org.openstreetmap.josm.Main; 11 12 /** 13 * This class allows all components of JOSM to register reclaimable amounts to memory. 14 * <p> 15 * It can be used to hold imagery caches or other data that can be reconstructed form disk/web if required. 16 * <p> 17 * Reclaimable storage implementations may be added in the future. 18 * 19 * @author Michael Zangl 20 * @since xxx 21 */ 22 public class MemoryManager { 23 /** 24 * assumed minimum JOSM memory footprint 25 */ 26 private static final long JOSM_CORE_FOOTPRINT = 50L * 1024L * 1024L; 27 28 private static final MemoryManager INSTANCE = new MemoryManager(); 29 30 private ArrayList<MemoryHandle<?>> activeHandles = new ArrayList<>(); 31 32 protected MemoryManager() { 33 } 34 35 /** 36 * Allocates a basic, fixed memory size. 37 * <p> 38 * If there is enough free memory, the factory is used to procude one element which is then returned as memory handle. 39 * <p> 40 * You should invoke {@link MemoryHandle#free()} if you do not need that handle any more. 41 * @param <T> The content type of the memory- 42 * @param name A name for the memory area. Only used for debugging. 43 * @param maxBytes The maximum amount of bytes the content may have 44 * @param factory The factory to use to procude the content if there is sufficient memory. 45 * @return A memory handle to the content. 46 * @throws NotEnoughMemoryException If there is not enough memory to allocate. 47 */ 48 public synchronized <T> MemoryHandle<T> allocateMemory(String name, long maxBytes, Supplier<T> factory) throws NotEnoughMemoryException { 49 if (isAvailable(maxBytes)) { 50 T content = factory.get(); 51 if (content == null) { 52 throw new IllegalArgumentException("Factory did not return a content element."); 53 } 54 Main.info(MessageFormat.format("Allocate for {0}: {1} MB of memory. Available: {2} MB.", 55 name, maxBytes / 1024 / 1024, getAvailableMemory() / 1024 / 1024)); 56 MemoryHandle<T> handle = new ManualFreeMemoryHandle<>(name, content, maxBytes); 57 activeHandles.add(handle); 58 return handle; 59 } else { 60 throw new NotEnoughMemoryException(maxBytes); 61 } 62 } 63 64 /** 65 * Check if that memory is available 66 * @param maxBytes The memory to check for 67 * @return <code>true</code> if that memory is available. 68 */ 69 public synchronized boolean isAvailable(long maxBytes) { 70 if (maxBytes < 0) { 71 throw new IllegalArgumentException(MessageFormat.format("Cannot allocate negative number of bytes: {0]", maxBytes)); 72 } 73 return getAvailableMemory() >= maxBytes; 74 } 75 76 /** 77 * Gets the maximum amount of memory available for use in this manager. 78 * @return The maximum amount of memory. 79 */ 80 public synchronized long getMaxMemory() { 81 return Runtime.getRuntime().maxMemory() - JOSM_CORE_FOOTPRINT; 82 } 83 84 /** 85 * Gets the memory that is considered free. 86 * @return The memory that can be used for new allocations. 87 */ 88 public synchronized long getAvailableMemory() { 89 return getMaxMemory() - activeHandles.stream().mapToLong(h -> h.getSize()).sum(); 90 } 91 92 /** 93 * Get the global memory manager instance. 94 * @return The memory manager. 95 */ 96 public static MemoryManager getInstance() { 97 return INSTANCE; 98 } 99 100 /** 101 * Reset the state of this manager to the default state. 102 * @return true if there were entries that have been reset. 103 */ 104 protected synchronized List<MemoryHandle<?>> resetState() { 105 ArrayList<MemoryHandle<?>> toFree = new ArrayList<>(activeHandles); 106 toFree.stream().forEach(h -> h.free()); 107 return toFree; 108 } 109 110 /** 111 * A memory area managed by the {@link MemoryManager}. 112 * @author Michael Zangl 113 * @param <T> The content type. 114 * @since xxx 115 */ 116 public interface MemoryHandle<T> { 117 118 /** 119 * Gets the content of this memory area. 120 * <p> 121 * This method should be the prefered access to the memory since it will do error checking when {@link #free()} was called. 122 * @return The memory area content. 123 */ 124 public T get(); 125 126 /** 127 * Get the size that was requested for this memory area. 128 * @return the size 129 */ 130 public long getSize(); 131 132 /** 133 * Manually release this memory area. There should be no memory consumed by this afterwards. 134 */ 135 public void free(); 136 } 137 138 private class ManualFreeMemoryHandle<T> implements MemoryHandle<T> { 139 private final String name; 140 private T content; 141 private final long size; 142 143 ManualFreeMemoryHandle(String name, T content, long size) { 144 this.name = name; 145 this.content = content; 146 this.size = size; 147 } 148 149 @Override 150 public T get() { 151 if (content == null) { 152 throw new IllegalStateException(MessageFormat.format("Memory area was accessed after free(): {0}", name)); 153 } 154 return content; 155 } 156 157 @Override 158 public long getSize() { 159 return size; 160 } 161 162 @Override 163 public void free() { 164 if (content == null) { 165 throw new IllegalStateException(MessageFormat.format("Memory area was already marked as freed: {0}", name)); 166 } 167 content = null; 168 synchronized (MemoryManager.this) { 169 activeHandles.remove(this); 170 } 171 } 172 173 @Override 174 public String toString() { 175 return "MemoryHandle [name=" + name + ", size=" + size + "]"; 176 } 177 } 178 179 /** 180 * This exception is thrown if there is not enough memory for allocating the given object. 181 * @author Michael Zangl 182 * @since xxx 183 */ 184 public static class NotEnoughMemoryException extends Exception { 185 NotEnoughMemoryException(long memoryBytesRequired) { 186 super(tr("To add another layer you need to allocate at least {0,number,#}MB memory to JOSM using -Xmx{0,number,#}M " 187 + "option (see http://forum.openstreetmap.org/viewtopic.php?id=25677).\n" 188 + "Currently you have {1,number,#}MB memory allocated for JOSM", 189 memoryBytesRequired / 1024 / 1024, Runtime.getRuntime().maxMemory() / 1024 / 1024)); 190 } 191 } 192 } -
test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java
diff --git a/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java b/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java index 9fba4cd..cf4887e 100644
a b import org.openstreetmap.josm.io.OsmApi; 19 19 import org.openstreetmap.josm.io.OsmApiInitializationException; 20 20 import org.openstreetmap.josm.io.OsmTransferCanceledException; 21 21 import org.openstreetmap.josm.tools.I18n; 22 import org.openstreetmap.josm.tools.MemoryManagerTest; 22 23 import org.openstreetmap.josm.tools.date.DateUtils; 23 24 24 25 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; … … public class JOSMTestRules implements TestRule { 39 40 private String i18n = null; 40 41 private boolean platform; 41 42 private boolean useProjection; 43 private boolean allowMemoryManagerLeaks; 42 44 43 45 /** 44 46 * Disable the default timeout for this test. Use with care. … … public class JOSMTestRules implements TestRule { 133 135 return this; 134 136 } 135 137 138 /** 139 * Allow the memory manager to contain items after execution of the test cases. 140 * @return this instance, for easy chaining 141 */ 142 public JOSMTestRules memoryManagerLeaks() { 143 allowMemoryManagerLeaks = true; 144 return this; 145 } 146 136 147 @Override 137 148 public Statement apply(final Statement base, Description description) { 138 149 Statement statement = new Statement() { … … public class JOSMTestRules implements TestRule { 225 236 */ 226 237 @SuppressFBWarnings("DM_GC") 227 238 private void cleanUpFromJosmFixture() { 239 MemoryManagerTest.resetState(true); 228 240 Main.getLayerManager().resetState(); 229 241 Main.pref = null; 230 242 Main.platform = null; … … public class JOSMTestRules implements TestRule { 244 256 }); 245 257 // Remove all layers 246 258 Main.getLayerManager().resetState(); 259 MemoryManagerTest.resetState(allowMemoryManagerLeaks); 247 260 248 261 // TODO: Remove global listeners and other global state. 249 262 Main.pref = null; -
new file test/unit/org/openstreetmap/josm/tools/MemoryManagerTest.java
diff --git a/test/unit/org/openstreetmap/josm/tools/MemoryManagerTest.java b/test/unit/org/openstreetmap/josm/tools/MemoryManagerTest.java new file mode 100644 index 0000000..bcb0bde
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.tools; 3 4 import static org.junit.Assert.assertEquals; 5 import static org.junit.Assert.assertFalse; 6 import static org.junit.Assert.assertSame; 7 import static org.junit.Assert.assertTrue; 8 import static org.junit.Assert.fail; 9 10 import java.util.List; 11 12 import org.junit.Rule; 13 import org.junit.Test; 14 import org.openstreetmap.josm.testutils.JOSMTestRules; 15 import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle; 16 import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException; 17 18 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 19 20 /** 21 * Tests the {@link MemoryManager} class. 22 * @author Michael Zangl 23 * @since xxx 24 */ 25 public class MemoryManagerTest { 26 /** 27 * Base test environment 28 */ 29 @Rule 30 @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") 31 public JOSMTestRules test = new JOSMTestRules().memoryManagerLeaks(); 32 33 /** 34 * Test {@link MemoryManager#allocateMemory(String, long, java.util.function.Supplier)} 35 * @throws NotEnoughMemoryException 36 */ 37 @Test 38 public void testUseMemory() throws NotEnoughMemoryException { 39 MemoryManager manager = MemoryManager.getInstance(); 40 long available = manager.getAvailableMemory(); 41 assertTrue(available < Runtime.getRuntime().maxMemory()); 42 assertEquals(available, manager.getMaxMemory()); 43 44 Object o1 = new Object(); 45 MemoryHandle<Object> testMemory = manager.allocateMemory("test", 10, () -> o1); 46 assertEquals(available - 10, manager.getAvailableMemory()); 47 assertSame(o1, testMemory.get()); 48 assertEquals(10, testMemory.getSize()); 49 assertTrue(testMemory.toString().startsWith("MemoryHandle")); 50 51 manager.allocateMemory("test2", 10, () -> new Object()); 52 assertEquals(available - 20, manager.getAvailableMemory()); 53 54 testMemory.free(); 55 assertEquals(available - 10, manager.getAvailableMemory()); 56 } 57 58 /** 59 * Test that {@link MemoryHandle#get()} checks for use after free. 60 * @throws NotEnoughMemoryException 61 */ 62 @Test(expected = IllegalStateException.class) 63 public void testUseAfterFree() throws NotEnoughMemoryException { 64 MemoryManager manager = MemoryManager.getInstance(); 65 MemoryHandle<Object> testMemory = manager.allocateMemory("test", 10, () -> new Object()); 66 testMemory.free(); 67 testMemory.get(); 68 } 69 70 /** 71 * Test that {@link MemoryHandle#get()} checks for free after free. 72 * @throws NotEnoughMemoryException 73 */ 74 @Test(expected = IllegalStateException.class) 75 public void testFreeAfterFree() throws NotEnoughMemoryException { 76 MemoryManager manager = MemoryManager.getInstance(); 77 MemoryHandle<Object> testMemory = manager.allocateMemory("test", 10, () -> new Object()); 78 testMemory.free(); 79 testMemory.free(); 80 } 81 /** 82 * Test that too big allocations fail 83 * @throws NotEnoughMemoryException 84 */ 85 @Test(expected = NotEnoughMemoryException.class) 86 public void testAllocationFails() throws NotEnoughMemoryException { 87 MemoryManager manager = MemoryManager.getInstance(); 88 long available = manager.getAvailableMemory(); 89 90 manager.allocateMemory("test", available + 1, () -> { 91 fail("Should not reach"); 92 return null; 93 }); 94 } 95 96 /** 97 * Test that allocations with null object fail 98 * @throws NotEnoughMemoryException 99 */ 100 @Test(expected = IllegalArgumentException.class) 101 public void testSupplierFails() throws NotEnoughMemoryException { 102 MemoryManager manager = MemoryManager.getInstance(); 103 104 manager.allocateMemory("test", 1, () -> null); 105 } 106 107 /** 108 * Test {@link MemoryManager#isAvailable(long)} 109 */ 110 @Test 111 public void testIsAvailable() { 112 MemoryManager manager = MemoryManager.getInstance(); 113 assertTrue(manager.isAvailable(10)); 114 assertTrue(manager.isAvailable(100)); 115 assertTrue(manager.isAvailable(10)); 116 } 117 118 /** 119 * Test {@link MemoryManager#isAvailable(long)} for negative number 120 * @throws NotEnoughMemoryException 121 */ 122 @Test(expected = IllegalArgumentException.class) 123 public void testIsAvailableFails() throws NotEnoughMemoryException { 124 MemoryManager manager = MemoryManager.getInstance(); 125 126 manager.isAvailable(-10); 127 } 128 129 /** 130 * Test {@link MemoryManager#resetState()} 131 * @throws NotEnoughMemoryException 132 */ 133 @Test 134 public void testResetState() throws NotEnoughMemoryException { 135 MemoryManager manager = MemoryManager.getInstance(); 136 assertTrue(manager.resetState().isEmpty()); 137 138 manager.allocateMemory("test", 10, () -> new Object()); 139 manager.allocateMemory("test2", 10, () -> new Object()); 140 assertEquals(2, manager.resetState().size()); 141 142 assertTrue(manager.resetState().isEmpty()); 143 } 144 145 /** 146 * Test {@link MemoryManager#resetState()} 147 * @throws NotEnoughMemoryException 148 */ 149 @Test(expected = IllegalStateException.class) 150 public void testResetStateUseAfterFree() throws NotEnoughMemoryException { 151 MemoryManager manager = MemoryManager.getInstance(); 152 MemoryHandle<Object> testMemory = manager.allocateMemory("test", 10, () -> new Object()); 153 154 assertFalse(manager.resetState().isEmpty()); 155 testMemory.get(); 156 } 157 158 /** 159 * Reset the state of the memory manager 160 * @param allowMemoryManagerLeaks If this is set, no exception is thrown if there were leaking entries. 161 */ 162 public static void resetState(boolean allowMemoryManagerLeaks) { 163 List<MemoryHandle<?>> hadLeaks = MemoryManager.getInstance().resetState(); 164 if (!allowMemoryManagerLeaks) { 165 assertTrue("Memory manager had leaking memory: " + hadLeaks, hadLeaks.isEmpty()); 166 } 167 } 168 }