/* * Created on Apr 24, 2004 * Created by Alon Rohter * Copyright (C) 2004, 2005, 2006 Aelitis, All Rights Reserved. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * AELITIS, SAS au capital de 46,603.30 euros * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France. * */ package com.aelitis.azureus.core.diskmanager; import java.io.*; import java.nio.*; import java.nio.channels.FileChannel; import java.util.*; import java.security.*; import org.gudy.azureus2.core3.util.AEMonitor; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.DirectByteBuffer; /** * This class implements a virtual disk file backed by a pool of direct * memory MappedByteBuffers, designed for high-speed random reads/writes. * * NOTE: Abandoning this code for now, as JRE 1.4 series does not provide an * unmap() method for MappedByteBuffers, so we have to wait for the VM * to lazily free the underlying direct memory regions, which means we * eventually run out of direct memory when accessing files larger than the * allowed direct memory space, since the unused buffers are not freed * quickly enough. Forcing the memory unmap via sun.misc.Cleaner does not work * properly under Windows, throwing the error described in the clean(x) method * below. * */ public class MemoryMappedFile { public static final int MODE_READ_ONLY = 0; public static final int MODE_READ_WRITE = 1; public static long cache_hits = 0; public static long cache_misses = 0; private static final int BLOCK_SIZE = 10*1024*1024; //10MB private final File file; private int access_mode = MODE_READ_ONLY; private FileChannel channel; private Object[] mapKeys; private int[] counts = new int[ 10000 ]; public MemoryMappedFile( File file ) { this.file = file; mapKeys = new Object[ new Long(file.length()/BLOCK_SIZE).intValue() + 1 ]; Arrays.fill( counts, 0 ); } public int getAccessMode() { return access_mode; } public void setAccessMode( int mode ) { if ( mode == MODE_READ_ONLY && access_mode == MODE_READ_WRITE && channel != null ) { try { channel.close(); } catch( Exception e ) { Debug.printStackTrace( e ); } channel = null; } access_mode = mode; } public void write( DirectByteBuffer buffer, int buffer_offset, long file_offset, int length ) throws IOException { if ( access_mode == MODE_READ_ONLY ) { throw new IOException( "cannot write to a read-only file" ); } if ( buffer.limit(DirectByteBuffer.SS_OTHER) - buffer_offset < length ) { throw new IOException( "not enough buffer remaining to write given length" ); } file.createNewFile(); int key_pos = new Long( file_offset / BLOCK_SIZE ).intValue(); int map_offset = new Long( file_offset % BLOCK_SIZE ).intValue(); int written = 0; while( written < length ) { MappedByteBuffer mbb = null; long f_offset = file_offset + written; int length_to_write = BLOCK_SIZE - map_offset; if ( length - written < length_to_write ) length_to_write = length - written; //try grab the buffer from the pool if ( mapKeys.length > key_pos ) { Object key = mapKeys[ key_pos ]; if ( key != null ) { mbb = MemoryMapPool.getBuffer( key ); //check if the current buff is too small if ( mbb != null && mbb.capacity() < (map_offset + length_to_write ) ) { MemoryMapPool.clean( mbb ); mbb = null; } } } else { //need to extend the key array mapKeys = new Object[ key_pos * 2 ]; } //create new buffer, fully-sized, unless it's extending the file if ( mbb == null ) { int size = BLOCK_SIZE; if ( f_offset + length_to_write > file.length() ) { size = map_offset + length_to_write; } mbb = createMappedBuffer( f_offset - map_offset, size ); cache_misses++; } else cache_hits++; //do the write buffer.position( DirectByteBuffer.SS_OTHER,buffer_offset + written ); buffer.limit( DirectByteBuffer.SS_OTHER,buffer.position(DirectByteBuffer.SS_OTHER) + length_to_write ); mbb.position( map_offset ); mbb.put( buffer.getBuffer(DirectByteBuffer.SS_OTHER) ); written += length_to_write; //add the buffer (back) to the pool mapKeys[ key_pos ] = MemoryMapPool.addBuffer( mbb ); key_pos++; map_offset = 0; } } private MappedByteBuffer createMappedBuffer( long file_offset, int length ) throws IOException { if ( channel == null ) { FileChannel fc = new RandomAccessFile( file, "rw" ).getChannel(); MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, file_offset, length ); if ( access_mode == MODE_READ_ONLY ) fc.close(); else channel = fc; return mbb; } return channel.map( FileChannel.MapMode.READ_WRITE, file_offset, length ); } private static class MemoryMapPool { private static long MAX_SIZE = 100*1024*1024; //100MB private static final MemoryMapPool instance = new MemoryMapPool(); private long total_size = 0; private final AEMonitor buffers_mon = new AEMonitor( "MemoryMappedFile:buffers" ); private final Map buffers = new LinkedHashMap( (int)(MAX_SIZE/BLOCK_SIZE), .75F, true ){ //This method is called just after a new entry has been added public boolean removeEldestEntry(Map.Entry eldest) { boolean remove = total_size > MAX_SIZE; if ( remove ) { MappedByteBuffer mbb = (MappedByteBuffer)eldest.getValue(); total_size -= mbb.capacity(); clean( mbb ); } return remove; } }; //may return null despite a previously-valid key, because the buffer was GC'd //note: the buffer is removed from the pool private static MappedByteBuffer getBuffer( Object key ) { try{ instance.buffers_mon.enter(); MappedByteBuffer mbb = (MappedByteBuffer)instance.buffers.remove( key ); if ( mbb != null ) instance.total_size -= mbb.capacity(); return mbb; }finally{ instance.buffers_mon.exit(); } } private static Object addBuffer( MappedByteBuffer buffer ) { Object key = new Object(); try{ instance.buffers_mon.enter(); instance.total_size += buffer.capacity(); instance.buffers.put( key, buffer ); }finally{ instance.buffers_mon.exit(); } return key; } //Manually Cleaning the buffer doesn't work properly under windows: //java.lang.Error: Cleaner terminated abnormally: //Caused by: java.io.IOException: The process cannot access the file //because another process has locked a portion of the file //See Sun bug id #4938372. private static void clean( final MappedByteBuffer buffer ) { AccessController.doPrivileged( new PrivilegedAction() { public Object run() { try { //Method getCleanerMethod = buffer.getClass().getMethod( "cleaner", new Class[0] ); //getCleanerMethod.setAccessible( true ); //sun.misc.Cleaner cleaner = (sun.misc.Cleaner)getCleanerMethod.invoke( buffer, new Object[0] ); //cleaner.clean(); } catch (Exception e) { Debug.printStackTrace( e ); } return null; } }); } } }