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:
- a deterministic increase in obsolete pre-tick callback activity; and
- 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:
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:
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:
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:
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:
- Construct a generation containing 3,000 ordinary ROHD wires.
- Warm up and benchmark simulator tick throughput.
- Clear/discard the application's references to that generation.
- Construct another generation of the same size.
- Repeat for four generations.
The currently useful design therefore remains approximately constant in size.
The reproducer also supports both:
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:
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:
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:
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
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
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:
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:
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:
mainmainAbsolute 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:
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
_Wireinstances retaining subscriptions to simulator-global lifecycle mechanisms such asSimulator.preTickand 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:
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:
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:
git clone https://github.com/pmjobin/rohd-stale-subscription-repro cd rohd-stale-subscription-repro dart pub get dart run bin/reproduce.dartDefault parameters are:
The program performs this sequence:
The currently useful design therefore remains approximately constant in size.
The reproducer also supports both:
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:
An empty-zone control produces zero activity.
The repository README contains the complete methodology and measurements for:
The repository contains no application-specific code and no modified ROHD source.
Quality checks pass on all tested targets:
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
Additional: Context
No response