Skip to content

perf(references): массивы вместо concurrent-множеств в LocationRepository (−305 МиБ, чтение 42×, индексация 37×)#4271

Merged
nixel2007 merged 2 commits into
developfrom
perf/location-repository-arrays
Jul 14, 2026
Merged

perf(references): массивы вместо concurrent-множеств в LocationRepository (−305 МиБ, чтение 42×, индексация 37×)#4271
nixel2007 merged 2 commits into
developfrom
perf/location-repository-arrays

Conversation

@nixel2007

@nixel2007 nixel2007 commented Jul 14, 2026

Copy link
Copy Markdown
Member

Что и зачем

LocationRepository хранил вхождения документа как Map<URI, ConcurrentHashMap.newKeySet()> — concurrent-множество на каждый URI. На больших проектах это ~1 ConcurrentHashMap$Node на каждое вхождение: на cpm (~13k модулей, 5.98M вхождений) — миллионы узлов и сотни МБ чистого оверхеда контейнеров.

Что сделано

  • Map<URI, SymbolOccurrence[]> вместо множеств. Вхождения одного документа пишутся одним потоком (перестроение документа сериализовано), а ReferenceIndex.replaceReferences заменяет весь набор расположений документа одним атомарным swap-ом ссылки на массив — конкурентные читатели всегда видят согласованный снимок без «пустого окна». Символьная сторона индекса остаётся инкрементальной.
  • Вторичный индекс findByPosition (бинарный поиск по sorted-снимку) не тронут.
  • Мёртвый код (второй коммит): перевод на replaceOccurrences сделал updateLocation/deleteAll и ReferenceIndex.saveOccurrence/addXxx мёртвыми (звались только из тестов) — удалены; тест переведён на прод-API.

Замеры на cpm

Память (live-heap дампы, full-vs-full, счётчики данных совпадают):

