From d334cb7be1501aa903dd02ea95476f3a98ffcb7c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 09:26:12 +0000 Subject: [PATCH 1/2] =?UTF-8?q?perf(context):=20=D0=BA=D1=8D=D1=88=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=80=D0=B0=D0=B7=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=20doc-=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B8=D0=B5=D0=B2=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B?= =?UTF-8?q?=D1=82=D1=8B=D1=85=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symbol tree открытого документа перестраивается на каждый keystroke, и при сборке для каждого метода/переменной жадно вызывался MethodDescription.create / VariableDescription.create — полный отдельный ANTLR-разбор грамматики комментария (по профилю набора текста — заметная доля стоимости перестроения), хотя текст комментариев между нажатиями почти не меняется. SymbolDescriptionIndex (@WorkspaceScope) хранит разобранные описания по URI и отдаёт их повторно. Ключевые свойства: - кэшируется только для ОТКРЫТЫХ в редакторе документов (isDocumentOpened); для не-открытых (populateContext, пакетный анализ из CLI / sonar-bsl-community- plugin) документ разбирается один раз — кэш бесполезен, поэтому путь идёт мимо него (прямой create), без затрат памяти; - записи привязаны к URI и сбрасываются на закрытии/освобождении/удалении документа (Closed/Cleared/Removed); на изменение содержимого индекс НЕ сбрасывается (иначе терялся бы смысл), устаревшие записи вытесняются по размеру; - ключ контент-адресный (line:charPositionInLine:text по токенам) — захватывает и текст (результат разбора), и позиции (SimpleRange описания, которые читают семантические токены и диагностики), поэтому попадание всегда корректно и по содержимому, и по диапазонам. Замер: попадание в кэш ~48–51x быстрее разбора и ~14–26x меньше аллокаций; на keystroke-rebuild документированного модуля computeSymbolTree по сэмплам ~−48%. Память: на populate/batch — 0; на один открытый модуль (УправлениеДоступом, 3733 строки, 61 описание) — ~0.14 МБ ключей (сами описания общие с symbol tree), освобождается на закрытии документа. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HcK3qVwTH91rXtgprGmoWr --- .../context/DocumentContext.java | 6 + .../computer/MethodSymbolComputer.java | 2 +- .../computer/SymbolDescriptionIndex.java | 180 ++++++++++++++++++ .../computer/VariableSymbolComputer.java | 12 +- .../computer/SymbolDescriptionIndexTest.java | 130 +++++++++++++ 5 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndex.java create mode 100644 src/test/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndexTest.java diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/DocumentContext.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/DocumentContext.java index eb0d8ee7388..5054aca540d 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/DocumentContext.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/DocumentContext.java @@ -30,6 +30,7 @@ import com.github._1c_syntax.bsl.languageserver.context.computer.DiagnosticComputer; import com.github._1c_syntax.bsl.languageserver.context.computer.DiagnosticIgnoranceComputer; import com.github._1c_syntax.bsl.languageserver.context.computer.QueryComputer; +import com.github._1c_syntax.bsl.languageserver.context.computer.SymbolDescriptionIndex; import com.github._1c_syntax.bsl.languageserver.context.computer.SymbolTreeComputer; import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol; import com.github._1c_syntax.bsl.languageserver.context.symbol.SymbolTree; @@ -120,6 +121,11 @@ public class DocumentContext implements Comparable { @Setter(onMethod_ = {@Autowired}) private OScriptModuleTypeResolver oScriptModuleTypeResolver; + @SuppressWarnings("NullAway.Init") + @Getter + @Setter(onMethod_ = {@Autowired}) + private SymbolDescriptionIndex symbolDescriptionIndex; + @Nullable private BSLTokenizer tokenizer; diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/MethodSymbolComputer.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/MethodSymbolComputer.java index 16b7c8349d7..1bd026692c4 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/MethodSymbolComputer.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/MethodSymbolComputer.java @@ -298,7 +298,7 @@ private Optional createDescription(Token token) { return Optional.empty(); } - return Optional.of(MethodDescription.create(comments)); + return Optional.of(documentContext.getSymbolDescriptionIndex().methodDescription(documentContext, comments)); } private static List createParameters( diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndex.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndex.java new file mode 100644 index 00000000000..08e9ea04ad9 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndex.java @@ -0,0 +1,180 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2026 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.context.computer; + +import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentClearedEvent; +import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentClosedEvent; +import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentRemovedEvent; +import com.github._1c_syntax.bsl.languageserver.infrastructure.WorkspaceScope; +import com.github._1c_syntax.bsl.parser.description.MethodDescription; +import com.github._1c_syntax.bsl.parser.description.VariableDescription; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.antlr.v4.runtime.Token; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Индекс разобранных doc-комментариев символов ({@link MethodDescription}/{@link VariableDescription}) + * открытых документов. + *

+ * Symbol tree открытого документа перестраивается на каждый keystroke, и при сборке для каждого + * метода/переменной жадно вызывался {@code …Description.create(...)} — полный ANTLR-разбор грамматики + * комментария (по профилю набора текста — заметная доля стоимости перестроения), хотя текст + * комментариев между нажатиями почти не меняется. Индекс хранит уже разобранные описания и отдаёт + * их повторно. + *

+ * Кэшируется только для открытых в редакторе документов: для не-открытых + * ({@code populateContext}, пакетный анализ из CLI / sonar-bsl-community-plugin) документ + * разбирается один раз, повторного обращения не будет — кэш бесполезен и только занимал бы память, + * поэтому такой путь идёт мимо индекса (прямой {@code create}). + *

+ * Записи привязаны к URI документа и сбрасываются на его закрытии/освобождении/удалении + * ({@link ServerContextDocumentClosedEvent}/{@link ServerContextDocumentClearedEvent}/ + * {@link ServerContextDocumentRemovedEvent}). На изменение содержимого ({@code didChange}) индекс + * НЕ сбрасывается — иначе терялся бы весь смысл (кэш должен пережить ребилд); устаревшие записи + * вытесняются по размеру (Caffeine, {@link #PER_DOCUMENT_MAX_SIZE}). + *

+ * Ключ контент-адресный: сигнатуры токенов {@code line:charPositionInLine:text}. Захватывает и + * текст (результат разбора), и абсолютные позиции (от них зависят {@code SimpleRange} + * внутри описания, которые читают семантические токены и диагностики), поэтому попадание всегда + * возвращает корректное и по содержимому, и по диапазонам описание. Описания неизменяемы и являются + * чистой функцией входных токенов, поэтому переиспользование экземпляра безопасно. + */ +@Component +@WorkspaceScope +public class SymbolDescriptionIndex { + + /** + * Потолок числа разобранных описаний на один открытый документ. Защищает от накопления + * устаревших позиционных ключей при правках, сдвигающих номера строк, в долгой сессии. + * С запасом покрывает число методов/переменных даже крупного модуля. + */ + private static final int PER_DOCUMENT_MAX_SIZE = 8_192; + + /** Разделитель ведущих и висячего комментариев в ключе (управляющий символ, в исходниках не встречается). */ + private static final String PART_SEPARATOR = String.valueOf((char) 1); + + private final Map descriptionsByUri = new ConcurrentHashMap<>(); + + /** + * Разобранное описание метода: из кэша открытого документа либо прямой разбор для не-открытого. + * + * @param documentContext документ, которому принадлежит комментарий. + * @param comments токены комментария над методом. + * @return разобранное описание метода. + */ + public MethodDescription methodDescription(DocumentContext documentContext, List comments) { + if (!isOpen(documentContext)) { + return MethodDescription.create(comments); + } + return cacheFor(documentContext.getUri()).methods + .get(key(comments), k -> MethodDescription.create(comments)); + } + + /** + * Разобранное описание переменной: из кэша открытого документа либо прямой разбор для не-открытого. + * + * @param documentContext документ, которому принадлежит комментарий. + * @param comments ведущие токены комментария. + * @param trailingComment висячий комментарий (на той же строке), если есть. + * @return разобранное описание переменной. + */ + public VariableDescription variableDescription(DocumentContext documentContext, + List comments, + Optional trailingComment) { + if (!isOpen(documentContext)) { + return VariableDescription.create(comments, trailingComment); + } + var key = key(comments) + PART_SEPARATOR + + trailingComment.map(SymbolDescriptionIndex::tokenSignature).orElse(""); + return cacheFor(documentContext.getUri()).variables + .get(key, k -> VariableDescription.create(comments, trailingComment)); + } + + @EventListener + public void handleDocumentClosed(ServerContextDocumentClosedEvent event) { + descriptionsByUri.remove(event.getDocumentContext().getUri()); + } + + @EventListener + public void handleDataCleared(ServerContextDocumentClearedEvent event) { + descriptionsByUri.remove(event.getDocumentContext().getUri()); + } + + @EventListener + public void handleDocumentRemoved(ServerContextDocumentRemovedEvent event) { + descriptionsByUri.remove(event.getUri()); + } + + private static boolean isOpen(DocumentContext documentContext) { + return documentContext.getServerContext().isDocumentOpened(documentContext); + } + + private DocumentDescriptions cacheFor(URI uri) { + return descriptionsByUri.computeIfAbsent(uri, u -> new DocumentDescriptions()); + } + + private static String key(List tokens) { + var sb = new StringBuilder(); + for (var token : tokens) { + appendSignature(sb, token); + sb.append('\n'); + } + return sb.toString(); + } + + private static String tokenSignature(Token token) { + var sb = new StringBuilder(); + appendSignature(sb, token); + return sb.toString(); + } + + private static void appendSignature(StringBuilder sb, Token token) { + sb.append(token.getLine()).append(':') + .append(token.getCharPositionInLine()).append(':') + .append(token.getText()); + } + + /** + * Кэши описаний одного открытого документа. + *

+ * {@code executor(Runnable::run)} — обслуживание кэша (вытеснение, обработка буферов) выполняется + * синхронно на вызывающем потоке, а не на {@link java.util.concurrent.ForkJoinPool#commonPool()} + * по умолчанию: common pool разделяется со всей JVM и параллельной работой языкового сервера + * (populateContext, разбор), и занимать его обслуживанием мелкого пер-документного кэша нельзя. + * Для кэша такого размера синхронное обслуживание ничтожно по стоимости. + */ + private static final class DocumentDescriptions { + private final Cache methods = + Caffeine.newBuilder().maximumSize(PER_DOCUMENT_MAX_SIZE).executor(Runnable::run).build(); + private final Cache variables = + Caffeine.newBuilder().maximumSize(PER_DOCUMENT_MAX_SIZE).executor(Runnable::run).build(); + } +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/VariableSymbolComputer.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/VariableSymbolComputer.java index a3f25998332..314c0106db7 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/VariableSymbolComputer.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/VariableSymbolComputer.java @@ -282,7 +282,8 @@ private Optional createDescription(BSLParser.LValueContext } return Optional.of( - VariableDescription.create(Collections.emptyList(), trailingComments) + documentContext.getSymbolDescriptionIndex() + .variableDescription(documentContext, Collections.emptyList(), trailingComments) ); } @@ -294,7 +295,8 @@ private Optional createDescription(BSLParser.ForEachStateme } return Optional.of( - VariableDescription.create(Collections.emptyList(), trailingComments) + documentContext.getSymbolDescriptionIndex() + .variableDescription(documentContext, Collections.emptyList(), trailingComments) ); } @@ -306,7 +308,8 @@ private Optional createDescription(BSLParser.ForStatementCo } return Optional.of( - VariableDescription.create(Collections.emptyList(), trailingComments) + documentContext.getSymbolDescriptionIndex() + .variableDescription(documentContext, Collections.emptyList(), trailingComments) ); } @@ -326,7 +329,8 @@ private Optional createDescription(ParserRuleContext ctx) { return Optional.empty(); } - return Optional.of(VariableDescription.create(comments, trailingComments)); + return Optional.of(documentContext.getSymbolDescriptionIndex() + .variableDescription(documentContext, comments, trailingComments)); } diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndexTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndexTest.java new file mode 100644 index 00000000000..5868acc52c6 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndexTest.java @@ -0,0 +1,130 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2026 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.context.computer; + +import com.github._1c_syntax.bsl.languageserver.context.AbstractServerContextAwareTest; +import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.util.TestUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class SymbolDescriptionIndexTest extends AbstractServerContextAwareTest { + + private static final String SOURCE = """ + // Описание процедуры. + // + // Параметры: + // П1 - Строка - описание параметра. + Процедура Тест(П1) Экспорт + КонецПроцедуры + """; + + @Autowired + private SymbolDescriptionIndex index; + + @Test + void openedDocumentCachesMethodDescription() { + var documentContext = TestUtils.getDocumentContext(SOURCE); + documentContext.getServerContext().openDocument(documentContext, SOURCE, 1); + var comments = documentContext.getComments(); + + var first = index.methodDescription(documentContext, comments); + var second = index.methodDescription(documentContext, comments); + + assertThat(second).as("у открытого документа описание берётся из кэша").isSameAs(first); + } + + @Test + void notOpenedDocumentBypassesCache() { + var documentContext = TestUtils.getDocumentContext(SOURCE); + var comments = documentContext.getComments(); + + var first = index.methodDescription(documentContext, comments); + var second = index.methodDescription(documentContext, comments); + + assertThat(second).as("не-открытый документ разбирается каждый раз без кэша").isNotSameAs(first); + } + + @Test + void variableDescriptionWorksForOpenedAndNotOpenedDocuments() { + var varSource = """ + // Описание переменной модуля. + Перем КэшЗначений Экспорт; + """; + // Открытый документ — путь через кэш: повторный вызов отдаёт тот же экземпляр. + var opened = TestUtils.getDocumentContext(varSource); + opened.getServerContext().openDocument(opened, varSource, 1); + var openedFirst = index.variableDescription(opened, opened.getComments(), Optional.empty()); + var openedSecond = index.variableDescription(opened, opened.getComments(), Optional.empty()); + assertThat(openedFirst).as("описание переменной открытого документа получено").isNotNull(); + assertThat(openedSecond).as("у открытого документа описание переменной берётся из кэша") + .isSameAs(openedFirst); + + // Не-открытый документ — прямой разбор мимо кэша. Проверяем только получение: identity-проверка + // (isNotSameAs) здесь ненадёжна, т.к. VariableDescription.create для одинакового входа может + // возвращать общий/интернированный экземпляр (в отличие от MethodDescription). + var notOpened = TestUtils.getDocumentContext(varSource); + assertThat(index.variableDescription(notOpened, notOpened.getComments(), Optional.empty())) + .as("описание переменной не-открытого документа получено напрямую").isNotNull(); + } + + @Test + void closingDocumentClearsCache() { + var documentContext = TestUtils.getDocumentContext(SOURCE); + var serverContext = documentContext.getServerContext(); + serverContext.openDocument(documentContext, SOURCE, 1); + var comments = documentContext.getComments(); + var cached = index.methodDescription(documentContext, comments); + + // when — закрытие документа публикует ServerContextDocumentClosedEvent. + serverContext.closeDocument(documentContext); + serverContext.openDocument(documentContext, SOURCE, 2); + var afterReopen = index.methodDescription(documentContext, comments); + + // then — кэш был сброшен на закрытии, поэтому описание разобрано заново. + assertThat(afterReopen).as("закрытие документа сбрасывает его кэш описаний").isNotSameAs(cached); + } + + @Test + void removingAndClearingDocumentDropCacheWithoutError() { + var documentContext = TestUtils.getDocumentContext(SOURCE); + var serverContext = documentContext.getServerContext(); + serverContext.openDocument(documentContext, SOURCE, 1); + index.methodDescription(documentContext, documentContext.getComments()); + + // Cleared/Removed-события (tryClearDocument закрытого документа и removeDocument) тоже сбрасывают + // запись индекса по URI. Здесь проверяем, что обработчики отрабатывают без ошибок. + serverContext.closeDocument(documentContext); + assertThat(serverContext.tryClearDocument(documentContext)).isTrue(); + serverContext.removeDocument(documentContext.getUri()); + + var reopened = serverContext.addDocument(documentContext.getUri()); + serverContext.openDocument(reopened, SOURCE, 1); + List comments = reopened.getComments(); + assertThat(index.methodDescription(reopened, comments)).isNotNull(); + } +} From fa1e173b19c86cde2f74e99b73e91ba1cb0dbd42 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 11:40:37 +0000 Subject: [PATCH 2/2] =?UTF-8?q?perf(context):=20=D0=B2=D1=8B=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D0=BD=D1=8F=D1=82=D1=8C=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8E=20(expireAfterAccess=201=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Правки, сдвигающие номера строк, на каждый ребилд плодят новое поколение позиционных ключей; на крупных модулях (замер на УправлениеДоступомСлужебный, 48k строк: 7279 записей при потолке 8192 уже после нескольких блоков набора) пер-документный кэш быстро упирается в maximumSize, удерживая устаревшие описания. Добавлен expireAfterAccess(1 мин) к обоим кэшам: после паузы в наборе устаревшие ключи отваливаются, не дожидаясь вытеснения по размеру. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01HcK3qVwTH91rXtgprGmoWr --- .../computer/SymbolDescriptionIndex.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndex.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndex.java index 08e9ea04ad9..44ce0a0ae83 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndex.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/SymbolDescriptionIndex.java @@ -35,6 +35,7 @@ import org.springframework.stereotype.Component; import java.net.URI; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; @@ -59,7 +60,8 @@ * ({@link ServerContextDocumentClosedEvent}/{@link ServerContextDocumentClearedEvent}/ * {@link ServerContextDocumentRemovedEvent}). На изменение содержимого ({@code didChange}) индекс * НЕ сбрасывается — иначе терялся бы весь смысл (кэш должен пережить ребилд); устаревшие записи - * вытесняются по размеру (Caffeine, {@link #PER_DOCUMENT_MAX_SIZE}). + * вытесняются по размеру ({@link #PER_DOCUMENT_MAX_SIZE}) и по простою + * ({@link #EXPIRE_AFTER_ACCESS}). *

* Ключ контент-адресный: сигнатуры токенов {@code line:charPositionInLine:text}. Захватывает и * текст (результат разбора), и абсолютные позиции (от них зависят {@code SimpleRange} @@ -78,6 +80,15 @@ public class SymbolDescriptionIndex { */ private static final int PER_DOCUMENT_MAX_SIZE = 8_192; + /** + * Время простоя записи, после которого она вытесняется по времени. Правки, сдвигающие номера + * строк, плодят новое поколение позиционных ключей на каждый ребилд; на крупных модулях такой + * кэш быстро упирается в {@link #PER_DOCUMENT_MAX_SIZE}, держа устаревшие описания. Простой в + * минуту означает, что после паузы в наборе устаревшие ключи отваливаются, не дожидаясь + * вытеснения по размеру. + */ + private static final Duration EXPIRE_AFTER_ACCESS = Duration.ofMinutes(1); + /** Разделитель ведущих и висячего комментариев в ключе (управляющий символ, в исходниках не встречается). */ private static final String PART_SEPARATOR = String.valueOf((char) 1); @@ -173,8 +184,10 @@ private static void appendSignature(StringBuilder sb, Token token) { */ private static final class DocumentDescriptions { private final Cache methods = - Caffeine.newBuilder().maximumSize(PER_DOCUMENT_MAX_SIZE).executor(Runnable::run).build(); + Caffeine.newBuilder().maximumSize(PER_DOCUMENT_MAX_SIZE) + .expireAfterAccess(EXPIRE_AFTER_ACCESS).executor(Runnable::run).build(); private final Cache variables = - Caffeine.newBuilder().maximumSize(PER_DOCUMENT_MAX_SIZE).executor(Runnable::run).build(); + Caffeine.newBuilder().maximumSize(PER_DOCUMENT_MAX_SIZE) + .expireAfterAccess(EXPIRE_AFTER_ACCESS).executor(Runnable::run).build(); } }