Skip to content

perf(context): мемоизировать findCommonModule (workspace-scoped кэш)#4186

Merged
nixel2007 merged 4 commits into
developfrom
claude/perf-findcommonmodule-cache
Jun 22, 2026
Merged

perf(context): мемоизировать findCommonModule (workspace-scoped кэш)#4186
nixel2007 merged 4 commits into
developfrom
claude/perf-findcommonmodule-cache

Conversation

@nixel2007

@nixel2007 nixel2007 commented Jun 22, 2026

Copy link
Copy Markdown
Member

Описание

CF.findCommonModule(name) при заполнении индекса ссылок вызывается на каждый идентификатор (call-site, комплексный идентификатор, параметр процедуры) — десятки тысяч раз на каждый keystroke-ребилд — и каждый раз заново проходит по case-insensitive карте конфигурации (CaseInsensitiveMap): посимвольное сворачивание регистра запроса (CharacterData.getProperties, StringLatin1.compareToCI).

Резолв общего модуля по имени зависит только от конфигурации воркспейса. Добавлен мемоизирующий ServerContext.findCommonModule поверх ограниченного Caffeine-кэша. Все вызывающие переведены на него: ReferenceIndexFiller (4 места) и MdoRefBuilder (горячий путь), а также диагностики MissingEventSubscriptionHandler / CommonModuleAssign / ScheduledJobHandler и documentlink SeeReferenceDocumentLinkSupplier (холодные пути) — теперь весь резолв идёт через один кэш.

Почему так

  • Кэш — @WorkspaceScope @Bean (CacheConfiguration, proxyMode = INTERFACES), инжектится в ServerContext. Один экземпляр на воркспейс: резолв зависит от конфигурации, поэтому общий singleton-кэш смешивал бы воркспейсы. ServerContext создаётся внутри WorkspaceContextHolder.forUri(...) и уже потребляет такие же @WorkspaceScope-бины (populateContextExecutor и др.) — проверенный паттерн.
  • Не @Cacheable — AOP-прокси + SpEL на ультра-горячем пути (десятки тысяч вызовов на keystroke) и непропуск внутренних self-вызовов.
  • Ограничен по размеру (maximumSize): кэшируются и промахи (основной поток вызовов). На полной типовой конфигурации SSL 3.2 намерено ~32 000 уникальных имён, проходящих через findCommonModule; потолок взят с запасом — 131072.
  • Ключ — сырой текст идентификатора (намеренно не нормализуется в нижний регистр). Замер JMH (-prof gc): попадание по сырому ключу — 5.5 ns/op, ~0 B/op; вариант с toLowerCase(Locale.ROOT)141 ns/op, 84.5 B/op (×26 по CPU + аллокация на каждый вызов). Сам резолв внутри остаётся регистронезависимым.
  • Сбрасывается в ServerContext.clear().

Замер (CPU)

JFR sampling, рабочий сценарий набора текста в общем модуле УправлениеДоступомСлужебный (SSL 3.2, 48 399 строк):

метрика до после
MdoRefBuilder.getMdoRef self-time 3.68% 1.87%
CF.findCommonModule (inclusive) 1.97% ~0% (уходит из профиля ребилда)
CharacterData.getProperties (case-folding) 4.68% 3.90%
StringLatin1.compareToCI 2.57% 1.76%

Чеклист

Общие

  • Отладочные, закомментированные и прочие, не имеющие смысла участки кода удалены
  • Изменения покрыты тестами (поведение не меняется; покрыто существующими ReferenceIndexFillerTest / ReferenceIndexTest / ReferenceIndexReferenceFinderTest / ServerContextTest и тестами затронутых диагностик)
  • Обязательные действия перед коммитом выполнены

🤖 Generated with Claude Code

CF.findCommonModule(name) при заполнении индекса ссылок вызывается на каждый
идентификатор (call-site, комплексный идентификатор, параметр процедуры) —
десятки тысяч раз на каждый keystroke-ребилд — и каждый раз заново проходит по
case-insensitive карте конфигурации (case-folding: CharacterData.getProperties,
StringLatin1.compareToCI).

Резолв общего модуля по имени зависит только от конфигурации воркспейса.
Добавлен мемоизирующий ServerContext.findCommonModule поверх ограниченного
Caffeine-кэша. Кэш оформлен как @WorkspaceScope @bean (CacheConfiguration,
proxyMode=INTERFACES) и инжектится в ServerContext — один экземпляр на воркспейс
(резолв зависит от конфигурации, поэтому общий singleton-кэш смешивал бы
воркспейсы). Сбрасывается в ServerContext.clear(). Горячие вызывающие в
ReferenceIndexFiller и MdoRefBuilder переведены на ServerContext.findCommonModule.

Кэшируются и промахи (основной поток вызовов), поэтому кэш ограничен по размеру:
на полной типовой конфигурации SSL 3.2 намерено ~32 000 уникальных имён,
проходящих через findCommonModule; потолок взят с запасом (131072).

Замер (JFR sampling, набор текста в УправлениеДоступомСлужебный SSL 3.2,
48 399 строк): MdoRefBuilder.getMdoRef self-time 3.68% -> 1.87%,
CF.findCommonModule уходит из профиля ребилда.
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

A workspace-scoped Caffeine cache (Cache<String, Optional<CommonModule>>) is introduced in CacheConfiguration and injected into ServerContext, which gains a new public findCommonModule(String name) method that memoizes lookups delegating to getConfiguration().findCommonModule(...). The clear() method is extended to invalidate this cache. All existing call sites in ReferenceIndexFiller and MdoRefBuilder are updated to use the new cached method.