класс было стало
ConcurrentHashMap$Node 11.46M 5.32M (−6.13M, −255 МиБ)
таблицы [LConcurrentHashMap$Node; 163 МиБ 74 МиБ (−89 МиБ)
SymbolOccurrence[] (новое) +39 МиБ

Итого ≈ −305 МиБ.

Время (JMH, cpm-распределение occ/URI, avgt ns/op, меньше=лучше):

метод было стало итог
getSymbolOccurrencesByLocationUri (чтение, горячий) 8537 201 42× быстрее
findByPosition (чтение, очень горячий) 53.9 51.4
индексация документа (replaceOccurrences vs цикл newKeySet.add) 47643 1300 37× быстрее

Чистая победа: и память, и время на горячих путях (чтение + индексация), без регрессий.

Тестирование

ReferenceIndexTest + LocationRepositoryTest — 33/33 зелёные, полный compileJava/compileTestJava прошёл.

🤖 Generated with Claude Code

https://claude.ai/code/session_017AZeyPcsbdUBaLUGWbhrCi

Summary by CodeRabbit

  • Bug Fixes

    • Improved reference index updates to keep symbol and location data synchronized.
    • Ensured replacement operations produce consistent, deterministic reference results.
    • Improved isolation when updating references across separate workspaces.
  • Refactor

    • Simplified internal reference and location storage and replacement behavior.

nixel2007 and others added 2 commits July 14, 2026 20:20
…tory

Заменяет `Map<URI, ConcurrentHashMap.newKeySet()>` на `Map<URI,
SymbolOccurrence[]>`. Вхождения одного документа пишутся одним потоком
(перестроение документа сериализовано), а `ReferenceIndex.replaceReferences`
теперь заменяет весь набор расположений документа одним атомарным swap-ом
ссылки на массив — конкурентные читатели видят согласованный снимок без
«пустого окна», плотнее и без ~1 `ConcurrentHashMap$Node` на каждое вхождение.

Символьная сторона индекса остаётся инкрементальной (добавить новые, удалить
устаревшие). Гранулярный `updateLocation`/`deleteAll` (прямой API `addXxx`,
используется тестами) сохранён как copy-on-write. Вторичный индекс
`findByPosition` не изменился — читает тот же массив.

На cpm (~13k модулей, 5.98M вхождений) `LocationRepository` держал ~5.98M
`ConcurrentHashMap$Node` (~250 МБ + таблицы), теперь — массивы (~48 МБ).

ReferenceIndexTest + LocationRepositoryTest: зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017AZeyPcsbdUBaLUGWbhrCi
Перевод записи расположений на replaceOccurrences сделал мёртвыми (остались
только в тестах / без вызовов):
  - LocationRepository.updateLocation, LocationRepository.deleteAll;
  - ReferenceIndex.saveOccurrence + addMethodCall/addModuleReference/
    addVariableUsage.

Билдеры *Occurrence оставлены (их зовёт ReferenceIndexFiller). Тест
crossWorkspaceIsolation переведён на прод-API replaceReferences.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017AZeyPcsbdUBaLUGWbhrCi
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Reference replacement now builds deterministic occurrence sets, deletes stale symbol records, and atomically replaces per-URI location snapshots. LocationRepository uses immutable arrays and removes incremental update APIs. Tests now inject occurrences through replaceReferences.

Changes

Reference replacement and location snapshots

Layer / File(s) Summary
Location snapshot storage and replacement API
src/main/java/.../references/model/LocationRepository.java
Per-URI occurrences use immutable arrays, sorted lookup snapshots are rebuilt from cloned arrays, and replaceOccurrences replaces the previous incremental update and partial deletion APIs.
Reference reconciliation and supporting API cleanup
src/main/java/.../references/ReferenceIndex.java, src/test/java/.../references/ReferenceIndexTest.java
replaceReferences uses ordered deduplication, persists only new occurrences, replaces location data, removes obsolete helpers, and updates the workspace-isolation test.
Estimated code review effort: 3 (Moderate) ~20 minutes

Possibly related PRs

Suggested reviewers: sfaqer

Sequence Diagram(s)

sequenceDiagram
  participant ReferenceIndex
  participant SymbolOccurrenceRepository
  participant LocationRepository
  ReferenceIndex->>ReferenceIndex: Build replacement occurrences
  ReferenceIndex->>SymbolOccurrenceRepository: Delete stale occurrences
  ReferenceIndex->>SymbolOccurrenceRepository: Save new occurrences
  ReferenceIndex->>LocationRepository: Replace URI snapshot
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: switching LocationRepository from concurrent sets to arrays with performance gains.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/location-repository-arrays

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/main/java/com/github/_1c_syntax/bsl/languageserver/references/model/LocationRepository.java (1)

147-160: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sort occurrences eagerly to eliminate the secondary index and halve memory usage.

Instead of maintaining a secondary sortedOccurrences map and lazily sorting it on read, you can sort the array eagerly in replaceOccurrences before inserting it into locations. This completely eliminates the need for the sortedOccurrences map, halving the memory usage for arrays (avoiding duplicate arrays per URI) and removing the lazy-evaluation complexity.

Since occurrences.toArray() allocates a new array that is not yet visible to other threads, sorting it in-place before locations.put() is completely thread-safe.

♻️ Proposed refactor for the entire file

Update replaceOccurrences to sort the array before insertion:

   public void replaceOccurrences(URI uri, Collection<SymbolOccurrence> occurrences) {
     if (occurrences.isEmpty()) {
       locations.remove(uri);
     } else {
-      locations.put(uri, occurrences.toArray(EMPTY_OCCURRENCES));
+      var array = occurrences.toArray(EMPTY_OCCURRENCES);
+      Arrays.sort(array, BY_RANGE_START);
+      locations.put(uri, array);
     }
-    sortedOccurrences.remove(uri);
   }

Simplify findByPosition and remove the sortedSnapshot method:

-  public Optional<SymbolOccurrence> findByPosition(URI uri, Position position) {
-    var sorted = sortedOccurrences.computeIfAbsent(uri, this::sortedSnapshot);
-    var index = lastStartingAtOrBefore(sorted, position);
-    if (index >= 0 && matches(sorted[index], position)) {
-      return Optional.of(sorted[index]);
-    }
-    return Optional.empty();
-  }
-
-  private SymbolOccurrence[] sortedSnapshot(URI uri) {
-    var snapshot = locations.getOrDefault(uri, EMPTY_OCCURRENCES).clone();
-    Arrays.sort(snapshot, BY_RANGE_START);
-    return snapshot;
-  }
+  public Optional<SymbolOccurrence> findByPosition(URI uri, Position position) {
+    var sorted = locations.getOrDefault(uri, EMPTY_OCCURRENCES);
+    var index = lastStartingAtOrBefore(sorted, position);
+    if (index >= 0 && matches(sorted[index], position)) {
+      return Optional.of(sorted[index]);
+    }
+    return Optional.empty();
+  }

Remove the sortedOccurrences field entirely:

-  /**
-   * Вторичный индекс для {`@link` `#findByPosition`}: отсортированный по началу
-   ...
-   */
-  private final Map<URI, SymbolOccurrence[]> sortedOccurrences = new ConcurrentHashMap<>();

Also, remove the sortedOccurrences.remove(uri) call from the delete(URI uri) method (Line 169).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/references/model/LocationRepository.java`
around lines 147 - 160, Update LocationRepository.replaceOccurrences to convert
occurrences to an array, sort that array before publishing it through locations,
and remove the sortedOccurrences invalidation. Remove the sortedOccurrences
field and sortedSnapshot helper, simplify findByPosition to use the eagerly
sorted locations array, and remove sortedOccurrences cleanup from delete(URI
uri).
src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java (1)

183-199: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Skip redundant database operations for duplicate occurrences.

The Javadoc notes that duplicates are permitted in newOccurrences. However, when a duplicate is encountered, !stale.remove(occurrence) will return true on the second pass (because it was already removed on the first), triggering an unnecessary symbolOccurrenceRepository.save(occurrence) operation.

You can use the boolean return value of replacement.add(occurrence) to easily bypass the lookup and save operations for duplicates.

♻️ Proposed refactor
-    var replacement = new LinkedHashSet<SymbolOccurrence>();
-    for (var occurrence : newOccurrences) {
-      replacement.add(occurrence);
-      // Успешное удаление из stale означает, что вхождение уже есть в индексе —
-      // символьную сторону не трогаем; остаток stale после цикла подлежит удалению.
-      if (!stale.remove(occurrence)) {
-        symbolOccurrenceRepository.save(occurrence);
-      }
-    }
+    var replacement = new LinkedHashSet<SymbolOccurrence>();
+    for (var occurrence : newOccurrences) {
+      if (replacement.add(occurrence)) {
+        // Успешное удаление из stale означает, что вхождение уже есть в индексе —
+        // символьную сторону не трогаем; остаток stale после цикла подлежит удалению.
+        if (!stale.remove(occurrence)) {
+          symbolOccurrenceRepository.save(occurrence);
+        }
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java`
around lines 183 - 199, Update the occurrence loop so the boolean result of
replacement.add(occurrence) identifies duplicates and skips stale removal and
symbolOccurrenceRepository.save for occurrences already present in replacement.
Retain the existing stale cleanup and save behavior for the first occurrence of
each value, then continue replacing the location occurrences with the complete
replacement set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/references/model/LocationRepository.java`:
- Around line 147-160: Update LocationRepository.replaceOccurrences to convert
occurrences to an array, sort that array before publishing it through locations,
and remove the sortedOccurrences invalidation. Remove the sortedOccurrences
field and sortedSnapshot helper, simplify findByPosition to use the eagerly
sorted locations array, and remove sortedOccurrences cleanup from delete(URI
uri).

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java`:
- Around line 183-199: Update the occurrence loop so the boolean result of
replacement.add(occurrence) identifies duplicates and skips stale removal and
symbolOccurrenceRepository.save for occurrences already present in replacement.
Retain the existing stale cleanup and save behavior for the first occurrence of
each value, then continue replacing the location occurrences with the complete
replacement set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cce0b5fb-d967-41fd-9255-0556d5f9c36e

📥 Commits

Reviewing files that changed from the base of the PR and between 316ff75 and 2ccee05.

📒 Files selected for processing (3)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/references/model/LocationRepository.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexTest.java

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Test Results

 3 636 files  ±0   3 636 suites  ±0   1h 45m 11s ⏱️ + 5m 33s
 3 593 tests ±0   3 575 ✅ ±0   18 💤 ±0  0 ❌ ±0 
21 558 runs  ±0  21 446 ✅ ±0  112 💤 ±0  0 ❌ ±0 

Results for commit 2ccee05. ± Comparison against base commit 316ff75.

♻️ This comment has been updated with latest results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant