Skip to content

Fix memory leak in ContinuousSpace agent removal#3031

Merged
quaquel merged 3 commits intomesa:mainfrom
Nithin9585:fix-ghost-agent-3029
Dec 29, 2025
Merged

Fix memory leak in ContinuousSpace agent removal#3031
quaquel merged 3 commits intomesa:mainfrom
Nithin9585:fix-ghost-agent-3029

Conversation

@Nithin9585
Copy link
Copy Markdown
Contributor

Fixes #3029

Description

When removing an agent from ContinuousSpace, the internal index mapping (_index_to_agent) was corrupted. The code shifted subsequent agents to fill the gap but failed to delete the old index entry of the last shifted agent. This created a "Ghost Agent" entry where two indices pointed to the same agent, causing the dictionary to grow larger than the number of active agents.

Root Cause

In the _remove_agent method, when agents were shifted down to fill gaps, the old index entry of the last shifted agent was never deleted.

Example: Removing agent B from [A, B, C]:

  • Before: _index_to_agent = {0: A, 1: B, 2: C}
  • After (buggy): _index_to_agent = {0: A, 1: C, 2: C} (ghost entry at index 2)
  • After (fixed): _index_to_agent = {0: A, 1: C}

Changes

  • Added tracking of last_old_index during agent shifting
  • Added cleanup logic to remove stale index entries
  • Added comprehensive test coverage with 3 scenarios

Impact

  • Severity: High (memory leak + data corruption)
  • Breaking Changes: None
  • Performance: Negligible overhead

- Fix stale index entries in _index_to_agent after agent removal
- Add test coverage for agent removal scenarios
- Fixes mesa#3029
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 -0.5% [-1.0%, +0.1%] 🔵 -0.6% [-0.8%, -0.5%]
BoltzmannWealth large 🔵 +2.1% [+0.0%, +5.8%] 🔵 -2.1% [-3.2%, -1.0%]
Schelling small 🔵 -1.4% [-2.4%, -0.5%] 🔵 -0.8% [-1.9%, +0.4%]
Schelling large 🔵 -1.2% [-2.4%, +0.7%] 🔵 -3.0% [-5.5%, -0.1%]
WolfSheep small 🔵 -2.2% [-3.1%, -1.5%] 🔵 -1.7% [-2.0%, -1.4%]
WolfSheep large 🔵 -1.9% [-5.5%, -0.0%] 🔵 -2.2% [-2.8%, -1.6%]
BoidFlockers small 🔵 -1.4% [-1.9%, -0.9%] 🔵 +0.2% [-0.0%, +0.5%]
BoidFlockers large 🔵 -1.1% [-1.6%, -0.4%] 🔵 -0.2% [-0.4%, +0.0%]

old_index = self._agent_to_index[agent]
self._agent_to_index[agent] = old_index - 1
self._index_to_agent[old_index - 1] = agent
last_old_index = old_index
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

By definition, last_old_index = self.active_agents[-1], so there is little need to update this inside the loop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

since the last shifted agent is always at active_agents[-1], its old index is just len(active_agents) (after deletion). I can simplify this to avoid updating last_old_index on every iteration.

Comment on lines +159 to +160
if last_old_index is not None:
self._index_to_agent.pop(last_old_index, None)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can only happen in the number of agents is 0, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It happens when removing the last agent (nothing to shift), not specifically when count reaches 0. But since line 148 already deletes that index, the None check just prevents redundant cleanup.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Dec 28, 2025

The bug is even a bit more severe, at least theoretically. Since dictionaries are ordered in Python by insertion, the old index in _index_to_agent will be used, resulting in the wrong agent being returned.

edit: at present, _index_to_agent is not used by the class itself for anything, so although the bug being fixed here exists, it has not affected the behavior and performance significantly (outside of the slight memory leak).

@Nithin9585
Copy link
Copy Markdown
Contributor Author

@quaquel Good point about dict ordering! The fix prevents both the memory leak and potential wrong lookups if _index_to_agent gets used in the future. Thanks for the thorough review!

- Remove redundant per-iteration update of last_old_index
- Calculate it directly as len(active_agents) after shifting
- Clarify logic with improved comments
- Addresses reviewer feedback on PR mesa#3029
@quaquel quaquel added bug Release notes label experimental Release notes label labels Dec 29, 2025
@quaquel quaquel merged commit c1574b6 into mesa:main Dec 29, 2025
12 of 14 checks passed
@quaquel
Copy link
Copy Markdown
Member

quaquel commented Dec 29, 2025

Thanks!

@Nithin9585 Nithin9585 deleted the fix-ghost-agent-3029 branch December 29, 2025 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Release notes label experimental Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix memory leak in ContinuousSpace agent removal

2 participants