Skip to content

Speed up automatic module pin handling - #637

Merged
joern274 merged 4 commits into
masterfrom
feature/module_pin_performance
Aug 14, 2026
Merged

Speed up automatic module pin handling#637
joern274 merged 4 commits into
masterfrom
feature/module_pin_performance

Conversation

@julianspeith

Copy link
Copy Markdown
Contributor

Assigning gates to a module recomputes its pins, and both halves of that were slower than they needed to be: the lookup that decides whether a net already has a pin scanned linearly, and every individual pin change was announced to the GUI as its own event.

Measured on a sha256 netlist with 7144 top level gates, creating and deleting 24 modules emitted 43040 pin_changed events. Because every event a Python script triggers is handed to the GUI thread over a blocking queued connection, the cost of an event is a thread round trip rather than the handler itself. A sample of that workload showed 91% of the script runtime was the Python thread waiting — 68% in the qApp->processEvents() that follows each emit, 23% blocked on the semaphore of the blocking connection. The netlist work itself was 5%.

What is in here

Characterization tests first. enable_automatic_net_checks() and update_nets() had no test at all, and the existing check_pins test covered the manual pin API but almost none of the automatic path. Six tests pin down the behavior that has to survive the change: pins appearing and disappearing as gates enter and leave a module, direction transitions through input/inout/output with a stable pin ID, pins propagating to ancestor modules and moving on re-parenting, the effect of marking a net global, and what happens with the automatic checks switched off.

Index module pins by net. get_pin_by_net() scanned all pins of a module, on the hot path of Module::check_net(), which runs for every net of every gate entering or leaving a module. The net of a pin never changes, so the index only needs maintaining when a pin is created or deleted.

Coalesce the events of bulk operations. A bulk operation changes a module's pins wholesale and a listener has to re-read them anyway, so PinChangedBulkScope collects the pin events of all affected modules and sends a single PinEvent::PinsReload per module. That takes the workload above from 43040 to 48 pin events.

Interactive pin changes such as renaming or reordering a pin group are deliberately not covered by a bulk scope and keep their fine grained events, so the pins tree still updates incrementally while the user edits pins. ModulePinsTree preserves expanded pin groups and the current selection across a reload, so a bulk change no longer collapses the tree.

Notes for review

  • The new PinEvent::PinsReload widens the pin event encoding; the 4-bit packing is now guarded by a static_assert.
  • The event coalescing is opt-in per call site, so nothing changes for code that does not open a scope.

🤖 Generated with Claude Code

julianspeith and others added 4 commits August 11, 2026 20:58
Before changing how module pins are recomputed, pin down the behavior that has
to survive. The existing check_pins test covers the manual pin API but almost
nothing of the automatic path, and enable_automatic_net_checks() and
update_nets() had no test at all.

Six tests are added:

* check_pins_on_gate_assignment: pins appear and disappear as gates enter and
  leave a module, including the generated names and the one-pin group per pin.
* check_pin_direction_transitions: the direction of a pin follows the
  connectivity of its net through input, inout and output, the pin keeps its ID
  across those transitions, and it is removed once the net is fully internal.
* check_pins_of_parent_modules: a boundary net creates a pin in the module and
  in every ancestor, and re-parenting a module moves the pin with it.
* check_pins_on_global_net_marking: marking a net global input or output adds an
  external endpoint and therefore creates, changes, or removes a pin.
* check_pins_with_disabled_net_checks: with the automatic checks disabled
  nothing is updated, update_nets() restores the net sets but deliberately
  creates no pin, re-enabling repairs nothing retroactively, and a manually
  created pin survives the next automatic check.
* check_manual_pin_creation_requires_disabled_checks: create_pin() is rejected
  while the automatic checks are enabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
get_pin_by_net() did a linear scan over all pins of a module. It sits on the
hot path of Module::check_net(), which runs for every net of every gate that
gets assigned to or removed from a module.

The net of a pin never changes, so the index only has to be maintained when a
pin is created or deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assigning gates to a module emits three pin events per created pin and two per
deleted pin. Creating and deleting 24 modules out of the 7144 top level gates
of a sha256 netlist therefore emitted 43040 pin_changed events.

Every event that a python script triggers is handed to the GUI thread over a
blocking queued connection, so the cost of an event is a thread round trip
rather than the handler itself. Measured with `sample` on the workload above,
91% of the script runtime was the python thread waiting: 68% in the
qApp->processEvents() that follows each emit and 23% blocked on the semaphore
of the blocking connection. The netlist work itself accounted for 5%.

A bulk operation changes the pins of a module wholesale and a listener has to
re-read them anyway, so PinChangedBulkScope now collects the pin events of all
affected modules and sends a single PinEvent::PinsReload per module instead.
That takes the workload from 43040 to 48 pin events.

Interactive pin changes such as renaming or reordering a pin group are not
covered by a bulk scope and keep their fine grained events, so the pins tree
still updates incrementally when the user edits pins.

ModulePinsTree preserves expanded pin groups and the current selection across
the reload, so a bulk change no longer collapses the tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@joern274
joern274 merged commit 30c8f0a into master Aug 14, 2026
4 checks passed
@joern274
joern274 deleted the feature/module_pin_performance branch August 14, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants