Skip to content

fix: replace global needRebuild() with project-scoped rebuild APIs#1300

Draft
joaodinissf wants to merge 1 commit intodsldevkit:masterfrom
joaodinissf:fix/remove-redundant-builder-needRebuild
Draft

fix: replace global needRebuild() with project-scoped rebuild APIs#1300
joaodinissf wants to merge 1 commit intodsldevkit:masterfrom
joaodinissf:fix/remove-redundant-builder-needRebuild

Conversation

@joaodinissf
Copy link
Copy Markdown
Collaborator

@joaodinissf joaodinissf commented Apr 2, 2026

Summary

  • Remove the vestigial IncrementalProjectBuilder.needRebuild() call from BuildContext.needRebuild() that triggered global workspace rebuilds every time a builder participant generated a file
  • Implement needRebuild(IProject) using the modern Eclipse 3.17+ project-scoped APIs (triggerRequestProjectRebuild / triggerRequestProjectsRebuild), matching the upstream Xtext 2.27+ pattern

Root Cause

The DDK's BuildContext.needRebuild() called builder.needRebuild() — an IncrementalProjectBuilder method that sets a global rebuildRequested flag on Eclipse's BuildManager, causing all projects in the build cycle to be rebuilt (not just the current project). This was redundant because the DDK's RebuildingXtextBuilder already handles generated-source reprocessing via an internal rebuild loop (up to 2 extra iterations in doBuild()).

