Skip to content

Commit 112ac90

Browse files
committed
implement #147: memory mapped file backed allocator
1 parent 0fd8452 commit 112ac90

File tree

12 files changed

+518
-38
lines changed

12 files changed

+518
-38
lines changed

compiler/src/test/java/org/capnproto/test/EncodingTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public void testAllTypes() {
2828

2929
@Test
3030
public void testAllTypesMultiSegment() {
31-
MessageBuilder message = new MessageBuilder(5, BuilderArena.AllocationStrategy.FIXED_SIZE);
31+
MessageBuilder message = new MessageBuilder(5, Allocator.AllocationStrategy.FIXED_SIZE);
3232
org.capnproto.test.Test.TestAllTypes.Builder allTypes = message.initRoot(org.capnproto.test.Test.TestAllTypes.factory);
3333
TestUtil.initTestMessage(allTypes);
3434

runtime/pom.xml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
</developers>
3232
<properties>
3333
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
34-
<maven.compiler.source>1.8</maven.compiler.source>
35-
<maven.compiler.target>1.8</maven.compiler.target>
34+
<maven.compiler.source>9</maven.compiler.source>
35+
<maven.compiler.target>9</maven.compiler.target>
3636
</properties>
3737
<dependencies>
3838
<dependency>
@@ -59,9 +59,11 @@
5959
<plugin>
6060
<groupId>org.apache.maven.plugins</groupId>
6161
<artifactId>maven-compiler-plugin</artifactId>
62-
<version>3.6.2</version>
62+
<version>3.11.0</version>
6363
<configuration>
6464
<compilerArgument>-Xlint:unchecked</compilerArgument>
65+
<source>9</source>
66+
<target>9</target>
6567
</configuration>
6668
</plugin>
6769
</plugins>
@@ -71,10 +73,10 @@
7173
<profile>
7274
<id>jdk9FF</id>
7375
<activation>
74-
<jdk>(1.8,)</jdk>
76+
<jdk>(9,)</jdk>
7577
</activation>
7678
<properties>
77-
<maven.compiler.release>8</maven.compiler.release>
79+
<maven.compiler.release>9</maven.compiler.release>
7880
</properties>
7981
</profile>
8082
<profile>

runtime/src/main/java/org/capnproto/Allocator.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@
44
* An object that allocates memory for a Cap'n Proto message as it is being built.
55
*/
66
public interface Allocator {
7+
public enum AllocationStrategy {
8+
FIXED_SIZE,
9+
GROW_HEURISTICALLY
10+
}
711
/**
812
* Allocates a ByteBuffer to be used as a segment in a message. The returned
913
* buffer must contain at least `minimumSize` bytes, all of which MUST be
1014
* set to zero.
1115
*/
1216
public java.nio.ByteBuffer allocateSegment(int minimumSize);
17+
18+
/**
19+
* set the size for the next buffer allocation in bytes
20+
*/
21+
public void setNextAllocationSizeBytes(int nextSize);
1322
}

runtime/src/main/java/org/capnproto/BuilderArena.java

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,38 @@
2424
import java.nio.ByteBuffer;
2525
import java.nio.ByteOrder;
2626
import java.util.ArrayList;
27+
import org.capnproto.Allocator.AllocationStrategy;
2728

2829
public final class BuilderArena implements Arena {
29-
public enum AllocationStrategy {
30-
FIXED_SIZE,
31-
GROW_HEURISTICALLY
30+
// allocator to use, default BYTE_BUFFER is faster but is limited by
31+
// available process memory limit
32+
public enum AllocatorType {
33+
BYTE_BUFFER,
34+
MEMORY_MAPPED
3235
}
3336

3437
public static final int SUGGESTED_FIRST_SEGMENT_WORDS = 1024;
3538
public static final AllocationStrategy SUGGESTED_ALLOCATION_STRATEGY =
3639
AllocationStrategy.GROW_HEURISTICALLY;
3740

41+
public static final AllocatorType SUGGESTED_ALLOCATOR_TYPE =
42+
AllocatorType.BYTE_BUFFER;
43+
3844
public final ArrayList<SegmentBuilder> segments;
3945
private final Allocator allocator;
4046

41-
public BuilderArena(int firstSegmentSizeWords, AllocationStrategy allocationStrategy) {
47+
// public BuilderArena(int firstSegmentSizeWords, AllocationStrategy allocationStrategy) {
48+
// this.segments = new ArrayList<SegmentBuilder>();
49+
// this.allocator = new DefaultAllocator(allocationStrategy);
50+
// this.allocator.setNextAllocationSizeBytes(
51+
// firstSegmentSizeWords * Constants.BYTES_PER_WORD);
52+
// }
53+
54+
public BuilderArena(int firstSegmentSizeWords, AllocationStrategy allocationStrategy, AllocatorType allocatorType) {
4255
this.segments = new ArrayList<SegmentBuilder>();
43-
{
44-
DefaultAllocator allocator = new DefaultAllocator(allocationStrategy);
45-
allocator.setNextAllocationSizeBytes(firstSegmentSizeWords * Constants.BYTES_PER_WORD);
46-
this.allocator = allocator;
47-
}
56+
this.allocator = createAllocator(allocatorType, allocationStrategy);
57+
this.allocator.setNextAllocationSizeBytes(
58+
firstSegmentSizeWords * Constants.BYTES_PER_WORD);
4859
}
4960

5061
public BuilderArena(Allocator allocator) {
@@ -77,15 +88,12 @@ public BuilderArena(Allocator allocator, ByteBuffer firstSegment) {
7788
segmentBuilder.id = ii;
7889
segmentBuilder.pos = segmentBuilder.capacity(); // buffer is pre-filled
7990
segments.add(segmentBuilder);
80-
8191
// Find the largest segment for the allocation strategy.
8292
largestSegment = Math.max(largestSegment, segment.buffer.capacity());
8393
}
84-
DefaultAllocator defaultAllocator = new DefaultAllocator(SUGGESTED_ALLOCATION_STRATEGY);
85-
86-
// Use largest segment as next size.
87-
defaultAllocator.setNextAllocationSizeBytes(largestSegment);
88-
this.allocator = defaultAllocator;
94+
AllocatorType allocatorType = suggestAllocator(largestSegment);
95+
this.allocator = createAllocator(allocatorType, SUGGESTED_ALLOCATION_STRATEGY);
96+
this.allocator.setNextAllocationSizeBytes(largestSegment);
8997
}
9098

9199
@Override
@@ -152,4 +160,31 @@ public final ByteBuffer[] getSegmentsForOutput() {
152160
}
153161
return result;
154162
}
163+
164+
private static Allocator createAllocator(AllocatorType allocatorType, AllocationStrategy allocationStrategy) {
165+
switch (allocatorType) {
166+
case BYTE_BUFFER:
167+
DefaultAllocator allocator1 = new DefaultAllocator(allocationStrategy);
168+
return allocator1;
169+
170+
case MEMORY_MAPPED:
171+
MemoryMappedAllocator allocator2 = new MemoryMappedAllocator(
172+
"capnp_mmf", allocationStrategy);
173+
return allocator2;
174+
default:
175+
throw new AssertionError(
176+
"Allocator must be BYTE_BUFFER or MEMORY_MAPPED");
177+
}
178+
}
179+
180+
public static AllocatorType suggestAllocator(int sizeToAllocate) {
181+
Runtime runtime = Runtime.getRuntime();
182+
long freeMemory = runtime.freeMemory(); // Free memory in bytes
183+
long totalMemory = runtime.totalMemory(); // Total memory in JVM
184+
long maxMemory = runtime.maxMemory(); // Max memory the JVM will attempt to use
185+
if (freeMemory - sizeToAllocate < 0)
186+
return AllocatorType.MEMORY_MAPPED;
187+
188+
return AllocatorType.BYTE_BUFFER;
189+
};
155190
}

runtime/src/main/java/org/capnproto/DefaultAllocator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.nio.ByteBuffer;
44

5-
import org.capnproto.BuilderArena.AllocationStrategy;
65

76
public class DefaultAllocator implements Allocator {
87

@@ -45,6 +44,7 @@ public DefaultAllocator(AllocationStrategy allocationStrategy,
4544
this.allocationStyle = style;
4645
}
4746

47+
@Override
4848
public void setNextAllocationSizeBytes(int nextSize) {
4949
this.nextSize = nextSize;
5050
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
2+
// Licensed under the MIT License:
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy
5+
// of this software and associated documentation files (the "Software"), to deal
6+
// in the Software without restriction, including without limitation the rights
7+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the Software is
9+
// furnished to do so, subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in
12+
// all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
// THE SOFTWARE.
21+
22+
package org.capnproto;
23+
24+
import java.io.File;
25+
import java.io.IOException;
26+
import java.io.RandomAccessFile;
27+
import java.lang.ref.Cleaner;
28+
import java.nio.MappedByteBuffer;
29+
import java.nio.channels.FileChannel;
30+
import java.util.Collections;
31+
import java.util.HashMap;
32+
import java.util.Map;
33+
import java.util.Random;
34+
35+
36+
public class MemoryMappedAllocator implements Allocator {
37+
// cleaner for file cleanup when this is GCed
38+
private static final Cleaner cleaner = Cleaner.create();
39+
private final Cleaner.Cleanable cleanable;
40+
41+
// the length of the random prefix part of the random filename string
42+
private final int PREFIX_LENGTH = 5;
43+
// the used charset for creating random filenames
44+
private static final String CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
45+
46+
// (minimum) number of bytes in the next allocation
47+
private int nextSize = BuilderArena.SUGGESTED_FIRST_SEGMENT_WORDS;
48+
// the maximum allocateable size
49+
public int maxSegmentBytes = Integer.MAX_VALUE - 2;
50+
51+
// the memory mapped file buffer name prefix
52+
private final String rPrefix;
53+
54+
// the allocation strategy with which the allocation size grows
55+
public AllocationStrategy allocationStrategy =
56+
AllocationStrategy.GROW_HEURISTICALLY;
57+
58+
// hashmaps used for keeping track of files
59+
private final Map<Integer, RandomAccessFile> randomAccFiles = Collections.synchronizedMap(new HashMap<>());
60+
private final Map<Integer, FileChannel> channelMap = Collections.synchronizedMap(new HashMap<>());
61+
62+
public MemoryMappedAllocator(String baseFileName) {
63+
// create random file name with baseFileNamePrefix:
64+
// ${baseFileName}_XXXXX_000001
65+
Random random = new Random();
66+
StringBuilder sb = new StringBuilder(PREFIX_LENGTH);
67+
for (int i = 0; i < PREFIX_LENGTH; i++) {
68+
int index = random.nextInt(CHARSET.length());
69+
sb.append(CHARSET.charAt(index));
70+
}
71+
rPrefix = baseFileName + "_" + sb.toString();
72+
this.cleanable = cleaner.register(this, new State(this.randomAccFiles, rPrefix));
73+
}
74+
75+
76+
public MemoryMappedAllocator(String baseFileName, AllocationStrategy allocationStrategy) {
77+
// create random file name with baseFileNamePrefix:
78+
// ${baseFileName}_XXXXX_000001
79+
Random random = new Random();
80+
StringBuilder sb = new StringBuilder(PREFIX_LENGTH);
81+
for (int i = 0; i < PREFIX_LENGTH; i++) {
82+
int index = random.nextInt(CHARSET.length());
83+
sb.append(CHARSET.charAt(index));
84+
}
85+
rPrefix = baseFileName + "_" + sb.toString();
86+
this.cleanable = cleaner.register(this, new State(this.randomAccFiles, rPrefix));
87+
this.allocationStrategy = allocationStrategy;
88+
}
89+
90+
91+
private static String nameForInt(String prefix, int key) {
92+
String ret = prefix + "_" + String.format("%05d", key);
93+
return ret;
94+
}
95+
96+
97+
private Integer generateFile() throws IOException {
98+
int fCount;
99+
synchronized (randomAccFiles) {
100+
fCount = randomAccFiles.size();
101+
String newFileName = nameForInt(rPrefix, fCount);
102+
RandomAccessFile newFile = new RandomAccessFile(newFileName, "rw");
103+
randomAccFiles.put(fCount, newFile);
104+
File test = new File(newFileName);
105+
test.deleteOnExit();
106+
}
107+
return fCount;
108+
}
109+
110+
111+
/**
112+
* set the grow size of the memory mapped file
113+
*/
114+
@Override
115+
public void setNextAllocationSizeBytes(int nextSize) {
116+
this.nextSize = nextSize;
117+
}
118+
119+
120+
private FileChannel createSegment(int segmentSize) throws IOException {
121+
int fileKey = generateFile();
122+
FileChannel channel = null;
123+
synchronized (channelMap) {
124+
if (!channelMap.containsKey(fileKey)) {
125+
synchronized (randomAccFiles) {
126+
RandomAccessFile file = randomAccFiles.get(fileKey);
127+
file.setLength(segmentSize);
128+
channel = file.getChannel();
129+
channelMap.put(fileKey, channel);
130+
}
131+
}
132+
}
133+
return channel;
134+
}
135+
136+
137+
@Override
138+
public java.nio.ByteBuffer allocateSegment(int minimumSize) {
139+
int size = Math.max(minimumSize, this.nextSize);
140+
MappedByteBuffer result = null;
141+
try {
142+
FileChannel channel = createSegment(size);
143+
result = channel.map(FileChannel.MapMode.READ_WRITE, 0, size);
144+
}
145+
catch (IOException e)
146+
{
147+
System.err.println("IOException: allocateSegment failed with:" + e);
148+
}
149+
150+
151+
switch (this.allocationStrategy) {
152+
case GROW_HEURISTICALLY:
153+
if (size < this.maxSegmentBytes - this.nextSize) {
154+
this.nextSize += size;
155+
} else {
156+
this.nextSize = maxSegmentBytes;
157+
}
158+
break;
159+
case FIXED_SIZE:
160+
break;
161+
}
162+
163+
164+
165+
// if (size < this.maxSegmentBytes - this.nextSize) {
166+
// this.nextSize += size;
167+
// } else {
168+
// this.nextSize = maxSegmentBytes;
169+
// }
170+
return result;
171+
}
172+
173+
174+
private static class State implements Runnable {
175+
private final Map<Integer, RandomAccessFile> randomAccFiles;
176+
private final String rPrefix;
177+
178+
State(Map<Integer, RandomAccessFile> files, String rPrefix) {
179+
this.randomAccFiles = files;
180+
this.rPrefix = rPrefix;
181+
}
182+
183+
@Override
184+
public void run() {
185+
// Cleanup logic: delete all files
186+
for (Map.Entry<Integer,RandomAccessFile> entry : randomAccFiles.entrySet()) {
187+
try {
188+
entry.getValue().close();
189+
}
190+
catch (IOException e)
191+
{
192+
}
193+
String name = nameForInt(rPrefix, entry.getKey());
194+
File file = new File(name);
195+
file.delete();
196+
}
197+
}
198+
}
199+
200+
/**
201+
* Explicit cleanup: WARNING: this invalidates all alloceted buffers,
202+
* use close() after using the ByteBuffers.
203+
*/
204+
public void close() {
205+
cleanable.clean();
206+
}
207+
}

0 commit comments

Comments
 (0)