/**
* Copyright (c) 2014 Richard Warburton (richard.warburton@gmail.com)
* <p>
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
* <p>
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
* <p>
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
**/
package com.insightfullogic.honest_profiler.ports.sources;
import static java.nio.channels.FileChannel.MapMode.READ_ONLY;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import com.insightfullogic.honest_profiler.core.sources.CantReadFromSourceException;
import com.insightfullogic.honest_profiler.core.sources.LogSource;
/**
* LogSource implementation which maintains a fixed-size memory-mapped window ({@value #BUFFER_SIZE} bytes) on the file.
* The read() method checks whether a certain amount of the current buffer ({@value #ELASTICITY} bytes) has been read
* already, and if so, remaps the buffer to the new position.
* <p>
* The ELASTICITY is provided for the benefit of the LogParser + Conductor logic. If a partial record is read, the
* Conductor will sleep for a bit. So ideally, BUFFER_SIZE - ELASTICITY should be large enough so that an "entire
* record" (i.e. an entire stack) which might start at the last byte within the ELASTICITY portion still would fit into
* the remains of the buffer.
*/
public class FileLogSource implements LogSource
{
// Class Properties
// Fixed buffer size
private static final int BUFFER_SIZE = 1024 * 1024 * 100; // 100 MB
// Remap if more than ELASTICITY has been read from the current buffer
private static final int ELASTICITY = 1024 * 1024 * 1; // 1 MB
// Instance Properties
private final RandomAccessFile raf;
private final FileChannel channel;
private final File file;
private MappedByteBuffer buffer;
// the offset in the file where the current buffer starts.
private long currentOffset = 0;
private int previousPosition;
// Instance Constructors
public FileLogSource(final File file)
{
this.file = file;
try
{
raf = new RandomAccessFile(file, "r");
channel = raf.getChannel();
mapBuffer(0);
}
catch (IOException e)
{
throw new CantReadFromSourceException(e);
}
}
// Instance Accessors
public File getFile()
{
return file;
}
// LogSource Implementation
@Override
public ByteBuffer read()
{
try
{
int position = buffer.position();
boolean hasRemaining = buffer.hasRemaining();
if (position == previousPosition)
{
// The buffer was rewound after the previous read. Either the data was not written (0 was read) or a
// buffer underflow occurred. Don't update currentOffset, or we'll skip data.
mapBuffer(currentOffset);
}
// channel.size() is *very* expensive so we try and minimize its invocation.
else if (position > ELASTICITY || (!hasRemaining && currentOffset < channel.size()))
{
// If the buffer is empty but the file size increased, or we've read more than ELASTICITY bytes, the
// currentOffset is updated and the buffer is remapped.
currentOffset += position;
mapBuffer(currentOffset);
}
else
{
previousPosition = position;
}
}
catch (IOException e)
{
throw new CantReadFromSourceException(e);
}
return buffer;
}
@Override
public void close() throws IOException
{
buffer = null;
raf.close();
}
// Shame there's no simple abstraction for reading over both files and
// network bytebuffers
/**
* Replaces the current buffer by a new ByteBuffer which is memory-mapped onto a BUFFER_SIZE (10 MB at time of
* writing) window starting at the specified offset.
* <p>
* @param offset the offset in the file of the area which will be mapped into the buffer
* @throws IOException any I/O exceptions encountered trying to map a portion of the file into memory
*/
private void mapBuffer(long offset) throws IOException
{
int length = BUFFER_SIZE;
long fileEnd = channel.size();
if (offset + BUFFER_SIZE > fileEnd)
{
// Cast to int is safe, since the test determines that the int
// BUFFER_SIZE > fileEnd - offset)
length = (int) (fileEnd - offset);
}
buffer = channel.map(READ_ONLY, offset, length);
// Ensures we know next time read() is called we can easily test whether the position moved or was remapped in a
// single comparison.
previousPosition = -1;
}
@Override
public String toString()
{
return "FileLogSource{" + "file=" + file + '}';
}
}