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