001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003import static org.openstreetmap.josm.tools.I18n.tr;
004
005import java.text.MessageFormat;
006import java.util.ArrayList;
007import java.util.List;
008import java.util.function.Supplier;
009
010/**
011 * This class allows all components of JOSM to register reclaimable amounts to memory.
012 * <p>
013 * It can be used to hold imagery caches or other data that can be reconstructed form disk/web if required.
014 * <p>
015 * Reclaimable storage implementations may be added in the future.
016 *
017 * @author Michael Zangl
018 * @since 10588
019 */
020public class MemoryManager {
021    /**
022     * assumed minimum JOSM memory footprint
023     */
024    private static final long JOSM_CORE_FOOTPRINT = 50L * 1024L * 1024L;
025
026    private static final MemoryManager INSTANCE = new MemoryManager();
027
028    private final ArrayList<MemoryHandle<?>> activeHandles = new ArrayList<>();
029
030    protected MemoryManager() {
031    }
032
033    /**
034     * Allocates a basic, fixed memory size.
035     * <p>
036     * If there is enough free memory, the factory is used to procude one element which is then returned as memory handle.
037     * <p>
038     * You should invoke {@link MemoryHandle#free()} if you do not need that handle any more.
039     * @param <T> The content type of the memory-
040     * @param name A name for the memory area. Only used for debugging.
041     * @param maxBytes The maximum amount of bytes the content may have
042     * @param factory The factory to use to procude the content if there is sufficient memory.
043     * @return A memory handle to the content.
044     * @throws NotEnoughMemoryException If there is not enough memory to allocate.
045     */
046    public synchronized <T> MemoryHandle<T> allocateMemory(String name, long maxBytes, Supplier<T> factory) throws NotEnoughMemoryException {
047        if (isAvailable(maxBytes)) {
048            T content = factory.get();
049            if (content == null) {
050                throw new IllegalArgumentException("Factory did not return a content element.");
051            }
052            Logging.info(MessageFormat.format("Allocate for {0}: {1} MB of memory. Available: {2} MB.",
053                    name, maxBytes / 1024 / 1024, getAvailableMemory() / 1024 / 1024));
054            MemoryHandle<T> handle = new ManualFreeMemoryHandle<>(name, content, maxBytes);
055            activeHandles.add(handle);
056            return handle;
057        } else {
058            throw new NotEnoughMemoryException(maxBytes);
059        }
060    }
061
062    /**
063     * Check if that memory is available
064     * @param maxBytes The memory to check for
065     * @return <code>true</code> if that memory is available.
066     */
067    public synchronized boolean isAvailable(long maxBytes) {
068        if (maxBytes < 0) {
069            throw new IllegalArgumentException(MessageFormat.format("Cannot allocate negative number of bytes: {0}", maxBytes));
070        }
071        return getAvailableMemory() >= maxBytes;
072    }
073
074    /**
075     * Gets the maximum amount of memory available for use in this manager.
076     * @return The maximum amount of memory.
077     */
078    public synchronized long getMaxMemory() {
079        return Runtime.getRuntime().maxMemory() - JOSM_CORE_FOOTPRINT;
080    }
081
082    /**
083     * Gets the memory that is considered free.
084     * @return The memory that can be used for new allocations.
085     */
086    public synchronized long getAvailableMemory() {
087        return getMaxMemory() - activeHandles.stream().mapToLong(MemoryHandle::getSize).sum();
088    }
089
090    /**
091     * Get the global memory manager instance.
092     * @return The memory manager.
093     */
094    public static MemoryManager getInstance() {
095        return INSTANCE;
096    }
097
098    /**
099     * Reset the state of this manager to the default state.
100     * @return true if there were entries that have been reset.
101     */
102    protected synchronized List<MemoryHandle<?>> resetState() {
103        ArrayList<MemoryHandle<?>> toFree = new ArrayList<>(activeHandles);
104        toFree.forEach(MemoryHandle::free);
105        return toFree;
106    }
107
108    /**
109     * A memory area managed by the {@link MemoryManager}.
110     * @author Michael Zangl
111     * @param <T> The content type.
112     */
113    public interface MemoryHandle<T> {
114
115        /**
116         * Gets the content of this memory area.
117         * <p>
118         * This method should be the prefered access to the memory since it will do error checking when {@link #free()} was called.
119         * @return The memory area content.
120         */
121        T get();
122
123        /**
124         * Get the size that was requested for this memory area.
125         * @return the size
126         */
127        long getSize();
128
129        /**
130         * Manually release this memory area. There should be no memory consumed by this afterwards.
131         */
132        void free();
133    }
134
135    private class ManualFreeMemoryHandle<T> implements MemoryHandle<T> {
136        private final String name;
137        private T content;
138        private final long size;
139
140        ManualFreeMemoryHandle(String name, T content, long size) {
141            this.name = name;
142            this.content = content;
143            this.size = size;
144        }
145
146        @Override
147        public T get() {
148            if (content == null) {
149                throw new IllegalStateException(MessageFormat.format("Memory area was accessed after free(): {0}", name));
150            }
151            return content;
152        }
153
154        @Override
155        public long getSize() {
156            return size;
157        }
158
159        @Override
160        public void free() {
161            if (content == null) {
162                throw new IllegalStateException(MessageFormat.format("Memory area was already marked as freed: {0}", name));
163            }
164            content = null;
165            synchronized (MemoryManager.this) {
166                activeHandles.remove(this);
167            }
168        }
169
170        @Override
171        public String toString() {
172            return "MemoryHandle [name=" + name + ", size=" + size + ']';
173        }
174    }
175
176    /**
177     * This exception is thrown if there is not enough memory for allocating the given object.
178     * @author Michael Zangl
179     */
180    public static class NotEnoughMemoryException extends Exception {
181        NotEnoughMemoryException(long memoryBytesRequired) {
182            super(tr("To add another layer you need to allocate at least {0,number,#}MB memory to JOSM using -Xmx{0,number,#}M "
183                            + "option (see http://forum.openstreetmap.org/viewtopic.php?id=25677).\n"
184                            + "Currently you have {1,number,#}MB memory allocated for JOSM",
185                            memoryBytesRequired / 1024 / 1024, Runtime.getRuntime().maxMemory() / 1024 / 1024));
186        }
187    }
188}