Skip to content
tools4jPublic

About

Memory mapped files used to implement off-heap low latency utilities such as queues.

Resources

Stars

14 stars

Watchers

3 watching

Forks

Repository files navigation

build Coverage Status   releases: mmap-mapping mmap-queue   javadoc: mmap-mapping mmap-queue

tools4j/mmap

Memory mapped files used to implement off-heap low latency utilities such as queues.

The project has two modules:

  • mmap-mapping: memory-mapped file regions exposed as Agrona buffers, with region caching, files rolling over when reaching a maximum size, and mapping/unmapping in a background thread
  • mmap-queue: a persistent off-heap queue built on mmap-mapping, with concurrent appenders and any number of pollers and readers

Mappings

Package org.tools4j.mmap.mapping.api contains the mapping types and the Mappings factory, the main entry point. Configuration is in org.tools4j.mmap.mapping.config. The low-level mapping engine in org.tools4j.mmap.mapping.unsafe is not intended for use by application code.

Main classes:

  • Mappings: factory methods for all mapping types and mapping pools
  • Mapping: a block of a file mapped into memory, accessed via its buffer(), an Agrona AtomicBuffer
  • FixedMapping: a mapping with fixed start and end position
  • RegionMapping: a mapping of exactly one region, moved region by region
  • ElasticMapping: a mapping that can be moved to any position, its buffer spans to the end of the region
  • AdaptiveMapping: a mapping that can be moved to any position with an explicit length within a region
  • MappingPool: provides many mappings of the same file that share the underlying mapped regions
  • OpenMode: how a file is opened: read-only (optionally before it exists), read/write, create, open or create, or recreate
  • MappingConfig and MappingStrategyConfig: file size, rolling files, region size, region caching and async mapping/unmapping; defaults can be set via system properties

Concurrent Access

Mappings and mapping pools are not thread-safe: each thread needs its own mappings, or its own pool. The mapped memory itself is shared, however, and the mapping's AtomicBuffer supports CAS and memory ordering operations, for long and int values:

  • CAS: compareAndSetLong, getAndAddLong, ...
  • Memory ordering: putLongRelease, getLongAcquire, putLongVolatile, getLongVolatile, ...

Atomic operations require naturally aligned file positions, e.g. a multiple of 8 for long values. Threads, and also processes, can hence coordinate through their own mappings of the same file, as the queue does when appenders claim the next entry.

Quick example

File file = new File("/tmp/fixed.map");

//FixedMapping: maps a fixed slice of the file, here the first 64 bytes
try (FixedMapping mapping = Mappings.fixedMapping(file, OpenMode.OPEN_OR_CREATE, 0, 64)) {
    mapping.buffer().putInt(0, 42);
    mapping.buffer().putInt(60, 43);
}

//without length, the mapping spans from the offset to the end of the file
try (FixedMapping mapping = Mappings.fixedMapping(file, OpenMode.READ_ONLY, 60)) {
    int available = mapping.bytesAvailable();//4
    int value = mapping.buffer().getInt(0);//43
}

Dynamic mappings can be moved within a file, mapping regions of the file on demand:

File file = new File("/tmp/data.map");

//RegionMapping: exposes a whole region of the file and is moved region by region
try (RegionMapping mapping = Mappings.regionMapping(file, OpenMode.OPEN_OR_CREATE)) {
    for (int region = 0; region < 3; region++) {
        mapping.moveToRegion(region);
        mapping.buffer().putInt(0, 100 * region);
        mapping.buffer().putInt(4, 100 * region + 1);
    }
}

//ElasticMapping: can be moved to any position, its buffer spans to the end of the region
try (ElasticMapping mapping = Mappings.elasticMapping(file, OpenMode.READ_ONLY)) {
    mapping.moveTo(2L * mapping.regionSize());
    int value = mapping.buffer().getInt(0);//200
    mapping.moveBy(4);
    value = mapping.buffer().getInt(0);//201
}

//AdaptiveMapping: maps an arbitrary slice of the underlying file from one region
try (AdaptiveMapping mapping = Mappings.adaptiveMapping(file, OpenMode.READ_ONLY)) {
    int readLength = 8;
    mapping.moveTo(2L * mapping.regionSize(), readLength);//buffer capped at size 8
    int first = mapping.buffer().getInt(0);//200
    int second = mapping.buffer().getInt(4);//201
}

