diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/config/LanguageServerProperties.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/config/LanguageServerProperties.java index 5009b245d..88d641e41 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/config/LanguageServerProperties.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/config/LanguageServerProperties.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Pivotal, Inc. + * Copyright (c) 2018, 2022 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -46,6 +46,11 @@ public class LanguageServerProperties { * Hover request handler timeout in milliseconds */ private long hoverTimeout = -1; + + /** + * Reconciling delay time interval. Maximum reconcile rate. + */ + private long reconcileDelay = 0; public boolean isStandalone() { return standalone; @@ -87,4 +92,12 @@ public class LanguageServerProperties { this.hoverTimeout = hoverTimeout; } + public long getReconcileDelay() { + return reconcileDelay; + } + + public void setReconcileDelay(long reconcileDelay) { + this.reconcileDelay = reconcileDelay; + } + } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java index 80b18f29c..94774a384 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java @@ -13,16 +13,15 @@ package org.springframework.ide.vscode.commons.languageserver.util; import java.lang.management.ManagementFactory; import java.net.URI; import java.nio.file.Paths; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; @@ -52,7 +51,6 @@ import org.eclipse.lsp4j.RegistrationParams; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentSyncKind; -import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.WorkDoneProgressBegin; import org.eclipse.lsp4j.WorkDoneProgressCreateParams; import org.eclipse.lsp4j.WorkDoneProgressEnd; @@ -594,7 +592,7 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC * in a burst. Rather than execute the same request repeatedly we can avoid queuing * up more requests if the previous request has not yet been started. */ - private Set queuedReconcileRequests = Collections.synchronizedSet(new HashSet<>()); + private Map> reconcileRequests = new ConcurrentHashMap<>(); private DiagnosticSeverityProvider severityProvider = DiagnosticSeverityProvider.DEFAULT; @@ -605,40 +603,36 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC public void validateWith(TextDocumentIdentifier docId, IReconcileEngine engine) { SimpleTextDocumentService documents = getTextDocumentService(); - TextDocument doc = documents.getLatestSnapshot(docId.getUri()); - if (doc == null) { + if (documents.getLatestSnapshot(docId.getUri()) == null) { log.debug("Reconcile skipped due to document doesn't exist anymore {}", docId.getUri()); return; } - int requestedVersion = doc.getVersion(); - VersionedTextDocumentIdentifier request = new VersionedTextDocumentIdentifier(docId.getUri(), requestedVersion); - log.debug("Reconcile requested {} - {}", request.getUri(), request.getVersion()); - if (!queuedReconcileRequests.add(request)) { - log.debug("Reconcile skipped {} - {}", request.getUri(), request.getVersion()); + String uri = docId.getUri(); + CompletableFuture newFuture = new CompletableFuture(); + CompletableFuture oldFuture = reconcileRequests.putIfAbsent(uri, newFuture); + if (oldFuture != null && oldFuture != newFuture) { + log.debug("Reconcile skipped {}", uri); return; } - CompletableFuture reconcileSession = this.busyReconcile = new CompletableFuture(); + CompletableFuture reconcileSession = this.busyReconcile = oldFuture == null ? newFuture : oldFuture; // Log.debug("Reconciling BUSY"); // Avoid running in the same thread as lsp4j as it can result // in long "hangs" for slow reconcile providers - Mono.fromRunnable(() -> { - queuedReconcileRequests.remove(request); - log.debug("Reconcile starting {} - {}", request.getUri(), request.getVersion()); + Mono mono = props.getReconcileDelay() > 0 + ? Mono.delay(Duration.ofMillis(props.getReconcileDelay())).publishOn(RECONCILER_SCHEDULER) + : Mono.empty().publishOn(RECONCILER_SCHEDULER); + + mono.then(Mono.fromRunnable(() -> { + reconcileRequests.remove(uri); + log.debug("Reconcile starting {}", uri); -// TextDocument doc = documents.getDocument(docId.getUri()).copy(); - -// if (doc == null) { -// log.debug("Reconcile aborted due to document doesn't exist {} - {}", request.getUri(), request.getVersion()); -// //Do not bother reconciling if document doesn't exist anymore (got closed in the meantime) -// return; -// } - - if (requestedVersion != doc.getVersion()) { - log.debug("Reconcile aborted due to document being already stale {} - {}", request.getUri(), request.getVersion()); - //Do not bother reconciling if document contents is already stale. + TextDocument doc = documents.getLatestSnapshot(docId.getUri()); + + if (doc == null) { + //Do not bother reconciling if document doesn't exist anymore (got closed in the meantime) return; } @@ -698,7 +692,7 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC }; engine.reconcile(doc, problems); - }) + })) .onErrorResume(error -> { log.error("", error); return Mono.empty(); @@ -707,7 +701,6 @@ public final class SimpleLanguageServer implements Sts4LanguageServer, LanguageC reconcileSession.complete(null); // Log.debug("Reconciler DONE : "+this.busyReconcile.isDone()); }) - .subscribeOn(RECONCILER_SCHEDULER) .subscribe(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java index a8132e789..7a6bd2044 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java @@ -78,7 +78,7 @@ public class BootLanguageServerInitializer implements InitializingBean { private static ProjectObserver.Listener reconcileOpenDocumentsForProjectChange(SimpleLanguageServer s, CompositeLanguageServerComponents c, JavaProjectFinder projectFinder) { return ProjectObserver.onAny(project -> { c.getReconcileEngine().ifPresent(reconciler -> { - log.info("A project changed {}, triggering reconcile on all project's open documents", project.getElementName()); + log.debug("A project changed {}, triggering reconcile on all project's open documents", project.getElementName()); for (TextDocument doc : s.getTextDocumentService().getAll()) { if (projectFinder.find(doc.getId()).orElse(null) == project) { s.validateWith(doc.getId(), reconciler); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteCompilationUnitCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteCompilationUnitCache.java index c9d307495..1013c607a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteCompilationUnitCache.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteCompilationUnitCache.java @@ -35,7 +35,6 @@ import org.openrewrite.java.JavaParser; import org.openrewrite.java.tree.J.CompilationUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.utils.DocumentContentProvider; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IClasspathUtil; @@ -54,9 +53,11 @@ import reactor.core.Disposable; public class RewriteCompilationUnitCache implements DocumentContentProvider, Disposable { - private static final Logger logger = LoggerFactory.getLogger(CompilationUnitCache.class); + private static final Logger logger = LoggerFactory.getLogger(RewriteCompilationUnitCache.class); + + private final Object URI_TO_CU_LOCK = new Object(); - private static final long CU_ACCESS_EXPIRATION = 5; +// private static final long CU_ACCESS_EXPIRATION = 5; private JavaProjectFinder projectFinder; private ProjectObserver projectObserver; @@ -106,20 +107,20 @@ public class RewriteCompilationUnitCache implements DocumentContentProvider, Dis @Override public void deleted(IJavaProject project) { - logger.info("CU Cache: deleted project {}", project.getElementName()); + logger.debug("CU Cache: deleted project {}", project.getElementName()); invalidateProject(project); } @Override public void created(IJavaProject project) { - logger.info("CU Cache: created project {}", project.getElementName()); + logger.debug("CU Cache: created project {}", project.getElementName()); invalidateProject(project); // loadJavaParser(project); } @Override public void changed(IJavaProject project) { - logger.info("CU Cache: changed project {}", project.getElementName()); + logger.debug("CU Cache: changed project {}", project.getElementName()); invalidateProject(project); // Load the new cache the value right away // loadJavaParser(project); @@ -173,15 +174,17 @@ public class RewriteCompilationUnitCache implements DocumentContentProvider, Dis } private void invalidateCuForJavaFile(String uriStr) { - logger.info("CU Cache: invalidate AST for {}", uriStr); - URI uri = URI.create(uriStr); - uriToCu.invalidate(uri); - Optional project = projectFinder.find(new TextDocumentIdentifier(uriStr)); - if (project.isPresent()) { - JavaParser parser = javaParsers.getIfPresent(project.get()); - if (parser != null) { - parser.reset(); + synchronized (URI_TO_CU_LOCK) { + if (uriToCu.getIfPresent(uri) != null) { + uriToCu.invalidate(uri); + Optional project = projectFinder.find(new TextDocumentIdentifier(uriStr)); + if (project.isPresent()) { + JavaParser parser = javaParsers.getIfPresent(project.get()); + if (parser != null) { + parser.reset(); + } + } } } } @@ -189,12 +192,14 @@ public class RewriteCompilationUnitCache implements DocumentContentProvider, Dis private void invalidateProject(IJavaProject project) { logger.info("CU Cache: invalidate project <{}>", project.getElementName()); - Set docUris = projectToDocs.getIfPresent(project); - if (docUris != null) { - uriToCu.invalidateAll(docUris); - projectToDocs.invalidate(project); + synchronized (URI_TO_CU_LOCK) { + Set docUris = projectToDocs.getIfPresent(project); + if (docUris != null) { + uriToCu.invalidateAll(docUris); + projectToDocs.invalidate(project); + } + javaParsers.invalidate(project); } - javaParsers.invalidate(project); } @Override @@ -211,27 +216,30 @@ public class RewriteCompilationUnitCache implements DocumentContentProvider, Dis public CompilationUnit getCU(IJavaProject project, URI uri) { try { if (project != null) { - return uriToCu.get(uri, () -> { - JavaParser javaParser = /*loadJavaParser(project)*/createJavaParser(project); - Input input = new Input(Paths.get(uri), () -> { - try { - return new ByteArrayInputStream(fetchContent(uri).getBytes()); - } catch (Exception e) { - throw new IllegalStateException("Unexpected error fetching document content"); + synchronized (URI_TO_CU_LOCK) { + return uriToCu.get(uri, () -> { + logger.debug("Parsing CU {}", uri); + JavaParser javaParser = loadJavaParser(project); + Input input = new Input(Paths.get(uri), () -> { + try { + return new ByteArrayInputStream(fetchContent(uri).getBytes()); + } catch (Exception e) { + throw new IllegalStateException("Unexpected error fetching document content"); + } + }); + + List cus = ORAstUtils.parseInputs(javaParser, List.of(input)); + + CompilationUnit cu = cus.get(0); + + + if (cu != null) { + projectToDocs.get(project, () -> new HashSet<>()).add(uri); } - }); - - List cus = ORAstUtils.parseInputs(javaParser, List.of(input)); - - CompilationUnit cu = cus.get(0); - - - if (cu != null) { - projectToDocs.get(project, () -> new HashSet<>()).add(uri); - } - return cu; - }); + return cu; + }); + } } } catch (Exception e) { logger.error("", e); diff --git a/headless-services/spring-boot-language-server/src/main/resources/application.properties b/headless-services/spring-boot-language-server/src/main/resources/application.properties index 18283d936..49bc73ded 100644 --- a/headless-services/spring-boot-language-server/src/main/resources/application.properties +++ b/headless-services/spring-boot-language-server/src/main/resources/application.properties @@ -1,4 +1,5 @@ languageserver.extension-id: vscode-spring-boot +languageserver.reconcile-delay=100 languageserver.completion-trigger-characters.java: languageserver.completion-trigger-characters.xml: languageserver.completion-trigger-characters.spring-boot-properties-yaml: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.{