The call was a leftover from a 2014 workaround (Eclipse Bug #452399, Comment #8) that was superseded by the internal loop approach (Comment #10) but never removed when the DDK was open-sourced in 2016.

Additionally, the DDK's BuildContext did not override needRebuild(IProject) (added in Xtext 2.27, PR #1821), so all callers using the modern API fell through to the deprecated no-arg version via the interface default method.

What Changes

Commit 1: Remove builder.needRebuild() from the no-arg needRebuild(), keeping only the internal rebuildRequired flag.

Commit 2: Override needRebuild(IProject) with project-scoped Eclipse APIs:

  • Same project: builder.triggerRequestProjectRebuild() (rebuilds only this project)
  • Different project: builder.triggerRequestProjectsRebuild(project) (rebuilds only that project)

This gives the DDK two complementary rebuild mechanisms:

  1. The internal loop handles immediate reprocessing within the same build() invocation
  2. The project-scoped Eclipse API acts as a safety net if the loop is exhausted, without cascading to unrelated projects

Commit 3: Analysis document (DDK-BUILDER-ANALYSIS.md) with full root cause analysis, historical context, architecture reference, and remaining modernization roadmap.

References

Test plan

  • Open workspace with multiple DSL projects
  • Edit a file in one project that triggers code generation
  • Verify that only the affected project is rebuilt (not the entire workspace)
  • Enable build tracing (-Dorg.eclipse.core.resources.debug=true) and compare builder invocation counts before/after
  • Verify generated sources are still correctly reprocessed (internal loop still works)
  • Test with project close/delete to confirm no regression in index cleanup

🤖 Generated with Claude Code

@joaodinissf joaodinissf force-pushed the fix/remove-redundant-builder-needRebuild branch from cd9b3aa to dd4b31f Compare April 2, 2026 16:29
@joaodinissf joaodinissf changed the title fix: remove redundant IncrementalProjectBuilder.needRebuild() from Bu… fix: remove global needRebuild() cascade, implement project-scoped rebuild Apr 2, 2026
@joaodinissf joaodinissf force-pushed the fix/remove-redundant-builder-needRebuild branch from dd4b31f to ba39b55 Compare April 2, 2026 16:30
BuildContext.needRebuild() called IncrementalProjectBuilder.needRebuild()
which sets a global rebuildRequested flag on Eclipse's BuildManager,
causing ALL projects in the build cycle to be rebuilt -- not just the
current project. This was redundant because the DDK's
RebuildingXtextBuilder already handles generated-source reprocessing
via an internal rebuild loop (up to 2 extra iterations in doBuild()).

The global call was a leftover from a 2014 workaround (Eclipse Bug
#452399, Comment dsldevkit#8) that was superseded by the internal loop approach
(Comment dsldevkit#10) but never removed when the DDK was open-sourced in 2016.
It caused cascading builder re-invocations across the workspace
dependency graph, resulting in unrelated projects being fully rebuilt
when editing files in completely unrelated projects.

Changes:
- Remove builder.needRebuild() from the no-arg needRebuild(), keeping
  only the internal rebuildRequired flag
- Override needRebuild(IProject) using the modern Eclipse 3.17+ APIs
  (triggerRequestProjectRebuild / triggerRequestProjectsRebuild) instead
  of relying on the IBuildContext default method that falls through to
  the deprecated global needRebuild(). This matches the upstream Xtext
  2.27+ pattern (PR eclipse-xtext/xtext#1821, Eclipse Bug 579082).

The DDK now has two complementary rebuild mechanisms:
1. Internal loop: immediate reprocessing within the same build()
   invocation (up to 2 extra iterations)
2. Project-scoped Eclipse API: safety net if the loop is exhausted,
   without cascading to unrelated projects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@joaodinissf joaodinissf force-pushed the fix/remove-redundant-builder-needRebuild branch from ba39b55 to 068faeb Compare April 2, 2026 16:43
@joaodinissf joaodinissf changed the title fix: remove global needRebuild() cascade, implement project-scoped rebuild fix: replace global needRebuild() with project-scoped rebuild APIs Apr 2, 2026
@joaodinissf
Copy link
Copy Markdown
Collaborator Author

DDK Builder Infrastructure: Full Analysis

1. Problem Statement

Editing a file in one Eclipse project (e.g. assistant.chat.ui) triggers full rebuilds
of completely unrelated DSL projects (e.g. intfdef.core), with progress messages like
"Compile IntfDefGroupCoreChecks.check" appearing despite no dependency between the two.

2. Root Cause

The DDK's BuildContext.needRebuild() contained a vestigial call to
IncrementalProjectBuilder.needRebuild() that had been present since the 2016
open-source commit and was never removed:

// BEFORE (BuildContext.java, 2016-2026)
@Override
public void needRebuild() {
    rebuildRequired = true;              // (1) Internal flag
    if (builder != null) {
        builder.needRebuild();           // (2) Eclipse global rebuild signal
    }
}

Line (1) is consumed by RebuildingXtextBuilder.doBuild(), which runs an internal
rebuild loop (up to 2 extra iterations) to reprocess generated sources within the same
build() invocation.

Line (2) calls IncrementalProjectBuilder.needRebuild(), which is a final method
inherited from the Eclipse Platform. It sets a global rebuildRequested boolean on
the BuildManager, causing Eclipse to rebuild ALL projects in the workspace on the
next build loop iteration -- not just the current project.

From the Eclipse Javadoc:

"Note: this method will schedule rebuild for all projects involved in the current build cycle!"

Line (2) was redundant -- line (1) already handles reprocessing -- and actively harmful.

The Cascade Mechanism

  1. Builder participant writes a generated file and calls needRebuild()
  2. builder.needRebuild() sets the global rebuildRequested flag
  3. Eclipse restarts the build loop for all projects (default max 10 iterations, or numConfigs * 2, whichever is greater)
  4. Other projects' builders run, their participants may also generate files, calling needRebuild() again
  5. Eventually getDelta(getProject()) returns null for some project (Eclipse drops the delta after too many iterations), triggering a FULL BUILD instead of incremental
  6. The full build processes all resources including .check files in unrelated projects

The needRebuild(IProject) API Gap

The DDK's BuildContext also did not override needRebuild(IProject), added to the IBuildContext interface in Xtext 2.27. When callers used the modern API, this hit the interface's default method, which simply delegated to the deprecated no-arg needRebuild() -- and from there to the global builder.needRebuild().

Standard Xtext 2.42's BuildContext overrides needRebuild(IProject) to use project-scoped APIs (triggerRequestProjectRebuild() / triggerRequestProjectsRebuild()) that only rebuild the affected project, not the entire workspace.

3. Historical Context

The DDK's RebuildingXtextBuilder was created as a workaround for Eclipse Bug #452399.

Date Event
Nov 2014 Bug #452399: Bernhard Buss posts initial workaround using builder.needRebuild() + rememberLastBuiltState() (Comment #8)
Nov 2014 Same bug: Bernhard posts better solution -- internal rebuild loop using workspace ElementTree diffing (Comment #10)
Nov 2016 DDK open-sourced (commit 14b48aa) with both mechanisms. The builder.needRebuild() was a leftover from the earlier workaround.
Mar 2022 Eclipse Bug #579082: needRebuild() identified as cause of O(n²) build behavior. Eclipse 3.17 adds requestProjectRebuild() / requestProjectsRebuild().
Mar 2022 Xtext PR #1821: Xtext 2.27 deprecates needRebuild(), adds needRebuild(IProject) with project-scoped semantics.
Apr 2026 This PR: vestigial builder.needRebuild() removed after 10 years; needRebuild(IProject) implemented with upstream pattern.

Related Issues

Issue Description
Eclipse Bug 579082 Slow autobuild with Xtext projects; needRebuild() causes global rebuild
Xtext #1339 File generation causes global rebuild of all build configs
Xtext #1761 needRebuild() in pollQueuedBuildData() stops Eclipse build
Xtext #1820 Request for fine-granular needRebuild per project

4. Eclipse needRebuild() API Reference

Method Since Scope Mechanism
needRebuild() 2.1 ALL projects Sets global rebuildRequested boolean on BuildManager
requestProjectRebuild(boolean) 3.17 Current project only Writes to per-project restartBuildImmediately map
requestProjectsRebuild(Collection) 3.17 Specified projects only Adds to projectsToRebuild set

The DDK targets Eclipse 4.34 (well above 3.17), so the modern project-scoped APIs are available directly. XtextBuilder (since Xtext 2.27) wraps these as triggerRequestProjectRebuild() and triggerRequestProjectsRebuild(IProject).

5. Standard Xtext 2.42 vs DDK Comparison

Feature Xtext 2.42 DDK (before) DDK (after this PR)
needRebuild() Deprecated; calls builder.needRebuild() rebuildRequired + builder.needRebuild() rebuildRequired only
needRebuild(IProject) triggerRequestProjectRebuild() (scoped) Not overridden (default fallthrough) triggerRequestProjectRebuild() (scoped)
Internal rebuild loop None -- relies on Eclipse re-invocation Yes, up to 2 extra iterations Yes, up to 2 extra iterations
doBuild() API 4-param (since 2.18) 3-param (deprecated) 3-param (deprecated)
BuildData constructor 7-param (since 2.27) 4-param (deprecated) 4-param (deprecated)
ClosedProjectsQueue Integrated Missing Missing
isSourceLevelURI() Tracks actual URIs Always returns true Always returns true
ensureBuilderStateLoaded() Called Not called Not called
Empty toBeBuilt guard No Yes (DDK optimization) Yes

Key insight: Standard Xtext has no internal rebuild loop, so it needs Eclipse re-invocation to handle generated sources. The DDK replaced this with its own loop -- but never removed the Eclipse-level needRebuild().

6. Remaining Modernization Opportunities

Not part of this PR. Listed for future reference:

Item Since Risk Priority Description
doBuild() 4-param API 2.18 Medium High Deprecated 3-param override; Xtext logs warning every build
BuildData 7-param constructor 2.27 Low Follows from above Adds indexingOnly, rebuildTrigger, removedProjects
ClosedProjectsQueue 2.18 Low-Med Medium Stale index entries from closed/deleted projects
ensureBuilderStateLoaded() 2.26 Low Low Graceful handling of builder state deserialization failures
isSourceLevelURI() 2.9 Low Low Always returns true; should track actual workspace URIs
pollQueuedBuildData() 2.19 HIGH Skip Intentionally skipped -- DDK uses MonitoredClusteringBuilderState

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