Skip to content

Discarded wires remain subscribed to simulator lifecycle events and progressively degrade tick performance #708

Description

@pmjobin

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:

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:

  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:

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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions