Skip to content

Commit

Permalink
Merge pull request #418 from nipunayf/fix-completions-oom
Browse files Browse the repository at this point in the history
Maintain a single temp directory per project
  • Loading branch information
nipunayf authored Oct 2, 2024
2 parents 40defdc + a906d64 commit e7d03a1
Show file tree
Hide file tree
Showing 6 changed files with 1,971 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.FileChangeType;
import org.eclipse.lsp4j.FileEvent;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
Expand Down Expand Up @@ -93,16 +96,23 @@ public CompletableFuture<VisibleVariableTypesResponse> visibleVariableTypes(Visi
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(
ExpressionEditorCompletionRequest request) {
return CompletableFuture.supplyAsync(() -> {
Path projectPath = null;
try {
// Load the project
// Load the original project
Path filePath = Path.of(request.filePath());
this.workspaceManager.loadProject(filePath);
Path projectPath = this.workspaceManager.projectRoot(filePath);
projectPath = this.workspaceManager.projectRoot(filePath);

// Create a temporary directory and load the project
ProjectCacheManager projectCacheManager = new ProjectCacheManager(projectPath, filePath);
projectCacheManager.createTempDirectory();
Path destination = projectCacheManager.getDestination();
// Load the shadowed project
ProjectCacheManager projectCacheManager =
ProjectCacheManager.InstanceHandler.getInstance(projectPath);
projectCacheManager.copyContent();
Path destination = projectCacheManager.getDestination(filePath);

FileEvent fileEvent = new FileEvent(destination.toUri().toString(), FileChangeType.Changed);
DidChangeWatchedFilesParams didChangeWatchedFilesParams =
new DidChangeWatchedFilesParams(List.of(fileEvent));
this.langServer.getWorkspaceService().didChangeWatchedFiles(didChangeWatchedFilesParams);
this.workspaceManager.loadProject(destination);

// Get the document
Expand All @@ -118,7 +128,7 @@ public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completio
TextEdit textEdit = TextEdit.from(TextRange.from(textPosition, 0), statement);
TextDocument newTextDocument =
textDocument.apply(TextDocumentChange.from(List.of(textEdit).toArray(new TextEdit[0])));
projectCacheManager.writeContent(newTextDocument);
projectCacheManager.writeContent(newTextDocument, filePath);
document.get().modify()
.withContent(String.join(System.lineSeparator(), newTextDocument.textLines()))
.apply();
Expand All @@ -133,10 +143,14 @@ public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completio
CompletableFuture<Either<List<CompletionItem>, CompletionList>> completableFuture =
langServer.getTextDocumentService().completion(params);
Either<List<CompletionItem>, CompletionList> completions = completableFuture.join();
projectCacheManager.deleteCache();
projectCacheManager.deleteContent();
return completions;
} catch (Throwable e) {
return Either.forLeft(List.of());
} finally {
if (projectPath != null) {
ProjectCacheManager.InstanceHandler.release(projectPath);
}
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,39 @@
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;

/**
* Manages the cache of the temporarily copied project directory.
*
* @since 1.4.0
*/
public class ProjectCacheManager {
class ProjectCacheManager {

private final Path sourceDir;
private final Path filePath;
private Path destinationProjectPath;
private Path destinationPath;
private final Path tempDir;

public ProjectCacheManager(Path sourceDir, Path filePath) {
public ProjectCacheManager(Path sourceDir) {
this.sourceDir = sourceDir;
this.filePath = filePath;
try {
Path tempDirPath = Files.createTempDirectory("project-cache");
this.tempDir = tempDirPath.resolve(sourceDir.getFileName());
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public void createTempDirectory() throws IOException {
// Create a temporary directory
Path tempDir = Files.createTempDirectory("project-cache");
Path tempDesintaitonPath = tempDir.resolve(sourceDir.getFileName());
destinationProjectPath = tempDesintaitonPath;

public void copyContent() throws IOException {
// Copy contents from sourceDir to destinationDir
if (Files.isDirectory(sourceDir)) {
try (Stream<Path> paths = Files.walk(sourceDir)) {
paths.forEach(source -> {
try {
Files.copy(source, tempDesintaitonPath.resolve(sourceDir.relativize(source)),
Files.copy(source, tempDir.resolve(sourceDir.relativize(source)),
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException("Failed to copy project directory to cache", e);
Expand All @@ -65,12 +67,12 @@ public void createTempDirectory() throws IOException {
}
return;
}
Files.copy(sourceDir, tempDesintaitonPath, StandardCopyOption.REPLACE_EXISTING);
Files.copy(sourceDir, tempDir, StandardCopyOption.REPLACE_EXISTING);
}

public void deleteCache() throws IOException {
if (Files.isDirectory(destinationProjectPath)) {
try (Stream<Path> paths = Files.walk(destinationProjectPath)) {
public void deleteContent() throws IOException {
if (Files.isDirectory(tempDir)) {
try (Stream<Path> paths = Files.walk(tempDir)) {
paths.sorted(Comparator.reverseOrder()).forEach(source -> {
try {
Files.delete(source);
Expand All @@ -81,24 +83,51 @@ public void deleteCache() throws IOException {
}
return;
}
Files.delete(destinationProjectPath);
Files.delete(tempDir);
}

public void writeContent(TextDocument textDocument) throws IOException {
if (destinationProjectPath == null) {
public void writeContent(TextDocument textDocument, Path filePath) throws IOException {
if (tempDir == null) {
throw new RuntimeException("Destination directory is not created");
}
Files.writeString(getDestination(), new String(textDocument.toCharArray()), StandardOpenOption.CREATE,
Files.writeString(getDestination(filePath), new String(textDocument.toCharArray()), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}

public Path getDestination() {
if (destinationProjectPath == null) {
public Path getDestination(Path filePath) {
if (tempDir == null) {
throw new RuntimeException("Destination directory is not created");
}
if (destinationPath == null) {
destinationPath = destinationProjectPath.resolve(sourceDir.relativize(sourceDir.resolve(filePath)));
return tempDir.resolve(sourceDir.relativize(sourceDir.resolve(filePath)));
}

/**
* The multiton design pattern to handle `ProjectCacheManager` instances mapped by source directory. Ensures that
* there is only one copy per each project.
*
* @since 1.4.0
*/

static class InstanceHandler {

private static final Map<Path, ProjectCacheManager> instances = new ConcurrentHashMap<>();
private static final Map<Path, Lock> locks = new ConcurrentHashMap<>();

private InstanceHandler() {
}

public static ProjectCacheManager getInstance(Path sourceDir) {
Lock lock = locks.computeIfAbsent(sourceDir, key -> new ReentrantLock());
lock.lock();
return instances.computeIfAbsent(sourceDir, key -> new ProjectCacheManager(sourceDir));
}

public static void release(Path sourceDir) {
Lock lock = locks.get(sourceDir);
if (lock != null) {
lock.unlock();
locks.remove(sourceDir);
}
}
return destinationPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@
requires io.ballerina.diagram.util;
requires io.ballerina.flow.model.generator;
requires io.ballerina.openapi.core;
requires io.ballerina.language.server.core;
}
Loading

0 comments on commit e7d03a1

Please sign in to comment.