Skip to content

Commit 4f67b34

Browse files
committed
GH-4554 Fix performance of memory overflow
- do not verify additions in Changeset via isEmpty() - proactively close MemoryOverflowModel in Changeset by using AutoClosable interface - use GarbageCollectorMXBean to monitor GC load - do not isolate transactions in LmdbStore when used in MemoryOverflowModel
1 parent 221ab7e commit 4f67b34

File tree

12 files changed

+348
-212
lines changed

12 files changed

+348
-212
lines changed

core/sail/base/src/main/java/org/eclipse/rdf4j/sail/base/Changeset.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,25 @@ public void close() throws SailException {
127127
refbacks = null;
128128
prepend = null;
129129
observed = null;
130-
approved = null;
130+
try {
131+
if (approved instanceof AutoCloseable) {
132+
try {
133+
((AutoCloseable) approved).close();
134+
} catch (Exception e) {
135+
throw new SailException(e);
136+
}
137+
}
138+
approved = null;
139+
} finally {
140+
if (deprecated instanceof AutoCloseable) {
141+
try {
142+
((AutoCloseable) deprecated).close();
143+
} catch (Exception e) {
144+
throw new SailException(e);
145+
}
146+
}
147+
deprecated = null;
148+
}
131149
deprecated = null;
132150
approvedContexts = null;
133151
deprecatedContexts = null;
@@ -416,7 +434,7 @@ public void approve(Statement statement) {
416434
approved = createEmptyModel();
417435
}
418436
approved.add(statement);
419-
approvedEmpty = approved == null || approved.isEmpty();
437+
approvedEmpty = false;
420438
if (statement.getContext() != null) {
421439
if (approvedContexts == null) {
422440
approvedContexts = new HashSet<>();
@@ -447,7 +465,7 @@ public void deprecate(Statement statement) {
447465
deprecated = createEmptyModel();
448466
}
449467
deprecated.add(statement);
450-
deprecatedEmpty = deprecated == null || deprecated.isEmpty();
468+
deprecatedEmpty = false;
451469
Resource ctx = statement.getContext();
452470
if (approvedContexts != null && approvedContexts.contains(ctx)
453471
&& !approved.contains(null, null, null, ctx)) {
@@ -885,7 +903,7 @@ public void approveAll(Set<Statement> approve, Set<Resource> approveContexts) {
885903
approved = createEmptyModel();
886904
}
887905
approved.addAll(approve);
888-
approvedEmpty = approved == null || approved.isEmpty();
906+
approvedEmpty = false;
889907

890908
if (approveContexts != null) {
891909
if (approvedContexts == null) {
@@ -912,7 +930,7 @@ public void deprecateAll(Set<Statement> deprecate) {
912930
deprecated = createEmptyModel();
913931
}
914932
deprecated.addAll(deprecate);
915-
deprecatedEmpty = deprecated == null || deprecated.isEmpty();
933+
deprecatedEmpty = false;
916934

917935
for (Statement statement : deprecate) {
918936
Resource ctx = statement.getContext();

core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/LmdbSailStore.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ CloseableIteration<? extends Statement, SailException> createStatementIterator(
372372
}
373373
}
374374

375+
public void setTransactionIsolation(boolean transactionIsolation) {
376+
this.tripleStore.setTransactionIsolation(transactionIsolation);
377+
}
378+
375379
private final class LmdbSailSource extends BackingSailSource {
376380

377381
private final boolean explicit;

core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/LmdbStore.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,11 +250,15 @@ protected void initializeInternal() throws SailException {
250250
FileUtils.writeStringToFile(versionFile, VERSION, StandardCharsets.UTF_8);
251251
}
252252
backingStore = new LmdbSailStore(dataDir, config);
253-
this.store = new SnapshotSailStore(backingStore, () -> new MemoryOverflowModel() {
253+
this.store = new SnapshotSailStore(backingStore, () -> new MemoryOverflowModel(false) {
254254
@Override
255255
protected SailStore createSailStore(File dataDir) throws IOException, SailException {
256256
// Model can't fit into memory, use another LmdbSailStore to store delta
257-
return new LmdbSailStore(dataDir, config);
257+
LmdbStoreConfig overflowConfig = new LmdbStoreConfig();
258+
LmdbSailStore store = new LmdbSailStore(dataDir, overflowConfig);
259+
// does not need to isolate transactions and therefore can optimize autogrow and others
260+
store.setTransactionIsolation(false);
261+
return store;
258262
}
259263
}) {
260264

core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/MemoryOverflowModel.java

Lines changed: 88 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
import java.io.IOException;
1515
import java.io.ObjectInputStream;
1616
import java.io.ObjectOutputStream;
17+
import java.lang.management.GarbageCollectorMXBean;
18+
import java.lang.management.ManagementFactory;
1719
import java.nio.file.Files;
18-
import java.util.Collection;
1920
import java.util.Iterator;
21+
import java.util.List;
22+
import java.util.Map;
2023
import java.util.Optional;
2124
import java.util.Set;
25+
import java.util.concurrent.ConcurrentHashMap;
2226

2327
import org.eclipse.rdf4j.common.io.FileUtil;
2428
import org.eclipse.rdf4j.model.IRI;
@@ -41,16 +45,16 @@
4145
* estimated memory usage is more than the amount of free memory available. Once the threshold is cross this
4246
* implementation seamlessly changes to a disk based {@link SailSourceModel}.
4347
*/
44-
abstract class MemoryOverflowModel extends AbstractModel {
48+
abstract class MemoryOverflowModel extends AbstractModel implements AutoCloseable {
4549

4650
private static final long serialVersionUID = 4119844228099208169L;
4751

4852
private static final Runtime RUNTIME = Runtime.getRuntime();
4953

5054
private static final int LARGE_BLOCK = 10000;
5155

52-
// To reduce the chance of OOM we will always overflow once we get close to running out of memory even if we think
53-
// we have space for one more block. The limit is currently set at 32 MB
56+
// To reduce the chance of OOM we will always overflow once we get close to running out of memory.
57+
// The limit is currently set at 32 MB
5458
private static final int MIN_AVAILABLE_MEM_BEFORE_OVERFLOWING = 32 * 1024 * 1024;
5559

5660
final Logger logger = LoggerFactory.getLogger(MemoryOverflowModel.class);
@@ -63,27 +67,17 @@ abstract class MemoryOverflowModel extends AbstractModel {
6367

6468
transient SailSourceModel disk;
6569

66-
private long baseline = 0;
70+
private final boolean verifyAdditions;
6771

68-
private long maxBlockSize = 0;
72+
private final SimpleValueFactory vf = SimpleValueFactory.getInstance();
6973

70-
SimpleValueFactory vf = SimpleValueFactory.getInstance();
71-
72-
public MemoryOverflowModel() {
74+
public MemoryOverflowModel(boolean verifyAdditions) {
75+
this.verifyAdditions = verifyAdditions;
7376
memory = new LinkedHashModel(LARGE_BLOCK);
7477
}
7578

76-
public MemoryOverflowModel(Model model) {
77-
this(model.getNamespaces());
78-
addAll(model);
79-
}
80-
81-
public MemoryOverflowModel(Set<Namespace> namespaces, Collection<? extends Statement> c) {
82-
this(namespaces);
83-
addAll(c);
84-
}
85-
86-
public MemoryOverflowModel(Set<Namespace> namespaces) {
79+
public MemoryOverflowModel(Set<Namespace> namespaces, boolean verifyAdditions) {
80+
this.verifyAdditions = verifyAdditions;
8781
memory = new LinkedHashModel(namespaces, LARGE_BLOCK);
8882
}
8983

@@ -227,40 +221,71 @@ private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundEx
227221
}
228222
}
229223

224+
static class GcInfo {
225+
long count;
226+
long time;
227+
}
228+
229+
private final Map<String, GcInfo> prevGcInfo = new ConcurrentHashMap<>();
230+
231+
private synchronized boolean highGcLoad() {
232+
boolean highLoad = false;
233+
234+
// get all garbage collector MXBeans.
235+
List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
236+
for (GarbageCollectorMXBean gcBean : gcBeans) {
237+
long count = gcBean.getCollectionCount();
238+
long time = gcBean.getCollectionTime();
239+
240+
GcInfo prevInfo = prevGcInfo.get(gcBean.getName());
241+
if (prevInfo != null) {
242+
long countDiff = count - prevInfo.count;
243+
long timeDiff = time - prevInfo.time;
244+
if (countDiff != 0) {
245+
double gcLoad = (double) timeDiff / countDiff;
246+
// TODO find good threshold
247+
if (gcLoad > 30) {
248+
highLoad = true;
249+
}
250+
}
251+
} else {
252+
prevInfo = new GcInfo();
253+
prevGcInfo.put(gcBean.getName(), prevInfo);
254+
}
255+
prevInfo.count = count;
256+
prevInfo.time = time;
257+
}
258+
return highLoad;
259+
}
260+
230261
private synchronized void checkMemoryOverflow() {
231262
if (disk == null) {
232263
int size = size();
233264
if (size >= LARGE_BLOCK && size % LARGE_BLOCK == 0) {
234-
// maximum heap size the JVM can allocate
235-
long maxMemory = RUNTIME.maxMemory();
265+
boolean overflow = highGcLoad();
266+
if (!overflow) {
267+
// maximum heap size the JVM can allocate
268+
long maxMemory = RUNTIME.maxMemory();
236269

237-
// total currently allocated JVM memory
238-
long totalMemory = RUNTIME.totalMemory();
270+
// total currently allocated JVM memory
271+
long totalMemory = RUNTIME.totalMemory();
239272

240-
// amount of memory free in the currently allocated JVM memory
241-
long freeMemory = RUNTIME.freeMemory();
273+
// amount of memory free in the currently allocated JVM memory
274+
long freeMemory = RUNTIME.freeMemory();
242275

243-
// estimated memory used
244-
long used = totalMemory - freeMemory;
276+
// estimated memory used
277+
long used = totalMemory - freeMemory;
245278

246-
// amount of memory the JVM can still allocate from the OS (upper boundary is the max heap)
247-
long freeToAllocateMemory = maxMemory - used;
279+
// amount of memory the JVM can still allocate from the OS (upper boundary is the max heap)
280+
long freeToAllocateMemory = maxMemory - used;
248281

249-
if (baseline > 0) {
250-
long blockSize = used - baseline;
251-
if (blockSize > maxBlockSize) {
252-
maxBlockSize = blockSize;
253-
}
254-
255-
// Sync if either the estimated size of the next block is larger than remaining memory, or
256-
// if less than 15% of the heap is still free (this last condition to avoid GC overhead limit)
257-
if (freeToAllocateMemory < MIN_AVAILABLE_MEM_BEFORE_OVERFLOWING ||
258-
freeToAllocateMemory < Math.min(0.15 * maxMemory, maxBlockSize)) {
259-
logger.debug("syncing at {} triples. max block size: {}", size, maxBlockSize);
260-
overflowToDisk();
261-
}
282+
// try to prevent OOM
283+
overflow = freeToAllocateMemory < MIN_AVAILABLE_MEM_BEFORE_OVERFLOWING;
284+
}
285+
if (overflow) {
286+
logger.debug("syncing at {} triples.", size);
287+
overflowToDisk();
262288
}
263-
baseline = used;
264289
}
265290
}
266291
}
@@ -271,26 +296,7 @@ private synchronized void overflowToDisk() {
271296
dataDir = Files.createTempDirectory("model").toFile();
272297
logger.debug("memory overflow using temp directory {}", dataDir);
273298
store = createSailStore(dataDir);
274-
disk = new SailSourceModel(store) {
275-
276-
@Override
277-
protected void finalize() throws Throwable {
278-
logger.debug("finalizing {}", dataDir);
279-
if (disk == this) {
280-
try {
281-
store.close();
282-
} catch (SailException e) {
283-
logger.error(e.toString(), e);
284-
} finally {
285-
FileUtil.deleteDir(dataDir);
286-
dataDir = null;
287-
store = null;
288-
disk = null;
289-
}
290-
}
291-
super.finalize();
292-
}
293-
};
299+
disk = new SailSourceModel(store, verifyAdditions);
294300
disk.addAll(memory);
295301
memory = new LinkedHashModel(memory.getNamespaces(), LARGE_BLOCK);
296302
logger.debug("overflow synced to disk");
@@ -299,4 +305,22 @@ protected void finalize() throws Throwable {
299305
logger.error("Error while writing to overflow directory " + path, e);
300306
}
301307
}
308+
309+
@Override
310+
public void close() throws IOException {
311+
if (disk != null) {
312+
logger.debug("closing {}", dataDir);
313+
disk.close();
314+
try {
315+
store.close();
316+
} catch (SailException e) {
317+
logger.error(e.toString(), e);
318+
} finally {
319+
FileUtil.deleteDir(dataDir);
320+
dataDir = null;
321+
store = null;
322+
disk = null;
323+
}
324+
}
325+
}
302326
}

0 commit comments

Comments
 (0)