From 898b04f2cda36c28f66c013e5811ccfdc77acf81 Mon Sep 17 00:00:00 2001 From: sfaqer Date: Sat, 11 Jul 2026 13:31:06 +1000 Subject: [PATCH] =?UTF-8?q?perf(references):=20=D0=BA=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=BD=D1=8B=D0=B9=20=D0=B8=D0=BD=D0=B4=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B2?= =?UTF-8?q?=D1=85=D0=BE=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B7=D0=B8=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вторичный индекс LocationRepository для findByPosition хранил Map>> с отдельным ConcurrentHashMap-множеством на каждую строку кода с вхождением. На больших проектах это доминирующая статья расхода памяти индекса ссылок: на УПП (~9500 модулей, 2.5M вхождений) — ~1.5M инстансов ConcurrentHashMap, 175 МБ таблиц Node[], ~4.6M boxed Integer; суммарно ~450 МБ. Вместо карты «строка → множество» — ленивый отсортированный по началу диапазона снимок вхождений документа (SymbolOccurrence[]) и бинарный поиск. Диапазоны вхождений непересекающиеся, поэтому единственный кандидат — вхождение с наибольшим началом диапазона, не превосходящим позицию. Снимок сбрасывается при любом изменении вхождений URI и пересобирается при первом следующем поиске; updateLocation больше не раскладывает вхождение по строкам. Замер (analyze УПП, jcmd GC.class_histogram, live): инстансы CHM 1.52M → 14K, узлы CHM 10M → 5.9M, таблицы Node[] 175 → 43 МБ; SARIF-отчёт эквивалентен базовому (идентичные счётчики правил), время анализа не выросло (2:34 → 2:21). Co-Authored-By: Claude Fable 5 --- .../references/model/LocationRepository.java | 89 +++++++++++++------ 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/model/LocationRepository.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/model/LocationRepository.java index 5125b130184..2d7ed926a2a 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/model/LocationRepository.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/model/LocationRepository.java @@ -27,7 +27,9 @@ import org.springframework.stereotype.Component; import java.net.URI; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -40,19 +42,34 @@ @Component @WorkspaceScope public class LocationRepository { + + private static final SymbolOccurrence[] EMPTY_OCCURRENCES = new SymbolOccurrence[0]; + + /** + * Порядок вхождений по началу диапазона. Диапазоны вхождений непересекающиеся, + * поэтому порядок тотален и однозначен. + */ + private static final Comparator BY_RANGE_START = Comparator + .comparingInt((SymbolOccurrence occurrence) -> occurrence.location().startLine()) + .thenComparingInt(occurrence -> occurrence.location().startCharacter()); + /** * Список обращений к символу, сгруппированный по URI. */ private final Map> locations = new ConcurrentHashMap<>(); /** - * Вторичный индекс вхождений по строке их расположения — для O(1)-поиска - * вхождения в позиции ({@link #findByPosition}) вместо линейного скана всего - * набора вхождений документа. На одной строке может быть несколько вхождений - * (разные колонки), поэтому значение — множество, разводимое по колонкам через - * {@link Ranges#containsPosition} в {@link #findByPosition}. + * Вторичный индекс для {@link #findByPosition}: отсортированный по началу + * диапазона снимок вхождений документа. Позволяет искать вхождение в позиции + * бинарным поиском без построения карты «строка → множество вхождений» + * (карта с per-строчными множествами на больших проектах доминировала в + * потреблении памяти индексом ссылок). + *

+ * Снимок ленивый: сбрасывается при любом изменении вхождений URI + * ({@link #updateLocation}, {@link #delete}) и пересобирается при первом + * следующем поиске. */ - private final Map>> locationsByLine = new ConcurrentHashMap<>(); + private final Map sortedOccurrences = new ConcurrentHashMap<>(); /** * Получить все обращения к символам в указанном URI. @@ -65,30 +82,53 @@ public Stream getSymbolOccurrencesByLocationUri(URI uri) { } /** - * Найти вхождение к символу, чей диапазон накрывает позицию. O(1)-lookup по - * строке через {@link #locationsByLine}; при нескольких вхождениях на строке - * разводит по колонке через {@link Ranges#containsPosition}. Диапазоны - * вхождений непересекающиеся, поэтому совпадение максимум одно. + * Найти вхождение к символу, чей диапазон накрывает позицию. Бинарный поиск + * по отсортированному снимку вхождений документа: диапазоны вхождений + * непересекающиеся, поэтому единственный кандидат — вхождение с наибольшим + * началом диапазона, не превосходящим позицию. * * @param uri URI документа. * @param position позиция. * @return вхождение в позиции либо empty. */ public Optional findByPosition(URI uri, Position position) { - var byLine = locationsByLine.get(uri); - if (byLine == null) { - return Optional.empty(); + var sorted = sortedOccurrences.computeIfAbsent(uri, this::sortedSnapshot); + var index = lastStartingAtOrBefore(sorted, position); + if (index >= 0 && matches(sorted[index], position)) { + return Optional.of(sorted[index]); } - var bucket = byLine.get(position.getLine()); - if (bucket == null) { - return Optional.empty(); - } - for (var symbolOccurrence : bucket) { - if (matches(symbolOccurrence, position)) { - return Optional.of(symbolOccurrence); + return Optional.empty(); + } + + private SymbolOccurrence[] sortedSnapshot(URI uri) { + var snapshot = locations.getOrDefault(uri, Collections.emptySet()).toArray(EMPTY_OCCURRENCES); + Arrays.sort(snapshot, BY_RANGE_START); + return snapshot; + } + + /** + * Индекс последнего вхождения, чей диапазон начинается не позже позиции, + * либо {@code -1}, если все вхождения начинаются позже. + */ + private static int lastStartingAtOrBefore(SymbolOccurrence[] sorted, Position position) { + var low = 0; + var high = sorted.length - 1; + var found = -1; + while (low <= high) { + var mid = (low + high) >>> 1; + if (startsAfter(sorted[mid].location(), position)) { + high = mid - 1; + } else { + found = mid; + low = mid + 1; } } - return Optional.empty(); + return found; + } + + private static boolean startsAfter(Location location, Position position) { + return location.startLine() > position.getLine() + || (location.startLine() == position.getLine() && location.startCharacter() > position.getCharacter()); } private static boolean matches(SymbolOccurrence symbolOccurrence, Position position) { @@ -107,10 +147,7 @@ public void updateLocation(SymbolOccurrence symbolOccurrence) { var location = symbolOccurrence.location(); locations.computeIfAbsent(location.uri(), uri -> ConcurrentHashMap.newKeySet()) .add(symbolOccurrence); - var byLine = locationsByLine.computeIfAbsent(location.uri(), uri -> new ConcurrentHashMap<>()); - for (var line = location.startLine(); line <= location.endLine(); line++) { - byLine.computeIfAbsent(line, l -> ConcurrentHashMap.newKeySet()).add(symbolOccurrence); - } + sortedOccurrences.remove(location.uri()); } /** @@ -120,6 +157,6 @@ public void updateLocation(SymbolOccurrence symbolOccurrence) { */ public void delete(URI uri) { locations.remove(uri); - locationsByLine.remove(uri); + sortedOccurrences.remove(uri); } }