2024-02-05 11:31:15 +00:00
|
|
|
package nu.marginalia.rwf;
|
|
|
|
|
|
|
|
import nu.marginalia.array.LongArray;
|
|
|
|
import nu.marginalia.array.LongArrayFactory;
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.nio.file.Files;
|
|
|
|
import java.nio.file.Path;
|
|
|
|
import java.nio.file.StandardCopyOption;
|
|
|
|
import java.nio.file.StandardOpenOption;
|
|
|
|
|
|
|
|
/** A RandomFileAssembler is a way to write a large file out of order
|
|
|
|
* in a way that is efficient for SSDs.
|
|
|
|
*/
|
|
|
|
public interface RandomFileAssembler extends AutoCloseable {
|
|
|
|
|
|
|
|
void put(long address, long data) throws IOException;
|
|
|
|
void write(Path file) throws IOException;
|
|
|
|
void close() throws IOException;
|
|
|
|
|
|
|
|
|
|
|
|
/** Select the appropriate RandomFileAssembler implementation based on
|
|
|
|
* the system configuration.
|
|
|
|
*/
|
|
|
|
static RandomFileAssembler create(Path workDir,
|
|
|
|
long totalSize) throws IOException {
|
|
|
|
// If the system is configured to conserve memory, we use temp files
|
2024-02-09 13:43:08 +00:00
|
|
|
if (Boolean.getBoolean("system.conserveMemory")) {
|
2024-02-05 20:01:32 +00:00
|
|
|
return ofTempFiles(workDir, totalSize);
|
2024-02-05 11:31:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the file is small, we use straight mmap
|
|
|
|
if (totalSize < 128_000_000) { // 128M longs = 1 GB
|
|
|
|
return ofMmap(workDir, totalSize);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the file is large, we use an in-memory buffer to avoid disk thrashing
|
|
|
|
return ofInMemoryAsssembly(totalSize);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Create a RandomFileAssembler that writes to a series of small files.
|
|
|
|
* This has negligible memory overhead, but is slower than in-memory
|
|
|
|
* or mmap for small files.
|
|
|
|
*/
|
2024-02-05 20:01:32 +00:00
|
|
|
static RandomFileAssembler ofTempFiles(Path workDir, long sizeInLongs) throws IOException {
|
2024-02-05 11:31:15 +00:00
|
|
|
|
|
|
|
return new RandomFileAssembler() {
|
|
|
|
private final RandomWriteFunnel funnel = new RandomWriteFunnel(workDir, 10_000_000);
|
|
|
|
@Override
|
|
|
|
public void put(long address, long data) throws IOException {
|
|
|
|
funnel.put(address, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void write(Path file) throws IOException {
|
|
|
|
try (var channel = Files.newByteChannel(file, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
|
|
|
|
funnel.write(channel);
|
2024-02-05 20:01:32 +00:00
|
|
|
|
|
|
|
// It's very likely we'll have overshot the size a bit, truncate to the correct size
|
|
|
|
channel.truncate(8 * sizeInLongs);
|
2024-02-05 11:31:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void close() throws IOException {
|
|
|
|
funnel.close();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Create a RandomFileAssembler that writes to a LongArray in memory. */
|
|
|
|
static RandomFileAssembler ofInMemoryAsssembly(long size) {
|
|
|
|
return new RandomFileAssembler() {
|
|
|
|
private final LongArray buffer = LongArrayFactory.onHeapConfined(size);
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void put(long address, long data) {
|
|
|
|
buffer.set(address, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void write(Path file) throws IOException {
|
|
|
|
buffer.write(file);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void close() {
|
|
|
|
buffer.close();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Create a RandomFileAssembler that writes to a file using mmap.
|
|
|
|
* This is the fastest method for small files, but has a large memory
|
|
|
|
* overhead and is slow for large files, where the OS will start pushing
|
|
|
|
* changes to disk continuously.
|
|
|
|
* */
|
|
|
|
static RandomFileAssembler ofMmap(Path destDir, long size) throws IOException {
|
|
|
|
return new RandomFileAssembler() {
|
|
|
|
private final Path workFile = Files.createTempFile(destDir, "mmap", ".dat");
|
|
|
|
private final LongArray buffer = LongArrayFactory.mmapForWritingConfined(workFile, size);
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void put(long address, long data) {
|
|
|
|
buffer.set(address, data);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void write(Path dest) throws IOException {
|
|
|
|
buffer.force();
|
|
|
|
|
|
|
|
Files.move(workFile, dest,
|
|
|
|
StandardCopyOption.REPLACE_EXISTING,
|
|
|
|
StandardCopyOption.ATOMIC_MOVE);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void close() throws IOException {
|
|
|
|
buffer.close();
|
|
|
|
|
|
|
|
// Catch the case where e.g. write() fails with an exception and workFile doesn't get moved
|
|
|
|
Files.deleteIfExists(workFile);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|