Discarded wires remain subscribed to simulator lifecycle events and progressively degrade tick performance
- Dominant language
- Dart
- Stars
- 489
- Forks
- 88
- Avg merge
- 3d 23h
- Merged PRs (30d)
- 10
Description
### Describe the bug
Repeatedly constructing and discarding ROHD wire/elaboration generations in the same long-lived Dart process leaves callbacks associated with obsolete generations active during subsequent simulator ticks.
This is observable both as:
1. a deterministic increase in obsolete pre-tick callback activity; and
2. a large progressive decline in simulator tick throughput.
I originally encountered this while profiling a larger ROHD application, but reduced the behavior to a standalone reproducer containing no application-specific code.
### Deterministic evidence
The standalone reproducer uses a passive Dart Zone hook to count callbacks occurring during the simulator pre-tick phase. The instrumentation does not retain discarded signals or their callback functions.
With 3,000 wires per generation:
| Discarded generations | Obsolete callbacks per tick |
|---:|---:|
| 0 | 0 |
| 1 | 3,000 |
| 2 | 6,000 |
| 3 | 9,000 |
This result is identical on every tested ROHD version and occurs both with and without `Simulator.reset()` between generations.
An empty-zone control produces zero activity.
This demonstrates that callbacks associated with discarded generations continue participating in later simulator ticks.
### Performance impact
Median tick throughput from the same reproducer:
| ROHD | Reset between generations | Gen 1 | Gen 2 | Gen 3 | Gen 4 |
|---|---:|---:|---:|---:|---:|
| 0.6.9 | Yes | 13,799.08 | 6,653.54 | 4,943.02 | 3,408.35 |
| 0.6.9 | No | 13,837.14 | 7,364.86 | 4,807.09 | 3,171.38 |
| 0.6.10 | Yes | 12,404.86 | 6,201.02 | 4,453.52 | 3,375.19 |
| 0.6.10 | No | 13,205.17 | 6,418.75 | 4,661.87 | 3,420.75 |
| `main` | Yes | 12,799.06 | 7,117.22 | 4,835.93 | 3,389.36 |
| `main` | No | 12,646.81 | 6,520.17 | 4,439.31 | 3,288.14 |
Absolute throughput is naturally host-dependent, but the progressive degradation is large and repeatable while the currently useful design remains constant in size.
### Tested versions
The issue reproduces on:
```text
ROHD 0.6.9
ROHD 0.6.10
ROHD main @ 51d6dad0d584d24ea353c356db01fd88bb84cdfc
```
It reproduces both with and without simulator reset between generations, so it does not appear to depend specifically on reset usage.
### Expected behavior
Once a wire/elaboration generation is no longer owned or used by the application, I would expect it to stop contributing work to subsequent simulator ticks.
Alternatively, if simulator participants require explicit lifetime management, I would expect a supported detach/dispose mechanism that allows obsolete elaborations to unregister from simulator-global lifecycle events.
Replacing an N-wire simulated design with another N-wire design should not make future ticks progressively more expensive merely because earlier generations once existed.
### Suspected mechanism
The behavior appears consistent with `_Wire` instances retaining subscriptions to simulator-global lifecycle mechanisms such as `Simulator.preTick` and reset-related callbacks after their originating elaboration has been discarded.
I have deliberately not implemented a fix yet because I would prefer maintainer guidance on the intended lifetime/ownership model. Explicit subscription ownership/detachment may be one possible solution, but I do not want to assume what the preferred ROHD API should be.
### Why this matters
This affects long-lived applications that repeatedly elaborate or replace simulated hardware, for example:
- interactive hardware development tools;
- IDEs/workbenches;
- design explorers;
- test applications switching configurations or personalities;
- applications performing repeated elaboration inside one process.
In the larger application where I originally encountered this, eliminating participation from obsolete elaborations was one of the dominant native-simulation performance improvements.
### Environment
Primary measurement system:
```text
OS: Fedora 44
Kernel: Linux 7.1.13
CPU: AMD Ryzen 9 3950X
Dart: 3.13.2
```
Measurements are sequential JIT runs. Absolute timing values should not be treated as portable performance thresholds; the deterministic obsolete-callback count is the stronger evidence.
### Possible next step
I would be happy to prepare a small PR containing a regression test and an appropriate lifecycle/subscription fix after confirming the intended API direction.
One process question before I do that: I use AI-assisted coding tools as part of my development workflow. I review the resulting changes, run the project's tests, and take responsibility for any code I submit.
Is AI-assisted contribution acceptable for ROHD pull requests, and is there any specific disclosure or contribution policy you would like me to follow?
### To Reproduce
Standalone reproducer:
https://github.com/pmjobin/rohd-stale-subscription-repro
Steps:
```bash
git clone https://github.com/pmjobin/rohd-stale-subscription-repro
cd rohd-stale-subscription-repro
dart pub get
dart run bin/reproduce.dart
```
Default parameters are:
```text
3,000 wires per generation
4 generations
2,000 measured ticks per repetition
1,000 warm-up ticks
5 repetitions
```
The program performs this sequence:
1. Construct a generation containing 3,000 ordinary ROHD wires.
2. Warm up and benchmark simulator tick throughput.
3. Clear/discard the application's references to that generation.
4. Construct another generation of the same size.
5. Repeat for four generations.
The currently useful design therefore remains approximately constant in size.
The reproducer also supports both:
```text
reset between generations
no reset between generations
```
and reproduces the issue in both modes.
In addition to timing, a passive Dart Zone hook counts callbacks during the simulator pre-tick phase. With the default 3,000-wire generations, obsolete callback activity is:
```text
discarded generations obsolete callbacks / tick
0 0
1 3000
2 6000
3 9000
```
An empty-zone control produces zero activity.
The repository README contains the complete methodology and measurements for:
```text
ROHD 0.6.9
ROHD 0.6.10
ROHD main @ 51d6dad0d584d24ea353c356db01fd88bb84cdfc
```
The repository contains no application-specific code and no modified ROHD source.
Quality checks pass on all tested targets:
```bash
dart analyze
dart test
dart format .
```
### Expected behavior
_No response_
### Actual behavior
_No response_
### Additional: Dart SDK info
Dart SDK version: 3.13.2 (stable) (Tue Aug 25 01:01:12 2026 -0700) on "linux_x64"
### Additional: pubspec.yaml
```yaml
name: rohd_stale_subscription_repro
description: Minimal reproducer for ROHD simulator activity retained across discarded signal generations.
version: 1.0.0
publish_to: none
environment:
sdk: ^3.13.0
dependencies:
rohd: 0.6.10
dev_dependencies:
test: ^1.31.0
```
### Additional: Context
_No response_
Contributor guide
Research direction
Start by running bin/reproduce.dart from the standalone reproducer and compare callback counts across discarded generations. Then trace ROHD's simulator lifecycle subscriptions for wires and elaborations, using the reported pre-tick behavior as the regression target. Done should include a regression test and a maintainer-approved lifetime or detach/dispose behavior that prevents obsolete generations from adding callbacks.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100