//MappingPool: mappings from the same pool share mapped regions (one pool per thread)
try (MappingPool pool = Mappings.mappingPool(file, OpenMode.READ_ONLY)) {
    ElasticMapping elastic = pool.acquireElasticMapping();
    AdaptiveMapping adaptive = pool.acquireAdaptiveMapping();
    elastic.moveToFirstRegion();
    adaptive.moveTo(4, Integer.BYTES);
    int first = elastic.buffer().getInt(0);//0
    int second = adaptive.buffer().getInt(0);//1
}

Rolling files with async mapping, i.e. regions mapped ahead in a background thread for sequential access:

//roll file every 1MB, map regions ahead in a background thread when accessed sequentially
MappingConfig config = MappingConfig.configure()
        .rollFiles(true)
        .maxFileSize(1 << 20)
        .maxOpenFiles(4)
        .regionSize(1 << 16)
        .mappingStrategy(cfg -> cfg.asyncMapping(true));
File file = new File("/tmp/rolling.map");//creates rolling_0.map, rolling_1.map, ...

try (var mapping = Mappings.regionMapping(file, OpenMode.OPEN_OR_CREATE, config)) {
    for (int region = 0; region < 64; region++) {
        mapping.moveToRegion(region);
        mapping.buffer().putInt(0, region);
    }
}
try (var mapping = Mappings.regionMapping(file, OpenMode.READ_ONLY, config)) {
    mapping.moveToRegion(63);
    int value = mapping.buffer().getInt(0);//63
}

The full example is in MappingIntroTest.

Queues

Package org.tools4j.mmap.queue.api contains the queue types, with Queue.open(..) as the main entry point. Configuration is in org.tools4j.mmap.queue.config.

Main classes:

  • Queue: a queue stored in a directory, opened as defined by an OpenMode; creates appenders, pollers and readers, and closes them when it is closed
  • Appender: appends entries from a byte array or buffer, or zero-copy via appending(..) and AppendingContext; multiple appenders can append to the same queue concurrently
  • Poller: sequential retrieval of entries via EntryHandler callback, which returns how to move to the next entry (see Move)
  • EntryReader: random access to entries by index
  • EntryIterator: iteration over entries from a start index, forward, backward or continuously (also returning entries appended later, like a continuous query)
  • IndexReader: first and last index, and number of entries
  • QueueConfig, AppenderConfig, ReaderConfig and IndexReaderConfig: configuration of the queue, appenders and readers; defaults can be set via system properties

Concurrent Access

Appenders, pollers and readers are not thread-safe: each thread needs its own instances. Multiple appenders can append to the same queue concurrently, and pollers and readers can read the queue while entries are being appended.

Quick example

try (Queue queue = Queue.open(new File("/tmp/myQueue"))) {

    //append: copy from a byte array or buffer, or encode in place (zero-copy) and commit
    Appender appender = queue.createAppender();
    appender.append("Hello".getBytes(StandardCharsets.US_ASCII));
    try (AppendingContext context = appender.appending(64)) {
        int length = context.buffer().putStringWithoutLengthAscii(0, "World");
        context.commit(length);
    }

    //poll: sequential retrieval via callback, the returned move determines the next entry
    Poller poller = queue.createPoller();
    while (poller.poll((index, buffer, offset, len) -> {
        //print: 0:Hello, 1:World
        System.out.println(index + ":" + buffer.getStringWithoutLengthAscii(offset, len));
        return Move.NEXT;
    }) > 0);

    //read: random access by index
    EntryReader reader = queue.createEntryReader();
    try (ReadingContext entry = reader.reading(1)) {
        if (entry.hasEntry()) {
            DirectBuffer buffer = entry.buffer();//World
        }
    }

    //iterate: forward, backward or continuously from a start index
    EntryIterator iterator = queue.createEntryIterator();
    try (IterableContext entries = iterator.readingFromLast().backward()) {
        for (Entry entry : entries) {
            DirectBuffer buffer = entry.buffer();//World, Hello
        }
    }

    //index: first and last index, size
    IndexReader indexReader = queue.createIndexReader();
    long first = indexReader.firstIndex();//0
    long last = indexReader.lastIndex();//1
    long size = indexReader.size();//2
}

