Skip to content

feat!: replace dependency graph with reactive invalidation - #5292

Open
DanySK wants to merge 31 commits into
masterfrom
feat/reactive-engine
Open

feat!: replace dependency graph with reactive invalidation#5292
DanySK wants to merge 31 commits into
masterfrom
feat/reactive-engine

Conversation

@DanySK

@DanySK DanySK commented Apr 21, 2026

Copy link
Copy Markdown
Member

Summary

This PR supersedes #5131.

It rebases the reactive-engine work on top of the current master and preserves the same overall direction: move the engine and the observable state model away from coarse-grained dependency recomputation and toward reactive, fine-grained invalidation.

At a high level, this branch:

  • introduces observable collections and lifecycle-aware observation primitives in alchemist-api
  • migrates engine-facing state changes to reactive observation
  • removes the old DependencyGraph / JGraphTDependencyGraph path
  • folds the reactive execution path back into Engine
  • tightens dependency tracking in SAPERE and related incarnations
  • updates affected tests and supporting code across engine, implementation base, loading, biochemistry, protelis, scafi, maps, physics, swing UI, and cognitive-agents

Why this exists

The original goal from #5131 remains the same:

  • reduce the amount of global recomputation triggered by local state changes
  • make engine scheduling and condition invalidation observable-driven
  • remove the explicit dependency-graph maintenance burden
  • improve runtime characteristics for workloads with frequent localized updates, especially SAPERE-like scenarios where the previous dependency invalidation strategy was too broad

Main changes

1. Observable model infrastructure

This PR adds a substantial observation layer in alchemist-api, including:

  • Observable
  • ObservableList
  • ObservableMap
  • ObservableSet
  • derived/event observables
  • lifecycle primitives (Lifecycle, LifecycleOwner, LifecycleRegistry, LifecycleState)

These components are then used to expose mutable simulation state in a way that can notify dependent logic incrementally instead of forcing coarse global refreshes.

2. Engine architecture

The branch first introduces a separate reactive engine during development, but the final rebased result does not keep a standalone ReactiveEngine class.

Instead, the final state:

  • removes DependencyGraph
  • removes JGraphTDependencyGraph
  • folds the reactive execution model into Engine
  • keeps AbstractEngine and related support code
  • updates tests accordingly

This is important because the PR is no longer “add a second engine implementation”; it is effectively “replace the old engine dependency-management model with a reactive one.”

3. Fine-grained invalidation in core model code

A large portion of the diff updates:

  • reactions
  • conditions
  • nodes
  • environments
  • linking rules

to subscribe to narrower observable sources and react only to relevant changes.

This includes changes in alchemist-implementationbase, where several conditions and reaction-related classes are updated to consume the new observable semantics.

4. SAPERE-focused performance work

The most performance-oriented part of the PR is the SAPERE work:

  • LsaNode
  • LsaNeighborhoodCondition
  • LsaStandardCondition
  • SAPEREGradient
  • SAPEREReaction

These changes aim to stop reacting to whole-LSA-space churn when only a targeted molecule or neighborhood-relevant subset changed.

This is likely the most immediately measurable performance improvement in the branch, because SAPERE workloads often stress dependency propagation and neighborhood-sensitive invalidation patterns.

5. Incarnation and integration updates

To keep the engine/model shift coherent, the branch also updates:

  • biochemistry neighbor conditions and related tests
  • protelis execution/context wiring
  • scafi action/condition paths
  • loading and extractor logic
  • maps/physics/cognitive-agents integration points

This is not incidental churn; most of these changes are required to make the reactive model consistent across the simulation stack.

What changed compared to #5131

This PR should be treated as the replacement for #5131, not as an unrelated follow-up.

Compared to #5131:

  • it is rebased on current master
  • conflict resolution keeps current master dependency versions where appropriate
  • the branch tip includes later cleanup/performance commits, including the final SAPERE fine-grained dependency work
  • the final engine shape is clearer: the old dependency-graph path is removed and the reactive path is integrated into Engine

Validation status

This branch was rebased with repository hooks enabled.

During the rebase, the only manual conflict resolutions required were:

  • alchemist-loading/src/main/kotlin/it/unibo/alchemist/boundary/loader/LoadingSystem.kt
  • gradle/libs.versions.toml

The resolutions were mechanical and aligned with the final intent of the rebased commits:

  • keep the integrated Engine(environment) path after the dedicated ReactiveEngine removal
  • keep master's newer shared dependency versions while preserving the branch’s added kotlinx-collections catalog entries

How to assess the performance change

There does not appear to be a dedicated microbenchmark suite in the repository, so the best assessment is comparative measurement on representative workloads.

Best candidates to measure

The most relevant areas are:

  • SAPERE simulations and tests
  • workloads with many localized state changes
  • scenarios with heavy neighborhood-sensitive conditions
  • cognitive-agent scenarios that repeatedly react to position/state updates

Practical ways to evaluate

  1. Compare master vs this branch using repeated runs of the same workloads.

  2. Focus first on SAPERE-heavy tests and simulations, because the final commits specifically optimize fine-grained SAPERE invalidation.

  3. Use wall-clock time, CPU time, allocation rate, and GC pressure as the primary metrics.

  4. Run on the same JVM and machine, ideally multiple times, ignoring the first run if dependency/classloading noise matters.

Concrete approaches

  • Run targeted Gradle test tasks repeatedly on master and this branch:

    • ./gradlew --parallel :alchemist-incarnation-sapere:test
    • ./gradlew --parallel :alchemist-engine:test
    • ./gradlew --parallel :alchemist-cognitive-agents:test
  • For scenario-level comparisons, build the simulator and execute the same representative simulations on both revisions, then compare:

    • end-to-end runtime
    • steady-state CPU usage
    • allocation / GC activity
  • If deeper profiling is needed, use JFR or an external profiler on identical simulations to confirm that:

    • fewer broad invalidation cascades occur
    • less time is spent in dependency recomputation / scheduling-related plumbing
    • SAPERE updates no longer trigger unnecessary wide recomputation

What I would expect to improve

If the branch behaves as intended, the most likely gains are:

  • better performance on SAPERE workloads with many local LSA updates
  • less unnecessary recomputation in condition/reaction invalidation
  • lower overhead from maintaining and traversing an explicit dependency graph

I would not assume uniform speedups everywhere. Some workloads may be neutral, and broad integration changes like this deserve empirical confirmation rather than a blanket performance claim.

Reference

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants