Ticket #13120: patch-memory-manager.patch

File patch-memory-manager.patch, 22.1 KB (added by michael2402, 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;  
    8585import org.openstreetmap.josm.gui.util.GuiHelper;
    8686import org.openstreetmap.josm.io.WMSLayerImporter;
    8787import org.openstreetmap.josm.tools.GBC;
     88import org.openstreetmap.josm.tools.MemoryManager;
     89import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;
     90import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;
    8891
    8992/**
    9093 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
    implements ImageObserver, TileLoaderListener, ZoomChangeListener {  
    644647            if (tileSource == null) {
    645648                throw new IllegalArgumentException(tr("Failed to create tile source"));
    646649            }
    647             checkLayerMemoryDoesNotExceedMaximum();
    648650            // check if projection is supported
    649651            projectionChanged(null, Main.getProjection());
    650652            initTileSource(this.tileSource);
    implements ImageObserver, TileLoaderListener, ZoomChangeListener {  
    653655
    654656    @Override
    655657    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();
    664659    }
    665660
    666661    /**
    implements ImageObserver, TileLoaderListener, ZoomChangeListener {  
    684679        }
    685680    }
    686681
    687     @Override
    688     protected long estimateMemoryUsage() {
    689         return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();
    690     }
    691 
    692682    protected int estimateTileCacheSize() {
    693683        Dimension screenSize = GuiHelper.getMaximumScreenSize();
    694684        int height = screenSize.height;
    implements ImageObserver, TileLoaderListener, ZoomChangeListener {  
    18231813    public File createAndOpenSaveFileChooser() {
    18241814        return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
    18251815    }
     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    }
    18261858}
  • 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  
    158158     * It is always called in the event dispatching thread.
    159159     * Note that Main.map is null as long as no layer has been added, so do
    160160     * not execute code in the constructor, that assumes Main.map.mapView is
    161      * not null. Instead override this method.
     161     * not null.
    162162     *
    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)}
    170165     */
    171166    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 layers
    180         long memoryBytesRequired = 50L * 1024L * 1024L; // assumed minimum JOSM memory footprint
    181         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         }
    191167    }
    192168
    193169    /**
    public abstract class Layer extends AbstractMapViewPaintable implements Destroya  
    588564
    589565    /**
    590566     * @return bytes that the tile will use. Needed for resource management
     567     * @deprecated Not used any more.
    591568     */
     569    @Deprecated
    592570    protected long estimateMemoryUsage() {
    593571        return 0;
    594572    }
  • 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.
     2package org.openstreetmap.josm.tools;
     3import static org.openstreetmap.josm.tools.I18n.tr;
     4
     5import java.text.MessageFormat;
     6import java.util.ArrayList;
     7import java.util.List;
     8import java.util.function.Supplier;
     9
     10import 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 */
     22public 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;  
    1919import org.openstreetmap.josm.io.OsmApiInitializationException;
    2020import org.openstreetmap.josm.io.OsmTransferCanceledException;
    2121import org.openstreetmap.josm.tools.I18n;
     22import org.openstreetmap.josm.tools.MemoryManagerTest;
    2223import org.openstreetmap.josm.tools.date.DateUtils;
    2324
    2425import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
    public class JOSMTestRules implements TestRule {  
    3940    private String i18n = null;
    4041    private boolean platform;
    4142    private boolean useProjection;
     43    private boolean allowMemoryManagerLeaks;
    4244
    4345    /**
    4446     * Disable the default timeout for this test. Use with care.
    public class JOSMTestRules implements TestRule {  
    133135        return this;
    134136    }
    135137
     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
    136147    @Override
    137148    public Statement apply(final Statement base, Description description) {
    138149        Statement statement = new Statement() {
    public class JOSMTestRules implements TestRule {  
    225236     */
    226237    @SuppressFBWarnings("DM_GC")
    227238    private void cleanUpFromJosmFixture() {
     239        MemoryManagerTest.resetState(true);
    228240        Main.getLayerManager().resetState();
    229241        Main.pref = null;
    230242        Main.platform = null;
    public class JOSMTestRules implements TestRule {  
    244256        });
    245257        // Remove all layers
    246258        Main.getLayerManager().resetState();
     259        MemoryManagerTest.resetState(allowMemoryManagerLeaks);
    247260
    248261        // TODO: Remove global listeners and other global state.
    249262        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.
     2package org.openstreetmap.josm.tools;
     3
     4import static org.junit.Assert.assertEquals;
     5import static org.junit.Assert.assertFalse;
     6import static org.junit.Assert.assertSame;
     7import static org.junit.Assert.assertTrue;
     8import static org.junit.Assert.fail;
     9
     10import java.util.List;
     11
     12import org.junit.Rule;
     13import org.junit.Test;
     14import org.openstreetmap.josm.testutils.JOSMTestRules;
     15import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;
     16import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;
     17
     18import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     19
     20/**
     21 * Tests the {@link MemoryManager} class.
     22 * @author Michael Zangl
     23 * @since xxx
     24 */
     25public 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}