The full example is in QueueIntroTest.

Benchmarks

The mmap-perf module is a command line benchmark measuring the latency from appending an entry to polling it, with concurrent senders and receivers. It runs the mmap queue or Chronicle Queue through the same harness, so you can compare them on your own machine.

Download the distribution mmap-perf-<version>.zip from Maven Central (next to the jar of org.tools4j:mmap-perf, e.g. mvn dependency:copy -Dartifact=org.tools4j:mmap-perf:<version>:zip -DoutputDirectory=.) and unzip it, or build it (from the project root):

./gradlew :mmap-perf:installDist

This puts mmap-perf.jar and its dependencies into mmap-perf/build/install/mmap-perf/lib (lib in the unzipped distribution); the directory can be copied to another machine. Chronicle Queue is included in the distribution, but optional when using mmap-perf as a dependency. Run it with Java 17 or later:

java -jar mmap-perf/build/install/mmap-perf/lib/mmap-perf.jar --help
java -jar mmap-perf/build/install/mmap-perf/lib/mmap-perf.jar --queue mmap
java -jar mmap-perf/build/install/mmap-perf/lib/mmap-perf.jar --queue chronicle

Alternatively use the start script mmap-perf/build/install/mmap-perf/bin/mmap-perf (mmap-perf.bat on Windows), or ./gradlew :mmap-perf:run --args='--queue mmap'.

By default, one sender sends 11 million messages of 100 bytes at 1 million messages per second to two receivers, and the latency percentiles of each receiver are printed after the first million (warmup) messages. Main options:

Option Description Default
--queue mmap|chronicle queue implementation mmap
--messages <n> messages per sender, including warmup 11m
--warmup <n> warmup messages per sender, not measured 1m
--rate <n> messages per second per sender, 0 for max speed 1m
--length <n> message length in bytes 100
--senders <n> sender threads 1
--receivers <n> receiver threads, each receiving all messages 2
--dir <path> parent directory of the temporary queue files java.io.tmpdir
--pin [<cpus>] pin senders, receivers, then mapper threads to CPUs, one each unpinned; 2-
--strategy <name> mmap mapping strategy, e.g. AsyncMapAheadStrategy per accessor kind
--map-ahead <n> mmap regions to map ahead (async mapping strategies only) 2
--reader poller|iterator mmap only: receivers use a poller or continuous iterator poller

Run --help for all options. Other mmap settings can be passed as system properties (see QueueConfigurations), e.g. java -Dmmap.queue.pollerHeaderRegionCacheSize=16 -jar mmap-perf/build/install/mmap-perf/lib/mmap-perf.jar.

Notes:

  • The Chronicle Queue version used (5.21) runs on Java 17 only; use Java 17 when comparing the two queues.
  • Results depend heavily on the machine: for stable numbers use a quiet machine (or isolated cores), a fast local disk or --dir /dev/shm on Linux, and enough messages for the JIT to warm up.
  • --pin assigns one CPU per thread from the given CPU list (e.g. 2-7 or 2,4,6,8; without a list from CPU 2 up to the highest online CPU, leaving CPUs 0 and 1 to the OS and other processes), first to the senders, then to the receivers, and the remaining CPUs to the mmap mapping runtime threads. Pinning is supported on Linux (x86_64 and aarch64) only; elsewhere a warning is logged and threads run unpinned. Pin to isolated cores for the most stable latencies.

Maven/Gradle

Requires Java 17 or later, and the following JVM options:

--add-opens=java.base/sun.nio.ch=ALL-UNNAMED
--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED

mmap-queue includes mmap-mapping; use mmap-mapping alone if you only need mappings.

Maven

<dependency>
    <groupId>org.tools4j</groupId>
    <artifactId>mmap-queue</artifactId>
    <version>2.0</version>
</dependency>

Gradle

implementation 'org.tools4j:mmap-queue:2.0'

See the changelog for the changes per release.

Download

You can download binaries, sources and javadoc from Maven Central:

API Javadoc

API documentation of the latest release:

Notes

  • Licensed under the MIT License.
  • AI assistance (Claude) was used in developing this repository; all changes are reviewed by the maintainers.

About

Memory mapped files used to implement off-heap low latency utilities such as queues.

Resources

Stars

14 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages