MVStore backed storage option for the dependency graph

GitOrigin-RevId: cb46daccff31af708046687e31c1a4203ced51be
This commit is contained in:
Eugene Zhuravlev
2025-04-26 20:37:14 +02:00
committed by intellij-monorepo-bot
parent a7a6116df7
commit 095f69b36c
15 changed files with 960 additions and 163 deletions

View File

@@ -1940,6 +1940,7 @@ org.jetbrains.jps.dependency.Delta
- a:isSourceOnly():Z
org.jetbrains.jps.dependency.DependencyGraph
- java.io.Closeable
- java.io.Flushable
- org.jetbrains.jps.dependency.Graph
- a:createDelta(java.lang.Iterable,java.lang.Iterable,Z):org.jetbrains.jps.dependency.Delta
- differentiate(org.jetbrains.jps.dependency.Delta,org.jetbrains.jps.dependency.DifferentiateParameters):org.jetbrains.jps.dependency.DifferentiateResult
@@ -1981,8 +1982,9 @@ org.jetbrains.jps.dependency.ExternalizableGraphElement
org.jetbrains.jps.dependency.Externalizer
- org.jetbrains.jps.dependency.DataReader
- org.jetbrains.jps.dependency.DataWriter
- s:forAnyGraphElement():org.jetbrains.jps.dependency.Externalizer
- s:forGraphElement(org.jetbrains.jps.dependency.DataReader):org.jetbrains.jps.dependency.Externalizer
- a:createStorage(I):java.lang.Object[]
- s:forAnyGraphElement(java.util.function.Function):org.jetbrains.jps.dependency.Externalizer
- s:forGraphElement(org.jetbrains.jps.dependency.DataReader,java.util.function.Function):org.jetbrains.jps.dependency.Externalizer
org.jetbrains.jps.dependency.FactoredExternalizableGraphElement
- org.jetbrains.jps.dependency.ExternalizableGraphElement
- a:getFactorData():org.jetbrains.jps.dependency.ExternalizableGraphElement

View File

@@ -4,12 +4,13 @@ package org.jetbrains.jps.dependency;
import org.jetbrains.annotations.NotNull;
import java.io.Closeable;
import java.io.Flushable;
import java.util.List;
/**
* A representation of the main dependency storage
*/
public interface DependencyGraph extends Graph, Closeable {
public interface DependencyGraph extends Graph, Closeable, Flushable {
Delta createDelta(Iterable<NodeSource> sourcesToProcess, Iterable<NodeSource> deletedSources, boolean isSourceOnly);

View File

@@ -1,12 +1,19 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency;
import java.io.IOException;
import java.util.function.Function;
public interface Externalizer<T> extends DataReader<T>, DataWriter<T> {
T[] createStorage(int size);
static <T extends ExternalizableGraphElement> Externalizer<T> forGraphElement(DataReader<? extends T> reader) {
static <T extends ExternalizableGraphElement> Externalizer<T> forGraphElement(DataReader<? extends T> reader, Function<Integer, T[]> arrayFactory) {
return new Externalizer<>() {
@Override
public T[] createStorage(int size) {
return arrayFactory.apply(size);
}
@Override
public T load(GraphDataInput in) throws IOException {
return reader.load(in);
@@ -19,8 +26,13 @@ public interface Externalizer<T> extends DataReader<T>, DataWriter<T> {
};
}
static <T extends ExternalizableGraphElement> Externalizer<T> forAnyGraphElement() {
static <T extends ExternalizableGraphElement> Externalizer<T> forAnyGraphElement(Function<Integer, T[]> arrayFactory) {
return new Externalizer<>() {
@Override
public T[] createStorage(int size) {
return arrayFactory.apply(size);
}
@Override
public T load(GraphDataInput in) throws IOException {
return in.readGraphElement();

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency.impl;
import org.jetbrains.annotations.NotNull;
@@ -18,7 +18,7 @@ public abstract class BackDependencyIndexImpl implements BackDependencyIndex {
protected BackDependencyIndexImpl(@NotNull String name, @NotNull MapletFactory cFactory) {
myName = name;
// important: if multiple implementations of ReferenceID are available, change to createMultitypeExternalizer
Externalizer<ReferenceID> ext = Externalizer.forGraphElement(JvmNodeReferenceID::new);
Externalizer<ReferenceID> ext = Externalizer.forGraphElement(JvmNodeReferenceID::new, JvmNodeReferenceID[]::new);
myMap = cFactory.createSetMultiMaplet(name, ext, ext);
}

View File

@@ -0,0 +1,106 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency.impl;
import org.h2.mvstore.DataUtils;
import org.jetbrains.annotations.NotNull;
import java.io.DataInput;
import java.io.EOFException;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
public class ByteBufferDataInput implements DataInput {
private final ByteBuffer myBuf;
public ByteBufferDataInput(ByteBuffer buf) {
myBuf = buf;
}
@Override
public void readFully(@NotNull byte[] b) throws IOException {
try {
myBuf.get(b, 0, b.length);
}
catch (BufferUnderflowException e) {
throw new EOFException(e.getMessage());
}
}
@Override
public void readFully(@NotNull byte[] b, int off, int len) throws IOException {
try {
myBuf.get(b, off, len);
}
catch (BufferUnderflowException e) {
throw new EOFException(e.getMessage());
}
}
@Override
public int skipBytes(int n) throws IOException {
int skip = Math.min(n, myBuf.remaining());
myBuf.position(myBuf.position() + skip);
return skip;
}
@Override
public boolean readBoolean() throws IOException {
return myBuf.get() != 0;
}
@Override
public byte readByte() throws IOException {
return myBuf.get();
}
@Override
public int readUnsignedByte() throws IOException {
return ((int)readByte()) & 0xFF;
}
@Override
public short readShort() throws IOException {
return myBuf.getShort();
}
@Override
public int readUnsignedShort() throws IOException {
return ((int)readShort()) & 0xFF;
}
@Override
public char readChar() throws IOException {
return myBuf.getChar();
}
@Override
public int readInt() throws IOException {
return myBuf.getInt();
}
@Override
public long readLong() throws IOException {
return myBuf.getLong();
}
@Override
public float readFloat() throws IOException {
return myBuf.getFloat();
}
@Override
public double readDouble() throws IOException {
return myBuf.getDouble();
}
@Override
public String readLine() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public @NotNull String readUTF() throws IOException {
return DataUtils.readString(myBuf);
}
}

View File

@@ -1,34 +1,28 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency.impl;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.intellij.openapi.util.LowMemoryWatcher;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.SystemProperties;
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.KeyDescriptor;
import com.intellij.util.io.PersistentStringEnumerator;
import com.intellij.util.io.StorageLockContext;
import it.unimi.dsi.fastutil.Hash;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.api.GlobalOptions;
import org.jetbrains.jps.builders.storage.BuildDataCorruptedException;
import org.jetbrains.jps.dependency.*;
import org.jetbrains.jps.javac.Iterators;
import org.jetbrains.jps.dependency.Externalizer;
import org.jetbrains.jps.dependency.Maplet;
import org.jetbrains.jps.dependency.MapletFactory;
import org.jetbrains.jps.dependency.MultiMaplet;
import java.io.*;
import java.nio.file.Path;
import java.util.*;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
public final class Containers {
public static MapletFactory createPersistentContainerFactory(String rootDirPath) throws IOException {
//return new PersistentMVStoreMapletFactory(Path.of(rootDirPath).resolve("dep-graph.mv").toString(), IncProjectBuilder.MAX_BUILDER_THREADS);
return new PersistentMapletFactory(rootDirPath);
}
@@ -70,139 +64,4 @@ public final class Containers {
};
}
private static final class PersistentMapletFactory implements MapletFactory, Closeable {
private static final int BASE_CACHE_SIZE = 512 * (SystemProperties.getBooleanProperty(GlobalOptions.COMPILE_PARALLEL_OPTION, false)? 2 : 1);
private final String myRootDirPath;
private final PersistentStringEnumerator myStringTable;
private final List<BaseMaplet<?>> myMaps = new ArrayList<>();
private final Enumerator myEnumerator;
private final Function<Object, Object> myDataInterner;
private final LoadingCache<Object, Object> myInternerCache;
private final LowMemoryWatcher myMemWatcher;
private final int myCacheSize;
PersistentMapletFactory(String rootDirPath) throws IOException {
myRootDirPath = rootDirPath;
// Important: The enumerator will be called from PHM data externalizers. A PHM acquires the page_cache lock before externalizing => this enumerator should use a separate StorageLockContext to avoid deadlocks
myStringTable = new PersistentStringEnumerator(getMapFile("string-table"), 1024 * 4, true, new StorageLockContext());
myEnumerator = new Enumerator() {
@Override
public String toString(int num) throws IOException {
return myStringTable.valueOf(num);
}
@Override
public int toNumber(String str) throws IOException {
return myStringTable.enumerate(str);
}
};
final int maxGb = (int)(Runtime.getRuntime().maxMemory() / 1_073_741_824L);
myCacheSize = BASE_CACHE_SIZE * Math.min(Math.max(1, maxGb), 5); // increase by BASE_CACHE_SIZE for every additional Gb
myInternerCache = Caffeine.newBuilder().maximumSize(myCacheSize).build(key -> key);
myDataInterner = elem -> elem instanceof Usage? myInternerCache.get(elem) : elem;
myMemWatcher = LowMemoryWatcher.register(() -> {
myInternerCache.invalidateAll();
myStringTable.force();
for (BaseMaplet<?> map : myMaps) {
try {
map.flush();
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
@Override
public <K, V> MultiMaplet<K, V> createSetMultiMaplet(String storageName, Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer) {
MultiMaplet<K, V> container = new CachingMultiMaplet<>(
new PersistentMultiMaplet<>(getMapFile(storageName), new GraphKeyDescriptor<>(keyExternalizer, myEnumerator), new GraphDataExternalizer<>(valueExternalizer, myEnumerator, myDataInterner), () -> (Set<V>)new HashSet<V>()),
myCacheSize
);
myMaps.add(container);
return container;
}
@Override
public <K, V> Maplet<K, V> createMaplet(String storageName, Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer) {
Maplet<K, V> container = new CachingMaplet<>(
new PersistentMaplet<>(getMapFile(storageName), new GraphKeyDescriptor<>(keyExternalizer, myEnumerator), new GraphDataExternalizer<>(valueExternalizer, myEnumerator, myDataInterner)),
myCacheSize
);
myMaps.add(container);
return container;
}
@Override
public void close() {
myMemWatcher.stop();
Throwable ex = null;
for (Closeable container : Iterators.flat(myMaps, Iterators.asIterable(myStringTable))) {
try {
container.close();
}
catch (Throwable e) {
if (ex == null) {
ex = e;
}
}
}
myMaps.clear();
myInternerCache.invalidateAll();
if (ex instanceof IOException) {
throw new BuildDataCorruptedException((IOException)ex);
}
else if (ex != null) {
throw new RuntimeException(ex);
}
}
private Path getMapFile(final String name) {
final File file = new File(myRootDirPath, name);
FileUtil.createIfDoesntExist(file);
return file.toPath();
}
}
private static class GraphDataExternalizer<T> implements DataExternalizer<T> {
private final Externalizer<T> myExternalizer;
private final @Nullable Enumerator myEnumerator;
private final @Nullable Function<Object, Object> myObjectInterner;
GraphDataExternalizer(Externalizer<T> externalizer, @Nullable Enumerator enumerator, @Nullable Function<Object, Object> objectInterner) {
myExternalizer = externalizer;
myEnumerator = enumerator;
myObjectInterner = objectInterner;
}
@Override
public void save(@NotNull DataOutput out, T value) throws IOException {
myExternalizer.save(GraphDataOutputImpl.wrap(out, myEnumerator), value);
}
@Override
public T read(@NotNull DataInput in) throws IOException {
return myExternalizer.load(GraphDataInputImpl.wrap(in, myEnumerator, myObjectInterner));
}
}
private static final class GraphKeyDescriptor<T> extends GraphDataExternalizer<T> implements KeyDescriptor<T> {
GraphKeyDescriptor(Externalizer<T> externalizer, @Nullable Enumerator enumerator) {
super(externalizer, enumerator, null);
}
@Override
public boolean isEqual(T val1, T val2) {
return val1.equals(val2);
}
@Override
public int getHashCode(T value) {
return value.hashCode();
}
}
}

View File

@@ -8,6 +8,7 @@ import org.jetbrains.jps.dependency.*;
import org.jetbrains.jps.dependency.java.JvmNodeReferenceID;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -27,9 +28,9 @@ public abstract class GraphImpl implements Graph {
addIndex(myDependencyIndex = new NodeDependenciesIndex(cFactory));
// important: if multiple implementations of NodeSource are available, change to generic graph element externalizer
Externalizer<NodeSource> srcExternalizer = Externalizer.forGraphElement(PathSource::new);
myNodeToSourcesMap = cFactory.createSetMultiMaplet("node-sources-map", Externalizer.forGraphElement(JvmNodeReferenceID::new), srcExternalizer);
mySourceToNodesMap = cFactory.createSetMultiMaplet("source-nodes-map", srcExternalizer, Externalizer.forAnyGraphElement());
Externalizer<NodeSource> srcExternalizer = Externalizer.forGraphElement(PathSource::new, NodeSource[]::new);
myNodeToSourcesMap = cFactory.createSetMultiMaplet("node-sources-map", Externalizer.forGraphElement(JvmNodeReferenceID::new, JvmNodeReferenceID[]::new), srcExternalizer);
mySourceToNodesMap = cFactory.createSetMultiMaplet("source-nodes-map", srcExternalizer, Externalizer.forAnyGraphElement(Node<?, ?>[]::new));
}
catch (RuntimeException e) {
closeIgnoreErrors();
@@ -94,5 +95,11 @@ public abstract class GraphImpl implements Graph {
((Closeable)myContainerFactory).close();
}
}
public void flush() throws IOException {
if (myContainerFactory instanceof Flushable) {
((Flushable)myContainerFactory).flush();
}
}
}

View File

@@ -64,6 +64,11 @@ public final class LoggingDependencyGraph extends LoggingGraph implements Depend
}
}
@Override
public void flush() throws IOException {
getDelegate().flush();
}
@Override
public void close() throws IOException {
try {

View File

@@ -0,0 +1,56 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency.impl;
import org.h2.mvstore.MVMap;
import org.h2.mvstore.MVStore;
import org.h2.mvstore.type.DataType;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.dependency.Maplet;
import java.io.IOException;
public final class PersistentMVStoreMaplet<K, V> implements Maplet<K, V> {
private final MVMap<K, V> myMap;
public PersistentMVStoreMaplet(MVStore store, String mapName, DataType<K> keyType, DataType<V> valueType) {
myMap = store.openMap(mapName, new MVMap.Builder<K, V>().keyType(keyType).valueType(valueType));
}
@Override
public boolean containsKey(K key) {
return myMap.containsKey(key);
}
@Override
public @Nullable V get(K key) {
return myMap.get(key);
}
@Override
public void put(K key, V value) {
if (value == null) {
myMap.remove(key);
}
else {
myMap.put(key, value);
}
}
@Override
public void remove(K key) {
myMap.remove(key);
}
@Override
public Iterable<K> getKeys() {
return myMap.keySet();
}
@Override
public void close() throws IOException{
}
@Override
public void flush() throws IOException {
}
}

View File

@@ -0,0 +1,223 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency.impl;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.intellij.openapi.util.LowMemoryWatcher;
import com.intellij.util.SystemProperties;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.h2.mvstore.MVMap;
import org.h2.mvstore.MVStore;
import org.h2.mvstore.WriteBuffer;
import org.h2.mvstore.type.BasicDataType;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.api.GlobalOptions;
import org.jetbrains.jps.builders.storage.BuildDataCorruptedException;
import org.jetbrains.jps.dependency.*;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
// suitable for relatively small amounts of stored data
final class PersistentMVStoreMapletFactory implements MapletFactory, Closeable, Flushable {
private static final int BASE_CACHE_SIZE = 512 * (SystemProperties.getBooleanProperty(GlobalOptions.COMPILE_PARALLEL_OPTION, false)? 2 : 1);
private static final int ALLOWED_STORE_COMPACTION_TIME_MS = -1; // -1 for full-compact, 0 to disable compaction
private final MVSEnumerator myEnumerator;
private final Function<Object, Object> myDataInterner;
private final LoadingCache<Object, Object> myInternerCache;
//private final LoadingCache<String, String> myStringsInternerCache;
private final LowMemoryWatcher myMemWatcher;
private final int myCacheSize;
private final MVStore myStore;
PersistentMVStoreMapletFactory(String filePath, int maxBuilderThreads) {
// todo: need transaction store for transactions?
myStore = new MVStore.Builder()
.fileName(filePath)
.autoCommitDisabled() // all read-write operations are expected to be initiated via Graph APIs, otherwise deadlocks are possible because of incorrect lock acquisition sequence
.autoCompactFillRate(70)
.cacheSize(8)
.compress()
.cacheConcurrency(getConcurrencyLevel(maxBuilderThreads))
.open();
myStore.setVersionsToKeep(0);
// MVStore counter-based enumerator?
myEnumerator = new MVSEnumerator(myStore);
final int maxGb = (int) (Runtime.getRuntime().maxMemory() / 1_073_741_824L);
myCacheSize = BASE_CACHE_SIZE * Math.min(Math.max(1, maxGb), 5); // increase by BASE_CACHE_SIZE for every additional Gb
myInternerCache = Caffeine.newBuilder().maximumSize(myCacheSize).build(key -> key);
//myStringsInternerCache = Caffeine.newBuilder().maximumSize(myCacheSize).build(key -> key);
myDataInterner = elem -> elem instanceof Usage? myInternerCache.get(elem) : /*elem instanceof String? myStringsInternerCache.get((String) elem) :*/ elem;
myMemWatcher = LowMemoryWatcher.register(() -> {
myInternerCache.invalidateAll();
//myStringsInternerCache.invalidateAll();
flush();
});
}
private static int getConcurrencyLevel(int builderThreads) {
int result = 1, next = 1;
while (next <= builderThreads) {
result = next;
next *= 2;
}
return result;
}
@Override
public <K, V> MultiMaplet<K, V> createSetMultiMaplet(String storageName, Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer) {
PersistentMVStoreMultiMaplet<K, V, Set<V>> maplet = new PersistentMVStoreMultiMaplet<K, V, Set<V>>(
myStore, storageName, new GraphDataType<>(keyExternalizer, myEnumerator, myDataInterner), new GraphDataType<>(valueExternalizer, myEnumerator, myDataInterner), HashSet::new, Set[]::new
);
return new CachingMultiMaplet<>(maplet, myCacheSize);
}
@Override
public <K, V> Maplet<K, V> createMaplet(String storageName, Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer) {
PersistentMVStoreMaplet<K, V> maplet = new PersistentMVStoreMaplet<>(
myStore, storageName, new GraphDataType<>(keyExternalizer, myEnumerator, myDataInterner), new GraphDataType<>(valueExternalizer, myEnumerator, myDataInterner)
);
return new CachingMaplet<>(maplet, myCacheSize);
}
@Override
public void close() {
myMemWatcher.stop();
Throwable ex = null;
try {
myStore.commit();// first commit all open maps, that might use enumerator for serialization
myEnumerator.flush(); // save enumerator state
myStore.close(ALLOWED_STORE_COMPACTION_TIME_MS); // completely close the store commiting the rest of unsaved data
}
catch (Throwable e) {
ex = e;
}
myInternerCache.invalidateAll();
if (ex instanceof IOException) {
throw new BuildDataCorruptedException((IOException) ex);
}
else if (ex != null) {
throw new RuntimeException(ex);
}
}
@Override
public void flush() {
// maintain consistent state between the content in maps and in the enumerator
if (myStore.tryCommit() >= 0L) { // first save data from the maps => this may add additional entries to the enumerator.
if (myEnumerator.flush()) { // then store enumerator data
myStore.commit(); // if any data were stored, commit finally
}
}
}
private static class GraphDataType<T> extends BasicDataType<T> {
private final Externalizer<T> myExternalizer;
private final @Nullable Enumerator myEnumerator;
private final @Nullable Function<Object, Object> myObjectInterner;
GraphDataType(Externalizer<T> externalizer, @Nullable Enumerator enumerator, @Nullable Function<Object, Object> objectInterner) {
myExternalizer = externalizer;
myEnumerator = enumerator;
myObjectInterner = objectInterner;
}
@Override
public int compare(T a, T b) {
return a.toString().compareTo(b.toString());
}
@Override
public boolean isMemoryEstimationAllowed() {
return false; // todo?
}
@Override
public int getMemory(T obj) {
return 0;
}
@Override
public void write(WriteBuffer buff, T value) {
try {
myExternalizer.save(GraphDataOutputImpl.wrap(new WriteBufferDataOutput(buff), myEnumerator), value);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public T read(ByteBuffer buff) {
try {
return myExternalizer.load(GraphDataInputImpl.wrap(new ByteBufferDataInput(buff), myEnumerator, myObjectInterner));
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public T[] createStorage(int size) {
return myExternalizer.createStorage(size);
}
}
private static final class MVSEnumerator implements Enumerator {
private final Object2IntMap<String> myToIntMap = new Object2IntOpenHashMap<>();
private final Int2ObjectMap<String> myToStringMap = new Int2ObjectOpenHashMap<>();
private final MVMap<String, Integer> myStoreMap;
private Object2IntMap<String> myDelta = new Object2IntOpenHashMap<>();
MVSEnumerator(MVStore store) {
myStoreMap = store.openMap("string-table");
for (Map.Entry<String, Integer> entry : myStoreMap.entrySet()) {
String str = entry.getKey();
int num = entry.getValue();
myToIntMap.put(str, num);
myToStringMap.put(num, str);
}
}
@Override
public synchronized String toString(int num) {
return myToStringMap.get(num);
}
@Override
public synchronized int toNumber(String str) {
int currentSize = myToIntMap.size();
int num = myToIntMap.getOrDefault(str, currentSize);
if (num == currentSize) { // not in map yet
myToIntMap.put(str, num);
myToStringMap.put(num, str);
myDelta.put(str, num);
}
return num;
}
public boolean flush() {
Object2IntMap<String> delta;
synchronized (this) {
if (myDelta.isEmpty()) {
return false;
}
delta = myDelta;
myDelta = new Object2IntOpenHashMap<>();
}
myStoreMap.putAll(delta);
return true;
}
}
}

View File

@@ -0,0 +1,246 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency.impl;
import org.h2.mvstore.MVMap;
import org.h2.mvstore.MVStore;
import org.h2.mvstore.WriteBuffer;
import org.h2.mvstore.type.BasicDataType;
import org.h2.mvstore.type.DataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.dependency.MultiMaplet;
import org.jetbrains.jps.javac.Iterators;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
public final class PersistentMVStoreMultiMaplet<K, V, C extends Collection<V>> implements MultiMaplet<K, V> {
private final MVMap<K, C> myMap;
private final C myEmptyCollection;
private final Supplier<? extends C> myCollectionFactory;
private final MVMap.DecisionMaker<C> myAppendDecisionMaker;
private final MVMap.DecisionMaker<C> myRemoveDecisionMaker;
public PersistentMVStoreMultiMaplet(MVStore store, String mapName, DataType<K> keyType, DataType<V> valueType, Supplier<? extends C> collectionFactory, Function<Integer, C[]> collectionArrayFactory) {
myCollectionFactory = collectionFactory;
try {
C col = collectionFactory.get();
//noinspection unchecked
myEmptyCollection = col instanceof List? (C)Collections.emptyList() : col instanceof Set? (C)Collections.emptySet() : col;
if (col instanceof Set) {
myAppendDecisionMaker = new AppendSetDecisionMaker();
myRemoveDecisionMaker = new RemoveSetDecisionMaker();
}
else {
myAppendDecisionMaker = new AppendDecisionMaker();
myRemoveDecisionMaker = new RemoveDecisionMaker();
}
MVMap.Builder<K, C> mapBuilder = new MVMap.Builder<K, C>().keyType(keyType).valueType(new BasicDataType<>() {
@Override
public boolean isMemoryEstimationAllowed() {
return false;
}
@Override
public int getMemory(C obj) {
return 0;
}
@Override
public void write(WriteBuffer buff, C col) {
buff.putInt(col.size());
for (V value : col) {
valueType.write(buff, value);
}
}
@Override
public C read(ByteBuffer buff) {
C acc = myCollectionFactory.get();
int size = buff.getInt();
while (size-- > 0) {
acc.add(valueType.read(buff));
}
return acc;
}
@Override
public C[] createStorage(int size) {
//noinspection unchecked
return collectionArrayFactory.apply(size);
}
});
myMap = store.openMap(mapName, mapBuilder);
}
catch (Throwable e) {
throw new RuntimeException(e);
}
}
@Override
public boolean containsKey(K key) {
return myMap.containsKey(key);
}
@Override
public @NotNull C get(K key) {
C col = myMap.get(key);
return col != null? col : myEmptyCollection;
}
@Override
public void put(K key, @NotNull Iterable<? extends V> values) {
//noinspection unchecked
C data = ensureCollection(values);
if (data.isEmpty()) {
myMap.remove(key);
}
else {
myMap.put(key, data);
}
}
/** @noinspection unchecked*/
private C ensureCollection(Iterable<? extends V> seq) {
if (myEmptyCollection instanceof Set && seq instanceof Set) {
return (C)seq;
}
if (myEmptyCollection instanceof List && seq instanceof List) {
return (C)seq;
}
if (myEmptyCollection.getClass().isInstance(seq)) {
return (C)seq;
}
return Iterators.collect(seq, myCollectionFactory.get());
}
@Override
public void remove(K key) {
myMap.remove(key);
}
@Override
public void appendValue(K key, V value) {
appendValues(key, Collections.singleton(value));
}
@Override
public void appendValues(K key, @NotNull Iterable<? extends V> values) {
if (!Iterators.isEmpty(values)) {
myMap.operate(key, ensureCollection(values), myAppendDecisionMaker);
}
}
@Override
public void removeValue(K key, V value) {
removeValues(key, Collections.singleton(value));
}
@Override
public void removeValues(K key, @NotNull Iterable<? extends V> values) {
if (!Iterators.isEmpty(values)) {
myMap.operate(key, ensureCollection(values), myRemoveDecisionMaker);
}
}
@Override
public @NotNull Iterable<K> getKeys() {
return myMap.keySet();
}
@Override
public void close() {
// no impl;
}
@Override
public void flush() {
// no impl
}
private class AppendDecisionMaker extends MVMap.DecisionMaker<C> {
@Override
public MVMap.Decision decide(C existingValue, C providedValue) {
if (providedValue == null || providedValue.isEmpty()) {
return MVMap.Decision.ABORT;
}
return MVMap.Decision.PUT;
}
@Override
public <T extends C> T selectValue(T existingValue, T providedValue) {
if (existingValue == null || existingValue.isEmpty()) {
return providedValue;
}
//noinspection unchecked
T c = (T)myCollectionFactory.get();
c.addAll(existingValue);
if (c.addAll(providedValue)) {
return c;
}
return existingValue;
}
}
private class AppendSetDecisionMaker extends AppendDecisionMaker {
@Override
public MVMap.Decision decide(C existingValue, C providedValue) {
if (super.decide(existingValue, providedValue) == MVMap.Decision.ABORT) {
return MVMap.Decision.ABORT;
}
if (existingValue == null || existingValue.isEmpty()) {
return MVMap.Decision.PUT;
}
for (V v : providedValue) {
if (!existingValue.contains(v)) {
return MVMap.Decision.PUT;
}
}
return MVMap.Decision.ABORT;
}
}
private class RemoveDecisionMaker extends MVMap.DecisionMaker<C> {
@Override
public MVMap.Decision decide(C existingValue, C providedValue) {
// if nothing to remove or nothing to remove from
if (providedValue == null || providedValue.isEmpty() || existingValue == null || existingValue.isEmpty()) {
return MVMap.Decision.ABORT;
}
return MVMap.Decision.PUT;
}
@Override
public <T extends C> T selectValue(T existingValue, T providedValue) {
// both provided and existing values are non-empty collections
//noinspection unchecked
T c = (T)myCollectionFactory.get();
c.addAll(existingValue);
if (c.removeAll(providedValue)) {
return c;
}
return existingValue;
}
}
private class RemoveSetDecisionMaker extends RemoveDecisionMaker {
@Override
public MVMap.Decision decide(C existingValue, C providedValue) {
if (super.decide(existingValue, providedValue) == MVMap.Decision.ABORT) {
return MVMap.Decision.ABORT;
}
for (V v : providedValue) {
if (existingValue.contains(v)) {
return MVMap.Decision.PUT;
}
}
return MVMap.Decision.ABORT;
}
}
}

View File

@@ -0,0 +1,161 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency.impl;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.intellij.openapi.util.LowMemoryWatcher;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.SystemProperties;
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.KeyDescriptor;
import com.intellij.util.io.PersistentStringEnumerator;
import com.intellij.util.io.StorageLockContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.api.GlobalOptions;
import org.jetbrains.jps.builders.storage.BuildDataCorruptedException;
import org.jetbrains.jps.dependency.*;
import org.jetbrains.jps.javac.Iterators;
import java.io.*;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
final class PersistentMapletFactory implements MapletFactory, Closeable {
private static final int BASE_CACHE_SIZE = 512 * (SystemProperties.getBooleanProperty(GlobalOptions.COMPILE_PARALLEL_OPTION, false)? 2 : 1);
private final String myRootDirPath;
private final PersistentStringEnumerator myStringTable;
private final List<BaseMaplet<?>> myMaps = new ArrayList<>();
private final Enumerator myEnumerator;
private final Function<Object, Object> myDataInterner;
private final LoadingCache<Object, Object> myInternerCache;
private final LowMemoryWatcher myMemWatcher;
private final int myCacheSize;
PersistentMapletFactory(String rootDirPath) throws IOException {
myRootDirPath = rootDirPath;
// Important: The enumerator will be called from PHM data externalizers. A PHM acquires the page_cache lock before externalizing => this enumerator should use a separate StorageLockContext to avoid deadlocks
myStringTable = new PersistentStringEnumerator(getMapFile("string-table"), 1024 * 4, true, new StorageLockContext());
myEnumerator = new Enumerator() {
@Override
public String toString(int num) throws IOException {
return myStringTable.valueOf(num);
}
@Override
public int toNumber(String str) throws IOException {
return myStringTable.enumerate(str);
}
};
final int maxGb = (int) (Runtime.getRuntime().maxMemory() / 1_073_741_824L);
myCacheSize = BASE_CACHE_SIZE * Math.min(Math.max(1, maxGb), 5); // increase by BASE_CACHE_SIZE for every additional Gb
myInternerCache = Caffeine.newBuilder().maximumSize(myCacheSize).build(key -> key);
myDataInterner = elem -> elem instanceof Usage? myInternerCache.get(elem) : elem;
myMemWatcher = LowMemoryWatcher.register(() -> {
myInternerCache.invalidateAll();
myStringTable.force();
for (BaseMaplet<?> map : myMaps) {
try {
map.flush();
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
@Override
public <K, V> MultiMaplet<K, V> createSetMultiMaplet(String storageName, Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer) {
PersistentMultiMaplet<K, V, Set<V>> maplet = new PersistentMultiMaplet<>(
getMapFile(storageName), new GraphKeyDescriptor<>(keyExternalizer, myEnumerator, null), new GraphDataExternalizer<>(valueExternalizer, myEnumerator, myDataInterner), HashSet::new
);
MultiMaplet<K, V> container = new CachingMultiMaplet<>(maplet, myCacheSize);
myMaps.add(container);
return container;
}
@Override
public <K, V> Maplet<K, V> createMaplet(String storageName, Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer) {
Maplet<K, V> container = new CachingMaplet<>(
new PersistentMaplet<>(getMapFile(storageName), new GraphKeyDescriptor<>(keyExternalizer, myEnumerator, null), new GraphDataExternalizer<>(valueExternalizer, myEnumerator, myDataInterner)),
myCacheSize
);
myMaps.add(container);
return container;
}
@Override
public void close() {
myMemWatcher.stop();
Throwable ex = null;
for (Closeable container : Iterators.flat(myMaps, Iterators.asIterable(myStringTable))) {
try {
container.close();
}
catch (Throwable e) {
if (ex == null) {
ex = e;
}
}
}
myMaps.clear();
myInternerCache.invalidateAll();
if (ex instanceof IOException) {
throw new BuildDataCorruptedException((IOException) ex);
}
else if (ex != null) {
throw new RuntimeException(ex);
}
}
private Path getMapFile(final String name) {
final File file = new File(myRootDirPath, name);
FileUtil.createIfDoesntExist(file);
return file.toPath();
}
private static class GraphDataExternalizer<T> implements DataExternalizer<T> {
private final Externalizer<T> myExternalizer;
private final @Nullable Enumerator myEnumerator;
private final @Nullable Function<Object, Object> myObjectInterner;
GraphDataExternalizer(Externalizer<T> externalizer, @Nullable Enumerator enumerator, @Nullable Function<Object, Object> objectInterner) {
myExternalizer = externalizer;
myEnumerator = enumerator;
myObjectInterner = objectInterner;
}
@Override
public void save(@NotNull DataOutput out, T value) throws IOException {
myExternalizer.save(GraphDataOutputImpl.wrap(out, myEnumerator), value);
}
@Override
public T read(@NotNull DataInput in) throws IOException {
return myExternalizer.load(GraphDataInputImpl.wrap(in, myEnumerator, myObjectInterner));
}
}
private static final class GraphKeyDescriptor<T> extends GraphDataExternalizer<T> implements KeyDescriptor<T> {
GraphKeyDescriptor(Externalizer<T> externalizer, @Nullable Enumerator enumerator, @Nullable Function<Object, Object> objectInterner) {
super(externalizer, enumerator, objectInterner);
}
@Override
public boolean isEqual(T val1, T val2) {
return val1.equals(val2);
}
@Override
public int getHashCode(T value) {
return value.hashCode();
}
}
}

View File

@@ -0,0 +1,93 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency.impl;
import org.h2.mvstore.WriteBuffer;
import org.jetbrains.annotations.NotNull;
import java.io.DataOutput;
import java.io.IOException;
public class WriteBufferDataOutput implements DataOutput {
private final WriteBuffer myBuf;
public WriteBufferDataOutput(WriteBuffer buf) {
myBuf = buf;
}
@Override
public void write(int b) throws IOException {
myBuf.putInt(b);
}
@Override
public void write(@NotNull byte[] b) throws IOException {
myBuf.put(b);
}
@Override
public void write(@NotNull byte[] b, int off, int len) throws IOException {
myBuf.put(b, off, len);
}
@Override
public void writeBoolean(boolean v) throws IOException {
writeByte(v? 1 : 0);
}
@Override
public void writeByte(int v) throws IOException {
myBuf.put(((byte)(v & 0xFF)));
}
@Override
public void writeShort(int v) throws IOException {
myBuf.putShort((short)(v & 0xFFFF));
}
@Override
public void writeChar(int v) throws IOException {
myBuf.putChar((char)(v & 0xFFFF));
}
@Override
public void writeInt(int v) throws IOException {
myBuf.putInt(v);
}
@Override
public void writeLong(long v) throws IOException {
myBuf.putLong(v);
}
@Override
public void writeFloat(float v) throws IOException {
myBuf.putFloat(v);
}
@Override
public void writeDouble(double v) throws IOException {
myBuf.putDouble(v);
}
@Override
public void writeBytes(@NotNull String s) throws IOException {
int length = s.length();
for (int idx = 0; idx < length; idx++) {
writeByte(s.charAt(idx));
}
}
@Override
public void writeChars(@NotNull String s) throws IOException {
int length = s.length();
for (int idx = 0; idx < length; idx++) {
myBuf.putChar(s.charAt(idx));
}
}
@Override
public void writeUTF(@NotNull String s) throws IOException {
int length = s.length();
myBuf.putVarInt(length).putStringData(s, length);
}
}

View File

@@ -228,6 +228,11 @@ enum JvmProtoMemberValueExternalizer implements Externalizer<Object> {
this.dataType = dataType;
}
@Override
public Object[] createStorage(int size) {
return new Object[size];
}
@Override
public abstract void save(GraphDataOutput out, Object v) throws IOException;

View File

@@ -463,6 +463,16 @@ public final class BuildDataManager {
mappings.flush(memoryCachesOnly);
}
}
GraphConfiguration graphConfig = getDependencyGraph();
if (graphConfig != null) {
try {
graphConfig.getGraph().flush();
}
catch (IOException e) {
LOG.warn(e);
}
}
}
public void close() throws IOException {
@@ -819,6 +829,17 @@ public final class BuildDataManager {
lock.writeLock().unlock();
}
}
@Override
public void flush() throws IOException {
lock.writeLock().lock();
try {
delegate.flush();
}
finally {
lock.writeLock().unlock();
}
}
};
}
}