Skip to content

Handle process deaths without scanning unrelated monitors and connections - #495

Merged
LaurentRDC merged 4 commits into
haskell-distributed:masterfrom
vasdommes:claude/nc-deaths-pr
Oct 13, 2026
Merged

LaurentRDC merged 4 commits into
haskell-distributed:masterfrom
vasdommes:claude/nc-deaths-pr

Conversation

@vasdommes

Copy link
Copy Markdown
Contributor

Handle process deaths without scanning unrelated monitors and connections

Problem

On a node with many monitors, links or connections, each process death took
time proportional to all of them, not only to the ones it affects:

  • ncEffectDied partitioned all links and monitors of the node
    (splitNotif), for a node death also all watchers, and it rebuilt
    registeredOnNodes.
  • cleanupProcess partitioned all of localConnections on every process exit.

Local sends also go through the node controller, so they wait behind slow
deaths. We found this in a task scheduler built on distributed-process, which
keeps ~1,000 monitors and ~600 connections and runs tens of thousands of
short-lived processes; these changes cut its total run time by ~10–20%.

Change

The identifiers that die with an identifier (see impliesDeathOf) form at most
three ranges of keys in the derived Ord instances: for a node, the node
itself, its processes and its typed channels; for a process, the process and
its typed channels.

  • Internal.Messaging: deathKey and deathKeys. deathKey ident is a
    monotone projection, and impliesDeathOf a b holds if and only if
    deathKey a b is in deathKeys a.
  • Internal.BiMultiMap: partitionOn, partitionOnBy1st and
    partitionOnBy2nd. partitionOn f ps m is
    Map.partitionWithKey (\k _ -> elem (f k) ps) m for a monotone f,
    computed with Map.spanAntitone.
  • Internal.Types: IdentifierKind and identifierKind, next to nodeOf.
  • Node: splitNotif, the removal of dead watchers, registeredOnNodes and
    cleanupProcess use them.

Behaviour does not change, including the order of notifications and of
forwarded deaths. The code relies on the derived Ord instances of
Identifier, ProcessId and SendPortId; their declarations now say so, and
the new tests check it.

Benchmark

The first commit adds a deaths group to the benchmarks: a process death
(spawn, monitor, exit, notification) while the node holds N unrelated
monitors or connections. Time per death, mean of two runs, GHC 9.10.3, -O2:

Unrelated entries Before + node controller fix + process exit fix
none 14 µs 16 µs 16 µs
1,000 monitors 87 µs 20 µs 20 µs
10,000 monitors 2,870 µs 45 µs 43 µs
1,000 connections 91 µs 92 µs 19 µs
10,000 connections 2,920 µs 2,280 µs 37 µs

Differences of a few µs are noise. The remaining growth from 1,000 to 10,000
entries comes from garbage collection of the larger heap: with +RTS -A64m,
both cases take the same time.

To compare, run at the first commit and then at the last one:

cabal bench distributed-process --benchmark-options '-p deaths --csv base.csv'
cabal bench distributed-process --benchmark-options '-p deaths --baseline base.csv'

Tests

  • New test-suite TestDeathKeys in distributed-process-tests:
    • for all pairs from a set of 26 identifiers, deathKey agrees with
      impliesDeathOf, and deathKey is monotone;
    • QuickCheck: partitionOn, partitionOnBy1st and partitionOnBy2nd give
      the same results as Map.partitionWithKey, partitionWithKeyBy1st and
      partitionWithKeyBy2nd.
  • All distributed-process-tests suites pass.

Notes for reviewers

  • TestDeathKeys uses functions that are new in this PR. Its lower bound on
    distributed-process should become the next release when versions are bumped.
  • Two scans remain, both out of scope here:
    • every death still scans registeredHere, which is keyed by name;
    • a node disconnect still scans all connections in
      closeImplicitReconnections, because connections are keyed by from,
      not to.

🤖 Generated with Claude Code

vasdommes and others added 3 commits October 9, 2026 11:20
The cost of a process death should not depend on the monitors and
connections of other processes on the node. Measure it with 0, 1,000 and
10,000 of each.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ange

ncEffectDied scanned all links and monitors of the node on every death
(splitNotif used partitionWithKeyBy1st with impliesDeathOf), for a node
death also all watchers, and it rebuilt registeredOnNodes. So each death
took time proportional to all monitors and links of the node, and local
sends, which also go through the node controller, waited behind it.

Add deathKey, a monotone projection of identifiers such that
impliesDeathOf a b if and only if deathKey a b is in deathKeys a. The
identifiers that die with a are then a few ranges of keys, which the new
BiMultiMap.partitionOn finds with Map.spanAntitone in time polylogarithmic
in the size of the map, plus the size of the result.

ncEffectDied still scans registeredHere, which is keyed by name.

In the deaths benchmark, a process death with 10,000 unrelated monitors on
the node went from about 2,900 to 45 µs (about 15 µs with none).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
cleanupProcess scanned all of localConnections on every process exit. The
result is part of the node state, which local and remote sends, spawns and
the node controller also use.

Connections are keyed by (from, to), so the connections of the process and
its typed channels are found with partitionOn and deathKey.

In the deaths benchmark, a process death with 10,000 unrelated connections
on the node went from about 2,300 to 37 µs (about 15 µs with none).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@davidsd

davidsd commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

For context, @vasdommes and I encountered performance problems with a distributed-process-based scheduler that we have been working on, and this was one of the fixes.

@LaurentRDC LaurentRDC left a comment

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.

Thanks! I mostly have some organizational comments. Nothing showstopping

Comment thread packages/distributed-process/src/Control/Distributed/Process/Internal/Types.hs Outdated
- Bump distributed-process to 0.7.10, and require it in TestDeathKeys.
- Add @SInCE 0.7.10 to the new exports of BiMultiMap and Messaging.
- Move IdentifierKind and identifierKind to Messaging, next to deathKey,
  their only user.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@LaurentRDC

Copy link
Copy Markdown
Member

Some tests are flaky, I think this is ready to merge.

Thanks @vasdommes !

@LaurentRDC
LaurentRDC merged commit 2dc5ada into haskell-distributed:master Oct 13, 2026
17 of 20 checks passed
@LaurentRDC

Copy link
Copy Markdown
Member

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.

3 participants