Created
February 5, 2025 10:19
-
-
Save mjgp2/a7e10c293453ef9586e62d5326a22bac to your computer and use it in GitHub Desktop.
Benchmarking different durability options in Java like fsync, channel.force, direct JNA calls, ...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.File; | |
import java.io.FileDescriptor; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.OutputStream; | |
import java.io.RandomAccessFile; | |
import java.lang.reflect.Field; | |
import java.nio.channels.Channels; | |
import java.nio.channels.FileChannel; | |
import java.nio.file.FileSystems; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
import java.nio.file.StandardOpenOption; | |
import java.util.stream.IntStream; | |
import com.sun.jna.Library; | |
import com.sun.jna.Native; | |
import com.sun.jna.Pointer; | |
import com.sun.jna.Memory; | |
import sun.misc.Unsafe; | |
public class FsyncBenchmark { | |
private static final int TEST_ITERATIONS = 100; // Number of writes | |
private static final int DATA_SIZE = 500; // 500 bytes per write | |
private static final byte[] TEST_BYTES = "X".repeat(DATA_SIZE).getBytes(); | |
private static final Path FILE_PATH = Paths.get("fsync_benchmark.dat"); | |
private static Unsafe unsafe; | |
static { | |
try { | |
Field f = Unsafe.class.getDeclaredField("theUnsafe"); | |
f.setAccessible(true); | |
unsafe = (Unsafe) f.get(null); | |
} catch (NoSuchFieldException | IllegalAccessException e) { | |
throw new RuntimeException("Unable to get Unsafe instance", e); | |
} | |
} | |
public static int getFD(FileDescriptor fd) { | |
try { | |
long fdOffset = unsafe.objectFieldOffset(FileDescriptor.class.getDeclaredField("fd")); | |
return unsafe.getInt(fd, fdOffset); | |
} catch (NoSuchFieldException e) { | |
throw new RuntimeException("Unable to access fd field", e); | |
} | |
} | |
public interface Constants extends Library { | |
Constants INSTANCE = Native.load("constants", Constants.class); | |
int getConstant(String name); | |
static int constant(String name) { | |
return INSTANCE.getConstant(name); | |
} | |
} | |
public interface CLib extends Library { | |
CLib INSTANCE = Native.load("c", CLib.class); | |
// Open a file with flags | |
int open(String path, int flags, int mode); | |
// Close the file | |
int close(int fd); | |
// Write to file | |
int write(int fd, Pointer buffer, int count); | |
// Sync (fsync) | |
int fsync(int fd); | |
int fdatasync(int fd); | |
int fcntl(int fd, int command, int arg); | |
} | |
// Get the file descriptor using JNA | |
public static class NativeDescriptor { | |
static { | |
Native.register("c"); // Link against standard C library | |
} | |
public static native int fileno(FileDescriptor fd); | |
} | |
public static boolean isMac() { | |
return System.getProperty("os.name").toLowerCase().contains("mac"); | |
} | |
public static void main(String[] args) throws Exception { | |
Files.deleteIfExists(FILE_PATH); | |
Files.createFile(FILE_PATH); | |
System.out.println("\nBenchmarking O_DSYNC..."); | |
benchmark_jna_O_DSYNC(); | |
System.out.println("\nBenchmarking O_SYNC..."); | |
benchmark_jna_O_SYNC(); | |
System.out.println("\nBenchmarking StandardOpenOption.DSYNC..."); | |
benchmark_dsync(); | |
System.out.println("\nBenchmarking StandardOpenOption.SYNC()..."); | |
benchmark_sync(); | |
System.out.println("\nBenchmarking benchmark_fsync..."); | |
benchmark_fsync(); | |
System.out.println("\nBenchmarking JNA fsync()..."); | |
benchmark_jna_fsync(); | |
System.out.println("\nBenchmarking JNA fdatasync()..."); | |
benchmark_fdatasync(); | |
System.out.println("\nBenchmarking force(true)..."); | |
benchmark_force(true); | |
System.out.println("\nBenchmarking force(false)..."); | |
benchmark_force(false); | |
if ( isMac() ) { | |
System.out.println("\nBenchmarking F_FULLFSYNC..."); | |
benchmark_F_FULLFSYNC(); | |
} | |
} | |
private static void benchmark_force(boolean meta) throws Exception { | |
try (FileChannel channel = | |
FileChannel.open(FILE_PATH, StandardOpenOption.APPEND, StandardOpenOption.WRITE); | |
OutputStream outputStream = Channels.newOutputStream(channel)) { | |
long totalTime = | |
IntStream.range(0, TEST_ITERATIONS) | |
.mapToLong( | |
i -> { | |
return measureTime( | |
() -> { | |
try { | |
outputStream.write(TEST_BYTES); // Append 500 bytes | |
outputStream.flush(); | |
channel.force(meta); // Calls fsync() | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
}); | |
}) | |
.sum(); | |
System.out.printf("force("+meta+") Avg Time: %.2f ms%n", (totalTime / (double) TEST_ITERATIONS)); | |
} | |
} | |
private static void benchmark_dsync() throws Exception { | |
try (FileChannel channel = | |
FileChannel.open(FILE_PATH, StandardOpenOption.APPEND, StandardOpenOption.WRITE, StandardOpenOption.DSYNC); | |
OutputStream outputStream = Channels.newOutputStream(channel)) { | |
long totalTime = | |
IntStream.range(0, TEST_ITERATIONS) | |
.mapToLong( | |
i -> { | |
return measureTime( | |
() -> { | |
try { | |
outputStream.write(TEST_BYTES); // Append 500 bytes | |
outputStream.flush(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
}); | |
}) | |
.sum(); | |
System.out.printf("StandardOpenOption.DSYNC Avg Time: %.2f ms%n", (totalTime / (double) TEST_ITERATIONS)); | |
} | |
} | |
private static void benchmark_sync() throws Exception { | |
try (FileChannel channel = | |
FileChannel.open(FILE_PATH, StandardOpenOption.APPEND, StandardOpenOption.WRITE, StandardOpenOption.SYNC); | |
OutputStream outputStream = Channels.newOutputStream(channel)) { | |
long totalTime = | |
IntStream.range(0, TEST_ITERATIONS) | |
.mapToLong( | |
i -> { | |
return measureTime( | |
() -> { | |
try { | |
outputStream.write(TEST_BYTES); // Append 500 bytes | |
outputStream.flush(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
}); | |
}) | |
.sum(); | |
System.out.printf("StandardOpenOption.SYNC Avg Time: %.2f ms%n", (totalTime / (double) TEST_ITERATIONS)); | |
} | |
} | |
private static void benchmark_jna_O_DSYNC() throws Exception { | |
int fd = CLib.INSTANCE.open(FILE_PATH.toFile().getAbsolutePath(), Constants.constant("O_WRONLY") | Constants.constant("O_CREAT") | Constants.constant("O_DSYNC") , 0644); | |
if (fd < 0) { | |
System.err.println("Failed to open file"); | |
return; | |
} | |
File file = new File("/dev/fd/" + fd); | |
try (FileOutputStream outputStream = new FileOutputStream( file)) { | |
if (fd < 0) { | |
throw new RuntimeException("Failed to get file descriptor"); | |
} | |
long totalTime = | |
IntStream.range(0, TEST_ITERATIONS) | |
.mapToLong( | |
i -> { | |
return measureTime( | |
() -> { | |
try { | |
Pointer buffer = new Memory(TEST_BYTES.length); | |
buffer.write(0, TEST_BYTES, 0, TEST_BYTES.length); // Copy Java byte array into JNA Memory | |
CLib.INSTANCE.write(fd, buffer, TEST_BYTES.length); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
}); | |
}) | |
.sum(); | |
System.out.printf("O_DSYNC Avg Time: %.2f ms%n", (totalTime / (double) TEST_ITERATIONS)); | |
} | |
} | |
private static void benchmark_jna_O_SYNC() throws Exception { | |
int fd = CLib.INSTANCE.open(FILE_PATH.toFile().getAbsolutePath(), Constants.constant("O_WRONLY") | Constants.constant("O_CREAT") | Constants.constant("O_SYNC") , 0644); | |
if (fd < 0) { | |
System.err.println("Failed to open file"); | |
return; | |
} | |
File file = new File("/dev/fd/" + fd); | |
try (FileOutputStream outputStream = new FileOutputStream( file)) { | |
if (fd < 0) { | |
throw new RuntimeException("Failed to get file descriptor"); | |
} | |
long totalTime = | |
IntStream.range(0, TEST_ITERATIONS) | |
.mapToLong( | |
i -> { | |
return measureTime( | |
() -> { | |
try { | |
Pointer buffer = new Memory(TEST_BYTES.length); | |
buffer.write(0, TEST_BYTES, 0, TEST_BYTES.length); // Copy Java byte array into JNA Memory | |
CLib.INSTANCE.write(fd, buffer, TEST_BYTES.length); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
}); | |
}) | |
.sum(); | |
System.out.printf("O_DSYNC Avg Time: %.2f ms%n", (totalTime / (double) TEST_ITERATIONS)); | |
} | |
} | |
private static void benchmark_F_FULLFSYNC() throws Exception { | |
int fd = CLib.INSTANCE.open(FILE_PATH.toFile().getAbsolutePath(), Constants.constant("O_WRONLY") | Constants.constant("O_CREAT") , 0644); | |
if (fd < 0) { | |
System.err.println("Failed to open file"); | |
return; | |
} | |
File file = new File("/dev/fd/" + fd); | |
try (FileOutputStream outputStream = new FileOutputStream( file)) { | |
if (fd < 0) { | |
throw new RuntimeException("Failed to get file descriptor"); | |
} | |
long totalTime = | |
IntStream.range(0, TEST_ITERATIONS) | |
.mapToLong( | |
i -> { | |
return measureTime( | |
() -> { | |
try { | |
outputStream.write(TEST_BYTES); // Append 500 bytes | |
outputStream.flush(); | |
CLib.INSTANCE.fcntl(fd, Constants.constant("F_FULLFSYNC"), 0); // Calls F_FULLFSYNC | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
}); | |
}) | |
.sum(); | |
System.out.printf("F_FULLFSYNC Avg Time: %.2f ms%n", (totalTime / (double) TEST_ITERATIONS)); | |
} | |
} | |
private static void benchmark_fdatasync() throws Exception { | |
int fd = CLib.INSTANCE.open(FILE_PATH.toFile().getAbsolutePath(), Constants.constant("O_WRONLY") | Constants.constant("O_CREAT") , 0644); | |
if (fd < 0) { | |
System.err.println("Failed to open file"); | |
return; | |
} | |
File file = new File("/dev/fd/" + fd); | |
try (FileOutputStream outputStream = new FileOutputStream( file)) { | |
if (fd < 0) { | |
throw new RuntimeException("Failed to get file descriptor"); | |
} | |
long totalTime = | |
IntStream.range(0, TEST_ITERATIONS) | |
.mapToLong( | |
i -> { | |
return measureTime( | |
() -> { | |
try { | |
outputStream.write(TEST_BYTES); // Append 500 bytes | |
outputStream.flush(); | |
CLib.INSTANCE.fdatasync(fd); // Calls F_FULLFSYNC | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
}); | |
}) | |
.sum(); | |
System.out.printf("JNA fdatasync Avg Time: %.2f ms%n", (totalTime / (double) TEST_ITERATIONS)); | |
} | |
} | |
private static void benchmark_jna_fsync() throws Exception { | |
int fd = CLib.INSTANCE.open(FILE_PATH.toFile().getAbsolutePath(), Constants.constant("O_WRONLY") | Constants.constant("O_CREAT") , 0644); | |
if (fd < 0) { | |
System.err.println("Failed to open file"); | |
return; | |
} | |
File file = new File("/dev/fd/" + fd); | |
try (FileOutputStream outputStream = new FileOutputStream( file)) { | |
if (fd < 0) { | |
throw new RuntimeException("Failed to get file descriptor"); | |
} | |
long totalTime = | |
IntStream.range(0, TEST_ITERATIONS) | |
.mapToLong( | |
i -> { | |
return measureTime( | |
() -> { | |
try { | |
outputStream.write(TEST_BYTES); // Append 500 bytes | |
outputStream.flush(); | |
CLib.INSTANCE.fsync(fd); // Calls F_FULLFSYNC | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
}); | |
}) | |
.sum(); | |
System.out.printf("JNA fsync Avg Time: %.2f ms%n", (totalTime / (double) TEST_ITERATIONS)); | |
} | |
} | |
private static void benchmark_fsync() throws Exception { | |
try (RandomAccessFile file = new RandomAccessFile(FILE_PATH.toFile(), "rw"); | |
FileOutputStream outputStream = new FileOutputStream(file.getFD())) { | |
long totalTime = | |
IntStream.range(0, TEST_ITERATIONS) | |
.mapToLong( | |
i -> { | |
return measureTime( | |
() -> { | |
try { | |
outputStream.write(TEST_BYTES); // Append 500 bytes | |
outputStream.flush(); | |
file.getFD().sync(); // Calls fsync() | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
}); | |
}) | |
.sum(); | |
System.out.printf("file.getFD().sync() Avg Time: %.2f ms%n", (totalTime / (double) TEST_ITERATIONS)); | |
} | |
} | |
private static long measureTime(Runnable action) { | |
long start = System.nanoTime(); | |
action.run(); | |
return (System.nanoTime() - start) / 1_000_000; // Convert to ms | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <fcntl.h> // For O_* flags | |
#include <string.h> | |
#ifdef __APPLE__ | |
#include <sys/types.h> | |
#include <sys/stat.h> | |
#include <unistd.h> | |
#endif | |
// Function to get the value of a constant by name | |
int getConstant(const char *name) { | |
if (strcmp(name, "O_RDONLY") == 0) { | |
return O_RDONLY; | |
} else if (strcmp(name, "O_WRONLY") == 0) { | |
return O_WRONLY; | |
} else if (strcmp(name, "O_RDWR") == 0) { | |
return O_RDWR; | |
} else if (strcmp(name, "O_CREAT") == 0) { | |
return O_CREAT; | |
} else if (strcmp(name, "O_EXCL") == 0) { | |
return O_EXCL; | |
} else if (strcmp(name, "O_NOCTTY") == 0) { | |
return O_NOCTTY; | |
} else if (strcmp(name, "O_TRUNC") == 0) { | |
return O_TRUNC; | |
} else if (strcmp(name, "O_APPEND") == 0) { | |
return O_APPEND; | |
} else if (strcmp(name, "O_NONBLOCK") == 0) { | |
return O_NONBLOCK; | |
} else if (strcmp(name, "O_SYNC") == 0) { | |
return O_SYNC; | |
} else if (strcmp(name, "O_DSYNC") == 0) { | |
return O_DSYNC; | |
} else if (strcmp(name, "O_DIRECT") == 0) { | |
#ifdef O_DIRECT | |
return O_DIRECT; // Available on Linux, not macOS | |
#else | |
return -1; // Not available | |
#endif | |
} else if (strcmp(name, "O_DIRECTORY") == 0) { | |
return O_DIRECTORY; | |
} else if (strcmp(name, "O_NOFOLLOW") == 0) { | |
return O_NOFOLLOW; | |
} else if (strcmp(name, "O_CLOEXEC") == 0) { | |
return O_CLOEXEC; | |
} else if (strcmp(name, "O_ASYNC") == 0) { | |
return O_ASYNC; | |
} | |
#ifdef __APPLE__ | |
else if (strcmp(name, "F_FULLFSYNC") == 0) { | |
return F_FULLFSYNC; // macOS only | |
} | |
#endif | |
return -1; // Return -1 if the constant is not found | |
} | |
// Test function to print all constants | |
void printConstants() { | |
char *constants[] = { | |
"O_RDONLY", "O_WRONLY", "O_RDWR", "O_CREAT", "O_EXCL", | |
"O_NOCTTY", "O_TRUNC", "O_APPEND", "O_NONBLOCK", "O_SYNC", | |
"O_DSYNC", "O_RSYNC", "O_DIRECT", "O_DIRECTORY", "O_NOFOLLOW", | |
"O_CLOEXEC", "O_ASYNC", | |
#ifdef __APPLE__ | |
"F_FULLFSYNC", | |
#endif | |
NULL | |
}; | |
for (int i = 0; constants[i] != NULL; i++) { | |
printf("%s = 0x%x\n", constants[i], getConstant(constants[i])); | |
} | |
} | |
// Main function for testing | |
int main() { | |
printConstants(); | |
return 0; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Benchmarking O_DSYNC... | |
O_DSYNC Avg Time: 1.71 ms | |
Benchmarking O_SYNC... | |
O_DSYNC Avg Time: 2.03 ms | |
Benchmarking StandardOpenOption.DSYNC... | |
StandardOpenOption.DSYNC Avg Time: 1.62 ms | |
Benchmarking StandardOpenOption.SYNC()... | |
StandardOpenOption.SYNC Avg Time: 1.52 ms | |
Benchmarking benchmark_fsync... | |
file.getFD().sync() Avg Time: 0.15 ms | |
Benchmarking JNA fsync()... | |
JNA fsync Avg Time: 1.52 ms | |
Benchmarking JNA fdatasync()... | |
JNA fdatasync Avg Time: 1.41 ms | |
Benchmarking force(true)... | |
force(true) Avg Time: 1.47 ms | |
Benchmarking force(false)... | |
force(false) Avg Time: 1.58 ms |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Benchmarking O_DSYNC... | |
O_DSYNC Avg Time: 0.00 ms | |
Benchmarking O_SYNC... | |
O_DSYNC Avg Time: 0.00 ms | |
Benchmarking StandardOpenOption.DSYNC... | |
StandardOpenOption.DSYNC Avg Time: 0.00 ms | |
Benchmarking StandardOpenOption.SYNC()... | |
StandardOpenOption.SYNC Avg Time: 0.00 ms | |
Benchmarking benchmark_fsync... | |
file.getFD().sync() Avg Time: 0.00 ms | |
Benchmarking JNA fsync()... | |
JNA fsync Avg Time: 0.00 ms | |
Benchmarking JNA fdatasync()... | |
JNA fdatasync Avg Time: 0.00 ms | |
Benchmarking force(true)... | |
force(true) Avg Time: 4.70 ms | |
Benchmarking force(false)... | |
force(false) Avg Time: 4.31 ms | |
Benchmarking F_FULLFSYNC... | |
F_FULLFSYNC Avg Time: 4.24 ms |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
On Mac, fsync doesn't do the same as on Linux... F_FULLFSYNC is required and it is incredibly slow.
I don't quite understand why the FileDescriptor.sync() is the best performing by an order of magnitude as it is calling fsync() under the hood.