Changes

Common module cache and call site migration

Layer / File(s) Summary
Cache bean and ServerContext.findCommonModule API
src/main/java/com/github/_1c_syntax/bsl/languageserver/infrastructure/CacheConfiguration.java, src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java
CacheConfiguration adds COMMON_MODULE_CACHE_SIZE constant and a @WorkspaceScope-annotated Caffeine Cache<String, Optional<CommonModule>> bean. ServerContext injects this cache, exposes findCommonModule(String name) as a memoizing public method using raw name keys, and calls commonModuleCache.invalidateAll() in clear().
Call site migration
src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFiller.java, src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/MdoRefBuilder.java
Four lookup sites in ReferenceIndexFiller (addModuleReferenceForCommonModuleIdentifier, registerCommonModuleMethodOnGetter, calcParams, visitAssignment) and one in MdoRefBuilder replace getServerContext().getConfiguration().findCommonModule(...) with getServerContext().findCommonModule(...).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • 1c-syntax/bsl-language-server#4180: Also caches results in DocumentContext.getMdoRef() which internally calls MdoRefBuilder.getMdoRef(...) — the same method whose common-module lookup path this PR updates.

Poem

🐇 Hop, hop, the cache is set,
No more config calls to fret!
findCommonModule leads the way,
Memoized results here to stay.
invalidateAll() clears the run —
A faster server, done and done! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: introducing memoization via workspace-scoped caching for the findCommonModule method, which is the primary performance optimization across all modified files.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 claude/perf-findcommonmodule-cache

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 and usage tips.

@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 (1)
src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java (1)

455-466: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Normalize cache keys before memoization.

Line 464 currently caches by raw identifier text. Since resolution is case-insensitive, different casings of the same module name produce separate cache entries and reduce hit ratio. Normalize once (e.g., Locale.ROOT) before commonModuleCache.get(...).

Proposed diff
+import java.util.Locale;
 import java.util.Optional;
@@
   public Optional<CommonModule> findCommonModule(String name) {
-    return commonModuleCache.get(name, key -> getConfiguration().findCommonModule(key));
+    var normalizedName = name.toLowerCase(Locale.ROOT);
+    return commonModuleCache.get(normalizedName, key -> getConfiguration().findCommonModule(key));
   }
🤖 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/context/ServerContext.java`
around lines 455 - 466, In the findCommonModule method, the name parameter used
as a cache key should be normalized before being passed to
commonModuleCache.get() to improve cache hit ratio. Since module name resolution
is case-insensitive, normalize the name parameter using Locale.ROOT (for
example, name.toLowerCase(Locale.ROOT)) before using it in the cache lookup to
ensure that different casings of the same module name produce a single cache
entry.
🤖 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/context/ServerContext.java`:
- Around line 455-466: In the findCommonModule method, the name parameter used
as a cache key should be normalized before being passed to
commonModuleCache.get() to improve cache hit ratio. Since module name resolution
is case-insensitive, normalize the name parameter using Locale.ROOT (for
example, name.toLowerCase(Locale.ROOT)) before using it in the cache lookup to
ensure that different casings of the same module name produce a single cache
entry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0386625b-3dc9-48a1-8e81-ad285b733564

📥 Commits

Reviewing files that changed from the base of the PR and between 8b63770 and ed00377.

📒 Files selected for processing (4)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/infrastructure/CacheConfiguration.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndexFiller.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/MdoRefBuilder.java

Резолв общего модуля регистронезависимый, поэтому ключ кэша нормализуется через
toLowerCase(Locale.ROOT) перед обращением — разные написания одного имени дают
одну запись, лучше hit-ratio и меньше distinct-ключей (как TypeRegistry.resolve).
@github-actions

Copy link
Copy Markdown
Contributor

Test Results

 3 528 files  ±0   3 528 suites  ±0   1h 34m 56s ⏱️ +41s
 3 461 tests ±0   3 443 ✅ ±0   18 💤 ±0  0 ❌ ±0 
20 766 runs  ±0  20 654 ✅ ±0  112 💤 ±0  0 ❌ ±0 

Results for commit 4cfba56. ± Comparison against base commit 8b63770.

claude added 2 commits June 22, 2026 17:48
…мализации)

toLowerCase(Locale.ROOT) на каждый вызов реинжектил на горячий путь посимвольное
сворачивание регистра + аллокацию строки — ровно тот расход, ради устранения
которого кэш и делался. Выгода (схлопывание редких регистровых вариантов одного
имени) околонулевая: distinct-ключей на ssl_3_2 ~32k что с нормализацией, что без.
Возвращаем сырой ключ; резолв внутри остаётся регистронезависимым.
…text

Холодные пути (диагностики MissingEventSubscriptionHandler / CommonModuleAssign /
ScheduledJobHandler и documentlink SeeReferenceDocumentLinkSupplier) переведены с
прямого getConfiguration().findCommonModule на ServerContext.findCommonModule —
теперь все вызовы идут через один кэш.
@sonarqubecloud

Copy link
Copy Markdown

@nixel2007
nixel2007 merged commit b4b61f2 into develop Jun 22, 2026
34 of 35 checks passed
@nixel2007
nixel2007 deleted the claude/perf-findcommonmodule-cache branch June 22, 2026 18:27
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.

2 participants