Skip to content

Commit 898b04f

Browse files
sfaqerclaude
andcommitted
perf(references): компактный индекс поиска вхождения по позиции
Вторичный индекс LocationRepository для findByPosition хранил Map<URI, Map<Integer, Set<SymbolOccurrence>>> с отдельным 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 <noreply@anthropic.com>
1 parent 61640a0 commit 898b04f

1 file changed

Lines changed: 63 additions & 26 deletions

File tree

src/main/java/com/github/_1c_syntax/bsl/languageserver/references/model/LocationRepository.java

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
import org.springframework.stereotype.Component;
2828

2929
import java.net.URI;
30+
import java.util.Arrays;
3031
import java.util.Collections;
32+
import java.util.Comparator;
3133
import java.util.Map;
3234
import java.util.Optional;
3335
import java.util.Set;
@@ -40,19 +42,34 @@
4042
@Component
4143
@WorkspaceScope
4244
public class LocationRepository {
45+
46+
private static final SymbolOccurrence[] EMPTY_OCCURRENCES = new SymbolOccurrence[0];
47+
48+
/**
49+
* Порядок вхождений по началу диапазона. Диапазоны вхождений непересекающиеся,
50+
* поэтому порядок тотален и однозначен.
51+
*/
52+
private static final Comparator<SymbolOccurrence> BY_RANGE_START = Comparator
53+
.comparingInt((SymbolOccurrence occurrence) -> occurrence.location().startLine())
54+
.thenComparingInt(occurrence -> occurrence.location().startCharacter());
55+
4356
/**
4457
* Список обращений к символу, сгруппированный по URI.
4558
*/
4659
private final Map<URI, Set<SymbolOccurrence>> locations = new ConcurrentHashMap<>();
4760

4861
/**
49-
* Вторичный индекс вхождений по строке их расположения — для O(1)-поиска
50-
* вхождения в позиции ({@link #findByPosition}) вместо линейного скана всего
51-
* набора вхождений документа. На одной строке может быть несколько вхождений
52-
* (разные колонки), поэтому значение — множество, разводимое по колонкам через
53-
* {@link Ranges#containsPosition} в {@link #findByPosition}.
62+
* Вторичный индекс для {@link #findByPosition}: отсортированный по началу
63+
* диапазона снимок вхождений документа. Позволяет искать вхождение в позиции
64+
* бинарным поиском без построения карты «строка → множество вхождений»
65+
* (карта с per-строчными множествами на больших проектах доминировала в
66+
* потреблении памяти индексом ссылок).
67+
* <p>
68+
* Снимок ленивый: сбрасывается при любом изменении вхождений URI
69+
* ({@link #updateLocation}, {@link #delete}) и пересобирается при первом
70+
* следующем поиске.
5471
*/
55-
private final Map<URI, Map<Integer, Set<SymbolOccurrence>>> locationsByLine = new ConcurrentHashMap<>();
72+
private final Map<URI, SymbolOccurrence[]> sortedOccurrences = new ConcurrentHashMap<>();
5673

5774
/**
5875
* Получить все обращения к символам в указанном URI.
@@ -65,30 +82,53 @@ public Stream<SymbolOccurrence> getSymbolOccurrencesByLocationUri(URI uri) {
6582
}
6683

6784
/**
68-
* Найти вхождение к символу, чей диапазон накрывает позицию. O(1)-lookup по
69-
* строке через {@link #locationsByLine}; при нескольких вхождениях на строке
70-
* разводит по колонке через {@link Ranges#containsPosition}. Диапазоны
71-
* вхождений непересекающиеся, поэтому совпадение максимум одно.
85+
* Найти вхождение к символу, чей диапазон накрывает позицию. Бинарный поиск
86+
* по отсортированному снимку вхождений документа: диапазоны вхождений
87+
* непересекающиеся, поэтому единственный кандидат — вхождение с наибольшим
88+
* началом диапазона, не превосходящим позицию.
7289
*
7390
* @param uri URI документа.
7491
* @param position позиция.
7592
* @return вхождение в позиции либо empty.
7693
*/
7794
public Optional<SymbolOccurrence> findByPosition(URI uri, Position position) {
78-
var byLine = locationsByLine.get(uri);
79-
if (byLine == null) {
80-
return Optional.empty();
95+
var sorted = sortedOccurrences.computeIfAbsent(uri, this::sortedSnapshot);
96+
var index = lastStartingAtOrBefore(sorted, position);
97+
if (index >= 0 && matches(sorted[index], position)) {
98+
return Optional.of(sorted[index]);
8199
}
82-
var bucket = byLine.get(position.getLine());
83-
if (bucket == null) {
84-
return Optional.empty();
85-
}
86-
for (var symbolOccurrence : bucket) {
87-
if (matches(symbolOccurrence, position)) {
88-
return Optional.of(symbolOccurrence);
100+
return Optional.empty();
101+
}
102+
103+
private SymbolOccurrence[] sortedSnapshot(URI uri) {
104+
var snapshot = locations.getOrDefault(uri, Collections.emptySet()).toArray(EMPTY_OCCURRENCES);
105+
Arrays.sort(snapshot, BY_RANGE_START);
106+
return snapshot;
107+
}
108+
109+
/**
110+
* Индекс последнего вхождения, чей диапазон начинается не позже позиции,
111+
* либо {@code -1}, если все вхождения начинаются позже.
112+
*/
113+
private static int lastStartingAtOrBefore(SymbolOccurrence[] sorted, Position position) {
114+
var low = 0;
115+
var high = sorted.length - 1;
116+
var found = -1;
117+
while (low <= high) {
118+
var mid = (low + high) >>> 1;
119+
if (startsAfter(sorted[mid].location(), position)) {
120+
high = mid - 1;
121+
} else {
122+
found = mid;
123+
low = mid + 1;
89124
}
90125
}
91-
return Optional.empty();
126+
return found;
127+
}
128+
129+
private static boolean startsAfter(Location location, Position position) {
130+
return location.startLine() > position.getLine()
131+
|| (location.startLine() == position.getLine() && location.startCharacter() > position.getCharacter());
92132
}
93133

94134
private static boolean matches(SymbolOccurrence symbolOccurrence, Position position) {
@@ -107,10 +147,7 @@ public void updateLocation(SymbolOccurrence symbolOccurrence) {
107147
var location = symbolOccurrence.location();
108148
locations.computeIfAbsent(location.uri(), uri -> ConcurrentHashMap.newKeySet())
109149
.add(symbolOccurrence);
110-
var byLine = locationsByLine.computeIfAbsent(location.uri(), uri -> new ConcurrentHashMap<>());
111-
for (var line = location.startLine(); line <= location.endLine(); line++) {
112-
byLine.computeIfAbsent(line, l -> ConcurrentHashMap.newKeySet()).add(symbolOccurrence);
113-
}
150+
sortedOccurrences.remove(location.uri());
114151
}
115152

116153
/**
@@ -120,6 +157,6 @@ public void updateLocation(SymbolOccurrence symbolOccurrence) {
120157
*/
121158
public void delete(URI uri) {
122159
locations.remove(uri);
123-
locationsByLine.remove(uri);
160+
sortedOccurrences.remove(uri);
124161
}
125162
}

0 commit comments

Comments
 (0)