All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.graphhopper.storage.MMapDataAccess Maven / Gradle / Ivy

Go to download

GraphHopper is a fast and memory efficient Java road routing engine working seamlessly with OpenStreetMap data.

There is a newer version: 10.0
Show newest version
/*
 *  Licensed to GraphHopper GmbH under one or more contributor
 *  license agreements. See the NOTICE file distributed with this work for
 *  additional information regarding copyright ownership.
 *
 *  GraphHopper GmbH licenses this file to you under the Apache License,
 *  Version 2.0 (the "License"); you may not use this file except in
 *  compliance with the License. You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.graphhopper.storage;

import com.graphhopper.util.Helper;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

/**
 * A DataAccess implementation using a memory-mapped file, i.e. a facility of the
 * operating system to access a file like an area of RAM.
 * 

* Java presents the mapped memory as a ByteBuffer, and ByteBuffer is not * thread-safe, which means that access to a ByteBuffer must be externally * synchronized. *

* This class itself is intended to be as thread-safe as other DataAccess * implementations are. *

* The exact behavior of memory-mapping is reported to be wildly platform-dependent. * * @author Peter Karich * @author Michael Zilske */ public final class MMapDataAccess extends AbstractDataAccess { private final boolean allowWrites; private RandomAccessFile raFile; private final List segments = new ArrayList<>(); MMapDataAccess(String name, String location, boolean allowWrites, int segmentSize) { super(name, location, segmentSize); this.allowWrites = allowWrites; } public static void cleanMappedByteBuffer(final ByteBuffer buffer) { // TODO avoid reflection on every call try { // >=JDK9 class sun.misc.Unsafe { void invokeCleaner(ByteBuffer buf) } final Class unsafeClass = Class.forName("sun.misc.Unsafe"); // fetch the unsafe instance and bind it to the virtual MethodHandle final Field f = unsafeClass.getDeclaredField("theUnsafe"); f.setAccessible(true); final Object theUnsafe = f.get(null); final Method method = unsafeClass.getDeclaredMethod("invokeCleaner", ByteBuffer.class); try { method.invoke(theUnsafe, buffer); } catch (Throwable t) { throw new RuntimeException(t); } } catch (Exception ex) { throw new RuntimeException("Unable to unmap the mapped buffer", ex); } } private void initRandomAccessFile() { if (raFile != null) return; try { // raFile necessary for loadExisting and create raFile = new RandomAccessFile(getFullName(), allowWrites ? "rw" : "r"); } catch (IOException ex) { throw new RuntimeException(ex); } } @Override public MMapDataAccess create(long bytes) { if (!segments.isEmpty()) { throw new IllegalThreadStateException("already created"); } initRandomAccessFile(); bytes = Math.max(10 * 4, bytes); ensureCapacity(bytes); return this; } @Override public boolean ensureCapacity(long bytes) { return mapIt(HEADER_OFFSET, bytes); } private boolean mapIt(long offset, long byteCount) { if (byteCount < 0) throw new IllegalArgumentException("new capacity has to be strictly positive"); if (byteCount <= getCapacity()) return false; long longSegmentSize = segmentSizeInBytes; int segmentsToMap = (int) (byteCount / longSegmentSize); if (segmentsToMap < 0) throw new IllegalStateException("Too many segments needs to be allocated. Increase segmentSize."); if (byteCount % longSegmentSize != 0) segmentsToMap++; if (segmentsToMap == 0) throw new IllegalStateException("0 segments are not allowed."); long bufferStart = offset; int newSegments; int i = 0; long newFileLength = offset + segmentsToMap * longSegmentSize; try { // ugly remapping // http://stackoverflow.com/q/14011919/194609 // This approach is probably problematic but a bit faster if done often. // Here we rely on the OS+file system that increasing the file // size has no effect on the old mappings! bufferStart += segments.size() * longSegmentSize; newSegments = segmentsToMap - segments.size(); // rely on automatically increasing when mapping // raFile.setLength(newFileLength); for (; i < newSegments; i++) { segments.add(newByteBuffer(bufferStart, longSegmentSize)); bufferStart += longSegmentSize; } return true; } catch (IOException ex) { // we could get an exception here if buffer is too small and area too large // e.g. I got an exception for the 65421th buffer (probably around 2**16 == 65536) throw new RuntimeException("Couldn't map buffer " + i + " of " + segmentsToMap + " with " + longSegmentSize + " for " + name + " at position " + bufferStart + " for " + byteCount + " bytes with offset " + offset + ", new fileLength:" + newFileLength + ", " + Helper.getMemInfo(), ex); } } private MappedByteBuffer newByteBuffer(long offset, long byteCount) throws IOException { // If we request a buffer larger than the file length, it will automatically increase the file length! // Will this cause problems? http://stackoverflow.com/q/14011919/194609 // For trimTo we need to reset the file length later to reduce that size MappedByteBuffer buf = null; IOException ioex = null; // One retry if it fails. It could fail e.g. if previously buffer wasn't yet unmapped from the jvm for (int trial = 0; trial < 1; ) { try { buf = raFile.getChannel().map( allowWrites ? FileChannel.MapMode.READ_WRITE : FileChannel.MapMode.READ_ONLY, offset, byteCount); break; } catch (IOException tmpex) { ioex = tmpex; trial++; try { // mini sleep to let JVM do unmapping Thread.sleep(5); } catch (InterruptedException iex) { throw new IOException(iex); } } } if (buf == null) { if (ioex == null) { throw new AssertionError("internal problem as the exception 'ioex' shouldn't be null"); } throw ioex; } buf.order(byteOrder); return buf; } @Override public boolean loadExisting() { if (segments.size() > 0) throw new IllegalStateException("already initialized"); if (isClosed()) throw new IllegalStateException("already closed"); File file = new File(getFullName()); if (!file.exists() || file.length() == 0) return false; initRandomAccessFile(); try { long byteCount = readHeader(raFile); if (byteCount < 0) return false; mapIt(HEADER_OFFSET, byteCount - HEADER_OFFSET); return true; } catch (IOException ex) { throw new RuntimeException("Problem while loading " + getFullName(), ex); } } @Override public void flush() { if (isClosed()) throw new IllegalStateException("already closed"); try { for (MappedByteBuffer bb : segments) { bb.force(); } writeHeader(raFile, raFile.length(), segmentSizeInBytes); // this could be necessary too // http://stackoverflow.com/q/14011398/194609 raFile.getFD().sync(); // equivalent to raFile.getChannel().force(true); } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Load memory mapped files into physical memory. */ public void load(int percentage) { if (percentage < 0 || percentage > 100) throw new IllegalArgumentException("Percentage for MMapDataAccess.load for " + getName() + " must be in [0,100] but was " + percentage); int max = Math.round(segments.size() * percentage / 100f); for (int i = 0; i < max; i++) { segments.get(i).load(); } } @Override public void close() { super.close(); clean(0, segments.size()); segments.clear(); Helper.close(raFile); } @Override public void setInt(long bytePos, int value) { int bufferIndex = (int) (bytePos >> segmentSizePower); int index = (int) (bytePos & indexDivisor); if (index + 4 > segmentSizeInBytes) throw new IllegalStateException("Padding required. Currently an int cannot be distributed over two segments. " + bytePos); ByteBuffer byteBuffer = segments.get(bufferIndex); byteBuffer.putInt(index, value); } @Override public int getInt(long bytePos) { int bufferIndex = (int) (bytePos >> segmentSizePower); int index = (int) (bytePos & indexDivisor); if (index + 4 > segmentSizeInBytes) throw new IllegalStateException("Padding required. Currently an int cannot be distributed over two segments. " + bytePos); ByteBuffer byteBuffer = segments.get(bufferIndex); return byteBuffer.getInt(index); } @Override public void setShort(long bytePos, short value) { int bufferIndex = (int) (bytePos >>> segmentSizePower); int index = (int) (bytePos & indexDivisor); ByteBuffer byteBuffer = segments.get(bufferIndex); if (index + 2 > segmentSizeInBytes) { ByteBuffer byteBufferNext = segments.get(bufferIndex + 1); // special case if short has to be written into two separate segments byteBuffer.put(index, (byte) value); byteBufferNext.put(0, (byte) (value >>> 8)); } else { byteBuffer.putShort(index, value); } } @Override public short getShort(long bytePos) { int bufferIndex = (int) (bytePos >>> segmentSizePower); int index = (int) (bytePos & indexDivisor); ByteBuffer byteBuffer = segments.get(bufferIndex); if (index + 2 > segmentSizeInBytes) { ByteBuffer byteBufferNext = segments.get(bufferIndex + 1); return (short) ((byteBufferNext.get(0) & 0xFF) << 8 | byteBuffer.get(index) & 0xFF); } return byteBuffer.getShort(index); } @Override public void setBytes(long bytePos, byte[] values, int length) { assert length <= segmentSizeInBytes : "the length has to be smaller or equal to the segment size: " + length + " vs. " + segmentSizeInBytes; final int bufferIndex = (int) (bytePos >>> segmentSizePower); final int index = (int) (bytePos & indexDivisor); final int delta = index + length - segmentSizeInBytes; final ByteBuffer bb1 = segments.get(bufferIndex); if (delta > 0) { length -= delta; bb1.put(index, values, 0, length); } else { bb1.put(index, values, 0, length); } if (delta > 0) { final ByteBuffer bb2 = segments.get(bufferIndex + 1); bb2.put(0, values, length, delta); } } @Override public void getBytes(long bytePos, byte[] values, int length) { assert length <= segmentSizeInBytes : "the length has to be smaller or equal to the segment size: " + length + " vs. " + segmentSizeInBytes; int bufferIndex = (int) (bytePos >>> segmentSizePower); int index = (int) (bytePos & indexDivisor); int delta = index + length - segmentSizeInBytes; final ByteBuffer bb1 = segments.get(bufferIndex); if (delta > 0) { length -= delta; bb1.get(index, values, 0, length); final ByteBuffer bb2 = segments.get(bufferIndex + 1); bb2.get(0, values, length, delta); } else { bb1.get(index, values, 0, length); } } @Override public void setByte(long bytePos, byte value) { int bufferIndex = (int) (bytePos >>> segmentSizePower); int index = (int) (bytePos & indexDivisor); final ByteBuffer bb1 = segments.get(bufferIndex); bb1.put(index, value); } @Override public byte getByte(long bytePos) { int bufferIndex = (int) (bytePos >>> segmentSizePower); int index = (int) (bytePos & indexDivisor); final ByteBuffer bb1 = segments.get(bufferIndex); return bb1.get(index); } @Override public long getCapacity() { long cap = 0; for (ByteBuffer bb : segments) { cap += bb.capacity(); } return cap; } @Override public int getSegments() { return segments.size(); } /** * Cleans up MappedByteBuffers. Be sure you bring the segments list in a consistent state * afterwards. *

* * @param from inclusive * @param to exclusive */ private void clean(int from, int to) { for (int i = from; i < to; i++) { ByteBuffer bb = segments.get(i); cleanMappedByteBuffer(bb); segments.set(i, null); } } @Override public DAType getType() { return DAType.MMAP; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy