/* * 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.Constants; import com.graphhopper.util.Helper; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; 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. * * <p> * * @author Peter Karich * @author Michael Zilske */ public final class MMapDataAccess extends AbstractDataAccess { private final boolean allowWrites; private RandomAccessFile raFile; private List<ByteBuffer> segments = new ArrayList<>(); MMapDataAccess(String name, String location, ByteOrder order, boolean allowWrites) { super(name, location, order); this.allowWrites = allowWrites; } 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); setSegmentSize(segmentSizeInBytes); ensureCapacity(bytes); return this; } @Override public DataAccess copyTo(DataAccess da) { // if(da instanceof MMapDataAccess) { // TODO PERFORMANCE make copying into mmap a lot faster via bytebuffer // also copying into RAMDataAccess could be faster via bytebuffer // is a flush necessary then? // } return super.copyTo(da); } @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 + " for " + name + " at position " + bufferStart + " for " + byteCount + " bytes with offset " + offset + ", new fileLength:" + newFileLength, ex); } } private ByteBuffer 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 ByteBuffer 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++; Helper.cleanHack(); 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 { if (!segments.isEmpty() && segments.get(0) instanceof MappedByteBuffer) { for (ByteBuffer bb : segments) { ((MappedByteBuffer) 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); } } @Override public void close() { super.close(); close(true); } /** * @param forceClean if true the clean hack (system.gc) will be executed and forces the system * to cleanup the mmap resources. Set false if you need to close many MMapDataAccess objects. */ void close(boolean forceClean) { clean(0, segments.size()); segments.clear(); Helper.close(raFile); if (forceClean) Helper.cleanHack(); } @Override public final void setInt(long bytePos, int value) { int bufferIndex = (int) (bytePos >> segmentSizePower); int index = (int) (bytePos & indexDivisor); ByteBuffer byteBuffer = segments.get(bufferIndex); synchronized (byteBuffer) { byteBuffer.putInt(index, value); } } @Override public final int getInt(long bytePos) { int bufferIndex = (int) (bytePos >> segmentSizePower); int index = (int) (bytePos & indexDivisor); ByteBuffer byteBuffer = segments.get(bufferIndex); synchronized (byteBuffer) { return byteBuffer.getInt(index); } } @Override public final void setShort(long bytePos, short value) { int bufferIndex = (int) (bytePos >>> segmentSizePower); int index = (int) (bytePos & indexDivisor); ByteBuffer byteBuffer = segments.get(bufferIndex); synchronized (byteBuffer) { byteBuffer.putShort(index, value); } } @Override public final short getShort(long bytePos) { int bufferIndex = (int) (bytePos >>> segmentSizePower); int index = (int) (bytePos & indexDivisor); ByteBuffer byteBuffer = segments.get(bufferIndex); synchronized (byteBuffer) { 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); synchronized (bb1) { bb1.position(index); if (delta > 0) { length -= delta; bb1.put(values, 0, length); } else { bb1.put(values, 0, length); } } if (delta > 0) { final ByteBuffer bb2 = segments.get(bufferIndex + 1); synchronized (bb2) { bb2.position(0); bb2.put(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); synchronized (bb1) { bb1.position(index); if (delta > 0) { length -= delta; bb1.get(values, 0, length); } else { bb1.get(values, 0, length); } } if (delta > 0) { final ByteBuffer bb2 = segments.get(bufferIndex + 1); synchronized (bb2) { bb2.position(0); bb2.get(values, length, delta); } } } @Override public long getCapacity() { long cap = 0; for (ByteBuffer bb : segments) { synchronized (bb) { 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. * <p> * * @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); Helper.cleanMappedByteBuffer(bb); segments.set(i, null); } } @Override public void trimTo(long capacity) { if (capacity < segmentSizeInBytes) { capacity = segmentSizeInBytes; } int remainingSegNo = (int) (capacity / segmentSizeInBytes); if (capacity % segmentSizeInBytes != 0) { remainingSegNo++; } clean(remainingSegNo, segments.size()); Helper.cleanHack(); segments = new ArrayList<>(segments.subList(0, remainingSegNo)); try { // windows does not allow changing the length of an open files if (!Constants.WINDOWS) { // reduce file size raFile.setLength(HEADER_OFFSET + remainingSegNo * segmentSizeInBytes); } } catch (Exception ex) { throw new RuntimeException(ex); } } @Override public void rename(String newName) { if (!checkBeforeRename(newName)) { return; } close(); super.rename(newName); // 'reopen' with newName raFile = null; closed = false; loadExisting(); } @Override public DAType getType() { return DAType.MMAP; } }