Skip to content

perf(diagnostics): считать диагностики на bounded-пуле вместо ForkJoinPool#4264

Merged
nixel2007 merged 3 commits into
developfrom
claude/perf-diagnostic-computer-executor
Jul 12, 2026
Merged

perf(diagnostics): считать диагностики на bounded-пуле вместо ForkJoinPool#4264
nixel2007 merged 3 commits into
developfrom
claude/perf-diagnostic-computer-executor

Conversation

@nixel2007

@nixel2007 nixel2007 commented Jul 12, 2026

Copy link
Copy Markdown
Member

Описание

DefaultDiagnosticComputer оборачивал расчёт в CompletableFuture.supplyAsync(...).join() на diagnosticComputerExecutor (ForkJoinPool) и разворачивал анализаторы через parallelStream(). Когда getDiagnostics() вызывается из воркера ForkJoinPool — а это и пакетный analyze (cliExecutor), и анализ проекта при старте LSP (AnalyzeProjectOnStartanalyzeOnStartExecutor) — этот join() является managed blocking: пул поднимает компенсирующие потоки, чтобы держать номинальный параллелизм, и каждый лишний воркер материализует ещё один DocumentContext. Естественный потолок «≈ число ядер» теряется.

Фикс

Сабмитить каждый анализатор отдельной задачей на diagnosticComputerExecutor и собирать результат через Future.get(). Блокировка на get() обычного ThreadPoolExecutor не проходит через ForkJoinPool.managedBlock, поэтому блокирующий вызов из воркера ForkJoinPool не плодит компенсирующие потоки. diagnosticComputerExecutor переведён с ForkJoinPool на bounded ThreadPoolExecutor (контекст workspace несёт ContextPropagatingExecutorService per-task); единственный потребитель — DefaultDiagnosticComputer.

В отличие от #4263 (bounded-пул только на внешней оси analyze), фикс на уровне diagnostic computer снимает компенсацию на всех путях, где диагностики считаются из ForkJoinPool, — включая LSP-путь AnalyzeProjectOnStart, которого #4263 не касался.

Замеры (JFR + /proc, SSL 3.2, 4 ядра, commonPoolParallelism=4, тёплый кэш)

Вывод идентичен до/после (analyze — 72160 диагностик, LSP — 72141, 2184 файла).

метрика analyze (было → стало) LSP analyzeOnStart (было → стало)
thread-starts на блокирующем пуле (cli-* / analyze-on-start-*) 17 → 3 17 → 3
пик одновременных compute() (≈ живых DocumentContext) 16 → 3
sustained active JVM-потоков (JFR) 45 → 31 ~45 → ~32

Про память. На просторной куче (-Xmx6g) peak RSS почти не меняется — он определяется темпом аллокаций (одинаковым: работа та же) и нежеланием JVM отдавать страницы ОС, а не live-set. Эффект проявляется под давлением кучи: при зажатом -Xmx baseline с 16 живыми документами упирается в потолок заметно чаще (Full GC при -Xmx1000m: 8 → 2; при -Xmx700m: 76 → 24), при паритетном выводе. Это ровно механика §4.2 #4248 (много ядер + крупная конфигурация → компенсация плодит десятки живых DocumentContext → 16–22 ГиБ / не заканчивается).

Связанные задачи

Пункт §4.2 архитектурного разбора #4248. Альтернатива #4263 (закрывается в пользу этого PR как более широкая по охвату). Продолжение серии #4249#4258.

Closes

Чеклист

Общие

  • Ветка PR обновлена из develop
  • Отладочные, закомментированные и прочие, не имеющие смысла участки кода удалены
  • Изменения покрыты тестами (DiagnosticProviderTest, AnalyzeCommandTest, ArchitectureTest — зелёные; вывод SARIF идентичен на реальном прогоне SSL, в т.ч. по LSP-пути)

Дополнительно

diagnosticComputerExecutor — единственный потребитель — теперь bounded ThreadPoolExecutor размером commonPoolParallelism; при желании легко вынести в конфиг. Взаимоблокировки нет: внешние пулы (cliExecutor/analyzeOnStartExecutor) и diagnosticComputerExecutor — разные пулы без обратной зависимости.


