Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -86,6 +89,8 @@ public class XMLLanguageServer implements ProcessLanguageServer, XMLLanguageServ

private static final Logger LOGGER = Logger.getLogger(XMLLanguageServer.class.getName());

private static ExecutorService SHARED_EXECUTOR;

private final XMLLanguageService xmlLanguageService;
private final XMLTextDocumentService xmlTextDocumentService;
private final XMLWorkspaceService xmlWorkspaceService;
Expand All @@ -96,7 +101,23 @@ public class XMLLanguageServer implements ProcessLanguageServer, XMLLanguageServ
private TelemetryManager telemetryManager;

public XMLLanguageServer() {
xmlTextDocumentService = new XMLTextDocumentService(this);
// Create a shared thread pool with limited parallelism (4 threads)
// to avoid excessive ForkJoinPool usage (was 20 threads)
synchronized (XMLLanguageServer.class) {
if (SHARED_EXECUTOR == null) {
SHARED_EXECUTOR = Executors.newFixedThreadPool(4, new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "lemminx-worker-" + counter.incrementAndGet());
thread.setDaemon(true);
return thread;
}
});
}
}

xmlTextDocumentService = new XMLTextDocumentService(this, SHARED_EXECUTOR);
xmlWorkspaceService = new XMLWorkspaceService(this);

xmlLanguageService = new XMLLanguageService();
Expand All @@ -109,6 +130,16 @@ public XMLLanguageServer() {
delayer = Executors.newScheduledThreadPool(1);
}

/**
* Returns the shared executor service for async operations.
* This executor has limited parallelism (4 threads) to reduce memory pressure.
*
* @return the shared executor service
*/
public static ExecutorService getSharedExecutor() {
return SHARED_EXECUTOR;
}