Generated by Claude Code

Summary by CodeRabbit

  • Performance & Reliability
    • Improved diagnostic processing using dedicated per-workspace thread pools for more predictable execution.
    • Added isolation for failures in individual diagnostics so one error won’t block other checks.
    • Enhanced interruption and error handling during diagnostic collection for safer runtime behavior.
    • Preserved workspace context across background diagnostic processing to keep results consistent.

…of ForkJoinPool

DefaultDiagnosticComputer вычислял диагностики через
CompletableFuture.supplyAsync(...).join() + parallelStream() на
diagnosticComputerExecutor (ForkJoinPool). Когда getDiagnostics вызывается
из воркера ForkJoinPool (пакетный analyze через cliExecutor,
AnalyzeProjectOnStart через analyzeOnStartExecutor), блокирующий join() —
это managed blocking: пул поднимает компенсирующие потоки, каждый лишний
воркер материализует ещё один DocumentContext, и естественный потолок
«≈ число ядер» теряется (претензия §4.2 из #4248).

Теперь каждая диагностика — отдельная задача на diagnosticComputerExecutor,
результат собирается через Future.get(). Сам diagnosticComputerExecutor
переведён с ForkJoinPool на bounded ThreadPoolExecutor: блокировка на
get() обычного пула не проходит через ForkJoinPool.managedBlock, поэтому
вызов из FJP-воркера компенсирующих потоков не плодит. Контекст workspace
несут per-task снапшоты ContextPropagatingExecutorService.

Замеры на SSL 3.2 (4 ядра, JFR + /proc, вывод идентичен):
- analyze:  thread-starts на cli-пуле 17→3, sustained active 45→31,
  SARIF 72160=72160.
- LSP (AnalyzeProjectOnStart): thread-starts на analyze-on-start 17→3,
  sustained active ~45→~32, диагностик 72141=72141 по 2184 файлам.
Компенсация снимается на обоих путях, в т.ч. на LSP-пути, которого
внешняя ось batch-анализа не касается.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BSiRGLm633B4EmvG3vkk4V
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@nixel2007, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b99bbb0c-7cba-4378-9dd2-a6959c3adba8

📥 Commits

Reviewing files that changed from the base of the PR and between ffc6c45 and 368e3e2.

📒 Files selected for processing (1)
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/DefaultDiagnosticComputerTest.java
📝 Walkthrough

Walkthrough

Diagnostic computation now submits individual diagnostic tasks to a workspace-bounded executor, collects Future results with explicit failure handling, and applies existing filters after collection. Executor configuration adds context propagation, bounded queuing, and named daemon threads.

Changes

Diagnostic execution

Layer / File(s) Summary
Workspace-bounded diagnostic executor
src/main/java/com/github/_1c_syntax/bsl/languageserver/infrastructure/ExecutorConfiguration.java
The diagnostic executor now uses a context-propagating bounded ThreadPoolExecutor with named daemon threads.
Future-based diagnostic collection
src/main/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/DefaultDiagnosticComputer.java
Diagnostics are submitted as individual tasks, collected through Future instances, and handled for runtime, interruption, and execution failures before existing filtering.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DefaultDiagnosticComputer
  participant ExecutorService
  participant BSLDiagnostic
  DefaultDiagnosticComputer->>ExecutorService: submit one task per diagnostic
  ExecutorService->>BSLDiagnostic: compute diagnostics
  BSLDiagnostic-->>ExecutorService: return diagnostic list
  ExecutorService-->>DefaultDiagnosticComputer: collect Future results
  DefaultDiagnosticComputer->>DefaultDiagnosticComputer: filter ignored diagnostics
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly summarizes the main change: diagnostics now run on a bounded pool instead of ForkJoinPool.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/perf-diagnostic-computer-executor

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.

@WorkspaceScope(proxyMode = ScopedProxyMode.INTERFACES)
public ExecutorService diagnosticComputerExecutor() {
return createWorkspaceForkJoinPool("diagnostic-computer-");
return createWorkspaceBoundedPool("diagnostic-computer-");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Это не pool

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

В соседних методах нет термина bounded. Делай по подобию.

@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/diagnostics/DefaultDiagnosticComputer.java (1)

42-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim calling-scenario details out of the class Javadoc.

Lines 48-52 describe who invokes this class and when (batch analyze, AnalyzeProjectOnStart workers), which the path instructions for this directory explicitly ask Javadoc to avoid. The rationale about avoiding managed-blocking compensation is useful, but the specific caller scenarios ("пакетный анализ, анализ проекта при старте") belong in an implementation-level comment (as already done in ExecutorConfiguration), not in the public class contract Javadoc.

As per path instructions, src/main/java/**/*.java: "Javadoc should describe the contract of a class or method (what it does, inputs/outputs, invariants, side effects), not calling scenarios or who invokes it."

📝 Suggested trim
 /**
  * Реализация {`@link` DiagnosticComputer} по умолчанию.
  * <p>
  * Параллельно вычисляет диагностики всеми зарегистрированными анализаторами {`@link` BSLDiagnostic}
  * с обработкой ошибок и фильтрацией по правилам подавления и игнорируемым авторам.
  * <p>
- * Каждый анализатор — отдельная задача на {`@code` diagnosticComputerExecutor}; результаты
- * собираются через {`@link` Future#get()}. Блокировка на {`@code` get()} обычного пула, в отличие от
- * {`@code` parallelStream()} на {`@code` ForkJoinPool} с блокирующим {`@code` join()}, не запускает
- * managed-blocking компенсацию, поэтому вызов из воркера {`@code` ForkJoinPool} (пакетный анализ,
- * анализ проекта при старте) не плодит компенсирующие потоки и живые {`@code` DocumentContext}.
+ * Каждый анализатор — отдельная задача на {`@code` diagnosticComputerExecutor}; результаты
+ * собираются через {`@link` Future#get()}.
  */

(Move the managed-blocking/compensating-thread rationale into an inline // comment near the compute() implementation instead.)

🤖 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/diagnostics/DefaultDiagnosticComputer.java`
around lines 42 - 52, Trim the class Javadoc for DefaultDiagnosticComputer to
remove specific caller scenarios such as batch analysis and
AnalyzeProjectOnStart, while retaining only the class contract and relevant
managed-blocking rationale. Move that caller-specific rationale into an inline
comment near the compute() implementation, without changing behavior.

Source: Path instructions

🤖 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/diagnostics/DefaultDiagnosticComputer.java`:
- Around line 42-52: Trim the class Javadoc for DefaultDiagnosticComputer to
remove specific caller scenarios such as batch analysis and
AnalyzeProjectOnStart, while retaining only the class contract and relevant
managed-blocking rationale. Move that caller-specific rationale into an inline
comment near the compute() implementation, without changing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: df388d2b-ead8-4a27-995d-a3fdcca8eee6

📥 Commits

Reviewing files that changed from the base of the PR and between 488c6af and e6dab73.

📒 Files selected for processing (2)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/diagnostics/DefaultDiagnosticComputer.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/infrastructure/ExecutorConfiguration.java

claude added 2 commits July 12, 2026 07:12
…ss javadoc

Address review feedback on #4264:
- rename createWorkspaceBoundedPool -> createWorkspaceExecutorService, matching
  the sibling createSharedForkJoinExecutorService (no "bounded"/"pool" in the
  name; the method returns a wrapped ExecutorService).
- move the managed-blocking / caller-scenario rationale out of the
  DefaultDiagnosticComputer class javadoc into an inline comment in compute(),
  per the repo convention that javadoc describes the contract, not callers.

No behavior change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BSiRGLm633B4EmvG3vkk4V
Add a unit test for the new Future-based collection paths that SonarCloud
flagged as uncovered on new code: results merged from all analyzers, a diagnostic
throwing RuntimeException is skipped (others still returned), and a non-runtime
failure surfaces as IllegalStateException from Future collection.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BSiRGLm633B4EmvG3vkk4V
@sonarqubecloud

Copy link
Copy Markdown

@nixel2007
nixel2007 enabled auto-merge July 12, 2026 07:42
@nixel2007
nixel2007 merged commit 280a06e into develop Jul 12, 2026
36 checks passed
@nixel2007
nixel2007 deleted the claude/perf-diagnostic-computer-executor branch July 12, 2026 07:45
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