@Override
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
Object initOptions = InitializationOptionsSettings.getSettings(params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Predicate;
Expand Down Expand Up @@ -124,6 +125,7 @@ public class XMLTextDocumentService implements TextDocumentService {
private final XMLLanguageServer xmlLanguageServer;
private final ModelTextDocuments<DOMDocument> documents;
private final ModelValidatorDelayer<DOMDocument> xmlValidatorDelayer;
private final ExecutorService executorService;

private SharedSettings sharedSettings;
private LimitExceededWarner limitExceededWarner;
Expand Down Expand Up @@ -194,12 +196,13 @@ public void triggerValidationIfNeeded() {

private Boolean clientConfigurationSupport;

public XMLTextDocumentService(XMLLanguageServer xmlLanguageServer) {
public XMLTextDocumentService(XMLLanguageServer xmlLanguageServer, ExecutorService executorService) {
this.xmlLanguageServer = xmlLanguageServer;
this.executorService = executorService;
DOMParser parser = DOMParser.getInstance();
this.documents = new ModelTextDocuments<DOMDocument>((document, cancelChecker) -> {
return parser.parse(document, getXMLLanguageService().getResolverExtensionManager(), true, cancelChecker);
});
}, executorService);
this.sharedSettings = new SharedSettings();
this.limitExceededWarner = null;
this.xmlValidatorDelayer = new ModelValidatorDelayer<DOMDocument>((document) -> {
Expand Down Expand Up @@ -690,7 +693,7 @@ void validate(TextDocument document, boolean withDelay) throws CancellationExcep
if (withDelay) {
xmlValidatorDelayer.validateWithDelay((ModelTextDocument<DOMDocument>) document);
} else {
CompletableFuture.runAsync(() -> {
Runnable validationTask = () -> {
DOMDocument xmlDocument = ((ModelTextDocument<DOMDocument>) document).getModel();
validate(xmlDocument, Collections.emptyMap());
getXMLLanguageService().getDocumentLifecycleParticipants().forEach(participant -> {
Expand All @@ -701,7 +704,12 @@ void validate(TextDocument document, boolean withDelay) throws CancellationExcep
+ participant.getClass().getName() + "'.", e);
}
});
});
};
if (executorService != null) {
CompletableFuture.runAsync(validationTask, executorService);
} else {
CompletableFuture.runAsync(validationTask);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
package org.eclipse.lemminx.commons;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.function.BiFunction;
import java.util.function.Function;

Expand All @@ -31,9 +32,15 @@
public class ModelTextDocuments<T> extends TextDocuments<ModelTextDocument<T>> {

private final BiFunction<TextDocument, CancelChecker, T> parse;
private final ExecutorService executorService;

public ModelTextDocuments(BiFunction<TextDocument, CancelChecker, T> parse) {
this(parse, null);
}

public ModelTextDocuments(BiFunction<TextDocument, CancelChecker, T> parse, ExecutorService executorService) {
this.parse = parse;
this.executorService = executorService;
}

@Override
Expand Down Expand Up @@ -108,6 +115,17 @@ public T getModel(String uri) {
*/
public <R> CompletableFuture<R> computeModelAsync(TextDocumentIdentifier documentIdentifier,
BiFunction<T, CancelChecker, R> code) {
if (executorService != null) {
return computeAsyncWithExecutor(cancelChecker -> {
// Get or parse the model.
T model = getModel(documentIdentifier);
if (model == null) {
return null;
}
// Execute the code with the model
return code.apply(model, cancelChecker);
});
}
return CompletableFutures.computeAsync(cancelChecker -> {
// Get or parse the model.
T model = getModel(documentIdentifier);
Expand All @@ -132,6 +150,17 @@ public <R> CompletableFuture<R> computeModelAsync(TextDocumentIdentifier documen
*/
public <R> CompletableFuture<R> computeModelAsyncCompose(TextDocumentIdentifier documentIdentifier,
BiFunction<T, CancelChecker, CompletableFuture<R>> code) {
if (executorService != null) {
return computeAsyncComposeWithExecutor(cancelChecker -> {
// Get or parse the model.
T model = getModel(documentIdentifier);
if (model == null) {
return CompletableFuture.completedFuture(null);
}
// Execute the code with the model
return code.apply(model, cancelChecker);
});
}
return computeAsyncCompose(cancelChecker -> {
// Get or parse the model.
T model = getModel(documentIdentifier);
Expand All @@ -144,6 +173,20 @@ public <R> CompletableFuture<R> computeModelAsyncCompose(TextDocumentIdentifier
});
}

private <R> CompletableFuture<R> computeAsyncWithExecutor(Function<CancelChecker, R> code) {
CompletableFuture<CancelChecker> start = new CompletableFuture<>();
CompletableFuture<R> result = start.thenApplyAsync(code, executorService);
start.complete(new FutureCancelChecker(result));
return result;
}

private <R> CompletableFuture<R> computeAsyncComposeWithExecutor(Function<CancelChecker, CompletableFuture<R>> code) {
CompletableFuture<CancelChecker> start = new CompletableFuture<>();
CompletableFuture<R> result = start.thenComposeAsync(code, executorService);
start.complete(new FutureCancelChecker(result));
return result;
}

private static <R> CompletableFuture<R> computeAsyncCompose(Function<CancelChecker, CompletableFuture<R>> code) {
CompletableFuture<CancelChecker> start = new CompletableFuture<>();
CompletableFuture<R> result = start.thenComposeAsync(code);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*/
public class ModelValidatorDelayer<T> {

private static final long DEFAULT_VALIDATION_DELAY_MS = 500;
private static final long DEFAULT_VALIDATION_DELAY_MS = 100;

private final ScheduledExecutorService executorService;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public class TreeLineTracker implements ILineTracker {
* must have this zero-length delimiter.
*/
private static final String NO_DELIM = ""; //$NON-NLS-1$

// Delimiter type constants for memory optimization
private static final byte DELIM_NONE = 0; // "" - no delimiter
private static final byte DELIM_LF = 1; // "\n" - Unix/Linux
private static final byte DELIM_CR = 2; // "\r" - old Mac
private static final byte DELIM_CRLF = 3; // "\r\n" - Windows

/**
* A node represents one line. Its character and line offsets are 0-based and
Expand All @@ -100,7 +106,7 @@ public class TreeLineTracker implements ILineTracker {
private static final class Node {
Node(int length, String delimiter) {
this.length = length;
this.delimiter = delimiter;
this.delimiterType = stringToDelimiterType(delimiter);
}

/**
Expand All @@ -115,8 +121,8 @@ private static final class Node {
int offset;
/** The number of characters in this line. */
int length;
/** The line delimiter of this line, needed to answer the delimiter query. */
String delimiter;
/** The line delimiter type of this line (0=none, 1=\n, 2=\r, 3=\r\n). Memory optimized. */
byte delimiterType;
/** The parent node, <code>null</code> if this is the root node. */
Node parent;
/** The left subtree, possibly <code>null</code>. */
Expand All @@ -125,6 +131,22 @@ private static final class Node {
Node right;
/** The balance factor. */
byte balance;

/**
* Returns the delimiter string for this node.
* @return the delimiter string
*/
String getDelimiter() {
return delimiterTypeToString(delimiterType);
}

/**
* Sets the delimiter for this node.
* @param delimiter the delimiter string
*/
void setDelimiter(String delimiter) {
this.delimiterType = stringToDelimiterType(delimiter);
}

@Override
public final String toString() {
Expand All @@ -148,7 +170,8 @@ public final String toString() {
default:
bal = Byte.toString(balance);
}
return "[" + offset + "+" + pureLength() + "+" + delimiter.length() + "|" + line + "|" + bal + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
String delim = getDelimiter();
return "[" + offset + "+" + pureLength() + "+" + delim.length() + "|" + line + "|" + bal + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
}

/**
Expand All @@ -157,7 +180,47 @@ public final String toString() {
* @return the pure line length
*/
int pureLength() {
return length - delimiter.length();
return length - getDelimiter().length();
}
}

/**
* Converts a delimiter string to a delimiter type byte.
* @param delimiter the delimiter string
* @return the delimiter type
*/
private static byte stringToDelimiterType(String delimiter) {
if (delimiter == null || delimiter.isEmpty() || delimiter == NO_DELIM) {
return DELIM_NONE;
}
switch (delimiter) {
case "\n":
return DELIM_LF;
case "\r":
return DELIM_CR;
case "\r\n":
return DELIM_CRLF;
default:
return DELIM_NONE;
}
}

/**
* Converts a delimiter type byte to a delimiter string.
* @param delimiterType the delimiter type
* @return the delimiter string
*/
private static String delimiterTypeToString(byte delimiterType) {
switch (delimiterType) {
case DELIM_LF:
return DELIMITERS[1]; // "\n"
case DELIM_CR:
return DELIMITERS[0]; // "\r"
case DELIM_CRLF:
return DELIMITERS[2]; // "\r\n"
case DELIM_NONE:
default:
return NO_DELIM;
}
}

Expand Down Expand Up @@ -203,7 +266,7 @@ public TreeLineTracker(ListLineTracker tracker) {
node = insertAfter(node, length, delim);
}

if (node.delimiter != NO_DELIM) {
if (!NO_DELIM.equals(node.getDelimiter())) {
insertAfter(node, 0, NO_DELIM);
}

Expand Down Expand Up @@ -693,13 +756,13 @@ private void replaceInternal(Node node, String text, int length, int firstLineDe
// b) more lines to add between two chunks of the first node
// remember what we split off the first line
int remainder = firstLineDelta - length;
String remDelim = node.delimiter;
String remDelim = node.getDelimiter();

// join the first line with the first added
int consumed = info.delimiterIndex + info.delimiterLength;
int delta = consumed - firstLineDelta;
updateLength(node, delta);
node.delimiter = info.delimiter;
node.setDelimiter(info.delimiter);

// Inline addlines start
info = nextDelimiterInfo(text, consumed);
Expand Down Expand Up @@ -755,7 +818,7 @@ private void replaceFromTo(Node node, Node last, String text, int length, int fi
// join the first line with the first added
int consumed = info.delimiterIndex + info.delimiterLength;
updateLength(node, consumed - firstLineDelta);
node.delimiter = info.delimiter;
node.setDelimiter(info.delimiter);
length -= firstLineDelta;

// Inline addLines start
Expand Down Expand Up @@ -802,7 +865,7 @@ private void updateLength(Node node, int delta) {

// check deletion
final int lineDelta;
boolean delete = node.length == 0 && node.delimiter != NO_DELIM;
boolean delete = node.length == 0 && !NO_DELIM.equals(node.getDelimiter());
if (delete) {
lineDelta = -1;
} else {
Expand Down Expand Up @@ -1147,7 +1210,8 @@ protected DelimiterInfo nextDelimiterInfo(String text, int offset) {
@Override
public final String getLineDelimiter(int line) throws BadLocationException {
Node node = nodeByLine(line);
return node.delimiter == NO_DELIM ? null : node.delimiter;
String delimiter = node.getDelimiter();
return NO_DELIM.equals(delimiter) ? null : delimiter;
}

@Override
Expand Down
Loading
Loading