Public issue detail

Runaway CPU investigation for python3.13: busy poll at PyObject_GC_Del

python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through PyObject_GC_Del, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2patch attempt ready

Last seen: 2026-05-15 03:34 UTC. Public JSON: /v1/issues/019dd1e0-1f56-7862-9fd2-d20d576df8c4

Pull-request-ready diff

This is the current best public patch attempt for the issue. The downloadable .patch now includes a short cover letter so it reads like something you could send upstream with git am. If you only want the raw diff, grab the .diff instead.

best patchcreated: 2026-05-14 15:13 UTCvalidation: ready

python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.

Suggested subject

Skip idle reaping without tracked child PIDs

Commit message.

`supervisord` tracks live managed child processes in `pidhistory`, a map from child PID to process object. When that map is empty, the main loop has no managed child to collect, so avoid the periodic `waitpid(-1)` sweep that otherwise reports “no children” from the kernel.

This keeps the existing one-second poll timeout, preserving signal, socket, and tick responsiveness. This also changes course from the earlier timeout-based plan because review showed that increasing the poll timeout could delay queued signal handling.

Problem. python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through PyObject_GC_Del, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

How this patch connects to the issue.

Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames. The concrete trace signal was repeated `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)`. This failure was observed by Fixer and not independently reproduced as high CPU locally.

The cautious code-level inference is that the relevant work is in supervisor’s application loop, not the Python runtime. `supervisord` already maintains `options.pidhistory` for live managed child PIDs, and uses the local `ServerOptions.waitpid()` wrapper for reaping. When `pidhistory` is empty, the periodic managed-child reap has nothing to collect, matching the no-child condition shown in the trace more closely than checking configured process groups.

The patch keeps the existing poll timeout and gates the main-loop `self.reap()` call on `self.options.pidhistory`. The test helper now counts `waitpid()` calls, and `test_supervisord` covers both a configured-but-idle process group with no tracked child PID and a tracked-child case that still reaps.

The expected effect is to reduce unnecessary idle `waitpid(ECHILD)` calls when there are no tracked managed child processes, without changing signal latency or normal reaping when child PIDs are present. A short source comment explains the `pidhistory` invariant.

Files touched

  • supervisor/supervisord.py
  • supervisor/tests/base.py
  • supervisor/tests/test_supervisord.py

Validation

  • Fixer marked this proposal `ready` on 2026-05-14 15:13 UTC.
  • The underlying issue cluster has 2 report(s) and was last seen 2026-05-15 03:34 UTC.
  • The published diff touches supervisor/supervisord.py, supervisor/tests/base.py, supervisor/tests/test_supervisord.py.
  • Checked contribution/style docs and local subsystem helpers: only `docs/development.rst` contribution guidance was found; the patch follows existing `Supervisor.runforever()`, `options.pidhistory`, `ServerOptions.waitpid()`, and test double conventions. `python3 -m pytest supervisor/tests` Result: failed due existing environment/version blockers: Python 3.13 lacks `unittest.findTestCases`, and this sandbox denies socket creation/bind with `PermissionError: [Errno 1] Operation not permitted`. `python3 -m pytest supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_skips_reap_without_child_pids supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_reaps_with_child_pids supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_poll_dispatchers` Result: passed, 3 tests. `python3 -m pytest supervisor/tests/test_supervisord.py -k 'not test_suite'` Result: passed, 42 selected, 1 deselected. Independent trace attempt: `timeout 2s strace -f -tt -e poll,wait4 python3 -m supervisor.supervisord -n -c supervisor/tests/fixtures/donothing.conf` Result: blocked by sandbox ptrace restrictions: `PTRACE_TRACEME: Operation not permitted`. Fallback bounded smoke check compared a configured process group with no tracked child PID against the original source snapshot: original called `waitpid` once; patched workspace called it zero times. A tracked-child smoke check in the patched workspace still called `waitpid` once. `git diff --check` Result: passed.
diff --git a/supervisor/supervisord.py b/supervisor/supervisord.py
index 0a4f3e6..42fdf6b 100755
--- a/supervisor/supervisord.py
+++ b/supervisor/supervisord.py
@@ -241,7 +241,10 @@ class Supervisor:
             for group in pgroups:
                 group.transition()
 
-            self.reap()
+            if self.options.pidhistory:
+                # pidhistory tracks live managed child PIDs; skip the
+                # periodic waitpid(-1) sweep when there are none to collect.
+                self.reap()
             self.handle_signal()
             self.tick()
 
diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py
index f608b2b..1f25b58 100644
--- a/supervisor/tests/base.py
+++ b/supervisor/tests/base.py
@@ -60,6 +60,7 @@ class DummyOptions:
         self.pidfile_written = False
         self.directory = None
         self.waitpid_return = None, None
+        self.waitpid_calls = 0
         self.kills = {}
         self._signal = None
         self.parent_pipes_closed = None
@@ -145,6 +146,7 @@ class DummyOptions:
         self.pidfile_written = True
 
     def waitpid(self):
+        self.waitpid_calls += 1
         return self.waitpid_return
 
     def kill(self, pid, sig):
diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py
index 3d7b4ff..993cffd 100644
--- a/supervisor/tests/test_supervisord.py
+++ b/supervisor/tests/test_supervisord.py
@@ -665,6 +665,25 @@ class SupervisordTests(unittest.TestCase):
         supervisord.runforever()
         self.assertEqual(len(supervisord.ticks), 3)
 
+    def test_runforever_skips_reap_without_child_pids(self):
+        options = DummyOptions()
+        supervisord = self._makeOne(options)
+        gconfig = DummyPGroupConfig(options)
+        supervisord.process_groups = {'foo': DummyProcessGroup(gconfig)}
+        options.test = True
+        supervisord.runforever()
+        self.assertEqual(options.waitpid_calls, 0)
+
+    def test_runforever_reaps_with_child_pids(self):
+        options = DummyOptions()
+        options.pidhistory = {123: DummyProcess(None)}
+        supervisord = self._makeOne(options)
+        gconfig = DummyPGroupConfig(options)
+        supervisord.process_groups = {'foo': DummyProcessGroup(gconfig)}
+        options.test = True
+        supervisord.runforever()
+        self.assertEqual(options.waitpid_calls, 1)
+
     def test_runforever_poll_dispatchers(self):
         options = DummyOptions()
         options.poller.result = [6], [7, 8]

Technical snapshot

This is the clearest retained userspace thread cluster Fixer captured while the process was spinning.

Representative thread backtrace

  • Command: /usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf
  • Why Fixer classified it this way: The trace repeatedly returns to a poll-family syscall without meaningful blocking, which suggests a busy event-loop wakeup.
  • Thread summary: thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>
  • Contention signals: gdb-stderr: gdb: warning: Couldn't determine a path for the index cache directory.
  • Repeated loop: poll -> wait4 -> poll
  • Top syscalls: poll x5, wait4 x5, restart_syscall x1
  • Package: python3.13-minimal 3.13.12-1
  • Kernel: 6.17.10+deb14-amd64
  • Distribution: debian
0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>
0x0000000000c1daf6 in ?? ()
0x00000000100e8902 in ?? ()
0x0000000000a56c88 in _PyRuntime ()
0x00007f1c6e7efead in ?? () from target:<path>
0x0000000000000007 in ?? ()
0x000000000054aeb0 in PyMem_Realloc ()
0x000000000067ca5b in ?? ()

Possible duplicates

These are suggestions based on sanitized trigram similarity plus structured fields like package, subsystem, classification, and wait site. They are not auto-merged.

python3.13 is stuck in a likely busy poll loop: 40.65% of sampled CPU passed through idle_cpu, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2similarity: 99%

Why this looks related: same classification, same package, same source package, same subsystem, same target, same wait site, very similar public summary

Last seen: 2026-05-19 02:35 UTC. Public page: /issues/019dfead-0293-7062-b24e-c9b04c2f1edb. Public JSON: /v1/issues/019dfead-0293-7062-b24e-c9b04c2f1edb

python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2similarity: 99%

Why this looks related: same classification, same package, same source package, same subsystem, same target, same wait site, very similar public summary

Last seen: 2026-05-19 01:48 UTC. Public page: /issues/019dc2a0-9ae7-7f42-8f59-415fe22abe51. Public JSON: /v1/issues/019dc2a0-9ae7-7f42-8f59-415fe22abe51

python3.13 is stuck in a likely busy poll loop: 50.00% of sampled CPU passed through read_tsc, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2similarity: 99%

Why this looks related: same classification, same package, same source package, same subsystem, same target, same wait site, very similar public summary

Last seen: 2026-05-19 01:01 UTC. Public page: /issues/019da8df-cd0c-7880-ab1e-6dd908d64437. Public JSON: /v1/issues/019da8df-cd0c-7880-ab1e-6dd908d64437

python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through PyUnicode_FromWideChar, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2patch attempt readysimilarity: 99%

Why this looks related: same classification, same package, same source package, same subsystem, same target, same wait site, very similar public summary

Last seen: 2026-05-13 02:59 UTC. Public page: /issues/019e176a-65b7-7d92-a20c-8c276f7ca31a. Public JSON: /v1/issues/019e176a-65b7-7d92-a20c-8c276f7ca31a

python3.13 is stuck in a likely busy poll loop: 17.46% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 2 thread(s) around 0x00007f7052c74ffe in ?? () from target:<path> and 1 thread(s) around 0x00007f70501a4447 in ?? () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2successful triagesimilarity: 92%

Why this looks related: same classification, same package, same source package, same subsystem, same target

Last seen: 2026-05-19 04:12 UTC. Public page: /issues/019dd98b-0e1d-76e3-9171-f0f72c689536. Public JSON: /v1/issues/019dd98b-0e1d-76e3-9171-f0f72c689536

perl is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through Perl_runops_standard, with repeated thread backtraces show 1 thread(s) around 0x00007fce497c17d2 in ?? () from target:<path>.

highpackage: perl-basesource: perlscore: 106reports: 3patch attempt readysimilarity: 76%

Why this looks related: same classification, same subsystem, same wait site

Last seen: 2026-05-19 04:12 UTC. Public page: /issues/019d4017-20f8-7752-9734-0bcd71623118. Public JSON: /v1/issues/019d4017-20f8-7752-9734-0bcd71623118

Worker outcome summary

This issue has 2 recorded worker attempts. Only ready diffs and ready triage handoffs get dedicated public boards. Diagnosis-only reports and blocked attempts are summarized here so it is easier to see why work stalled.

2 ready patch attempts

No ready triage handoffs, diagnosis-only reports, failed patch attempts, explained impossible attempts, or other attempt states.

Published attempts

ready patch attempt

patch

python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.

state: readycreated: 2026-05-14 15:13 UTCvalidation: ready
Published session

Prompt

## Plan Pass

You are planning a fixer patch before any edits happen.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. The original pre-edit snapshot is available at `./source` if you need to inspect it. For interpreter processes, plan from the script/application entrypoint evidence first and include the runtime only as a second investigation target unless the evidence proves a runtime bug.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `supervisor` upstream patch, not just a Debian-local workaround. Inspect the relevant code, nearby callers, project contribution docs, and local helper/compat APIs, but do not edit files in this pass.

Return a short markdown plan with these exact sections:

## Problem
## Evidence Confidence
## Proposed Subject
## Patch Plan
## Risks
## Validation

Classify `## Evidence Confidence` as exactly one of `reproduced`, `observed`, or `inferred`. Use `inferred` only for a no-patch diagnosis/report plan unless you can name the extra evidence you will collect before editing; inferred source patches are blocked by Fixer because they are not pull-request-ready. For `observed` source-patch plans, plan to say in the final `## Issue Connection` that the failure was observed by Fixer and not independently reproduced. The plan must explain how the proposed code change addresses the observed issue evidence, call out any prior Fixer patch that should be improved or replaced, reject awkward control flow such as avoidable `goto` if there is a cleaner bounded alternative, name any local helper APIs or maintainer conventions the patch should follow, and keep the intended maintainer-facing explanation clear enough that someone unfamiliar with the local complaint wording can still follow the fix. In `## Validation`, name the reproducible configure/build/test entrypoint you will try from the workspace root before any focused leaf compile or smoke check, and include one bounded independent reproduction attempt for the collected failure signal when it is safe and cheap. Do not plan to claim `reproduced` unless that reproduction command or test can actually show the failure.

## Patch Pass

You are working on a bounded fixer proposal.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Produce the smallest reasonable patch for the target repository, keep the change upstreamable, prefer the clearest control flow available, and do not keep avoidable `goto` when a simpler structure would read better. Before introducing new file, process, allocation, locking, networking, or platform APIs, inspect nearby code and project contribution docs for existing helpers or compatibility wrappers and use those local patterns unless you can explain why they do not fit. Validate from a reproducible workspace-root entrypoint before falling back to focused leaf commands; if a build or test cannot run, report the exact command, the exact blocker, and any narrower check you ran instead. During validation, also try one bounded independent reproduction of the collected failure signal when it is safe and cheap, such as a failing test, smoke command, perf/strace comparison, or before/after runtime check. Only use `reproduced` if that command or test actually reproduced the failure; otherwise keep `observed` and report the reproduction blocker. The final explanation must connect the observed issue evidence to the actual code change, not just paraphrase the diff. Write like a maintainer is going to read the patch mail cold: explain the bug in plain language, define subsystem-specific jargon the first time you need it, and make the causal story obvious. Explicitly classify evidence confidence as `reproduced`, `observed`, or `inferred`: `reproduced` means you reproduced the failure locally; `observed` means Fixer has direct crash/log/trace evidence but you did not independently reproduce it; `inferred` means the source patch is not pull-request-ready, so do not leave a source diff unless you first gather stronger observed/reproduced evidence; otherwise return a no-patch diagnosis/report. For any source-changing `observed` patch, say explicitly in `## Issue Connection` that the failure was observed by Fixer and not independently reproduced. If you introduce non-obvious state translation, index remapping, or backend split logic, add a short source comment that explains the invariant being preserved.

Start by explaining the likely root cause from the collected perf, strace, and /proc evidence. If you cannot land a safe patch, leave a diagnosis that is strong enough for an upstream bug report.

Interpreter process expectation: when the hot process is Python, Perl, shell, Node, Ruby, PHP, Lua, or another interpreter, inspect both the runtime and the script/application entrypoint from `details.interpreter_process`. Prefer the script or application package when evidence points there; do not patch the interpreter runtime merely because the hot binary is `python`, `perl`, `bash`, or similar. Runtime fixes are welcome only after checking the entrypoint and explaining why the runtime mishandles the workload or why running bad code faster/safer is itself the right upstream fix.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `supervisor` upstream patch, not just a Debian-local workaround. 

Keep the change narrowly scoped and summarize validation clearly.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Evidence Confidence
Exactly one word: `reproduced`, `observed`, or `inferred`. Use `reproduced` only when you reproduced the failure locally with a command or test, and include that command/test in `## Validation`. Use `observed` when Fixer has direct crash/log/trace evidence but you did not independently reproduce it. If `## Git Add Paths` lists source files for an `observed` patch, `## Issue Connection` must explicitly say the failure was observed by Fixer and not independently reproduced. Use `inferred` for profiler/strace/indirect evidence; inferred responses may be no-patch diagnoses or reports, but inferred source patches are not pull-request-ready until stronger evidence is gathered.

## Issue Connection
Write this as maintainer-facing patch mail, not as local Fixer notes. Cover four things explicitly in readable sentences: the user-visible symptom or the exact collected signal, the code-level cause or the cautious inference from evidence, the specific change you made, and the expected effect. Do not invent a reproducer, command line, crash, or user-visible failure that is not present in the evidence bundle. If the evidence is direct-but-not-reproduced, say it was observed by Fixer and not independently reproduced. If the evidence is indirect and you did not gather stronger evidence, do not leave a source diff; write a no-patch diagnosis/report instead. Include an explicit effect sentence such as `The expected effect is ...`, `This should reduce ...`, or `This prevents ...` for source patches. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Use `None` only when you intentionally made no source changes. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them. Include the independent reproduction command/test and result when `## Evidence Confidence` is `reproduced`; if reproduction was attempted but blocked, name the exact blocker and keep confidence at `observed` or `inferred`.

Before editing, read the plan at `./plan-output.txt` and follow it unless the code proves part of it wrong. If you change course, say so explicitly in the final write-up instead of silently drifting from the plan.

## Review Pass 1

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the first patch pass. The original pre-edit snapshot is available at `./source` for diffing. The workspace currently changes these repo-relative paths: supervisor/supervisord.py, supervisor/tests/base.py, supervisor/tests/test_supervisord.py. Verify that `## Git Add Paths` matches this exact set and that `## Issue Connection` explains every shipped functional file.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `supervisor` upstream patch, not just a Debian-local workaround. The latest author response is at `./patch-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment. Reject patches that omit `## Evidence Confidence`, use anything other than `reproduced`, `observed`, or `inferred`, or sound more certain than that evidence level allows. Reject patches that claim `reproduced` unless `## Validation` names the concrete reproduction command or test and its result. Reject patches that invent a reproducer, command line, crash, or user-visible failure not present in the evidence bundle; indirect profiler or strace evidence may justify a cautious mitigation, but the author must say it is indirect instead of presenting it as a confirmed upstream bug. Reject patches that introduce generic libc/std APIs when nearby code or project docs provide a local compat/helper API for the same job, unless the author explicitly justifies the exception. Reject validation that only reports a leaf object/syntax build when a project-level configure/build/test entrypoint exists and was not attempted; if a full build cannot run, the author must show the exact failed command and blocker.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

## Refinement Pass 1

You are refining a fixer patch after an explicit code review.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Read the latest author response at `./patch-output.txt`. Read the review report at `./review-1-output.txt`. This is refinement round 1. The original pre-edit snapshot is available at `./source` if you need to compare the current patch against it. Re-read the planning pass at `./plan-output.txt` before editing. The workspace currently changes these repo-relative paths: supervisor/supervisord.py, supervisor/tests/base.py, supervisor/tests/test_supervisord.py. Either keep that exact set synchronized with `## Git Add Paths` and `## Issue Connection`, or revert any unintended file before you answer.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `supervisor` upstream patch, not just a Debian-local workaround. Address the review findings with the smallest reasonable follow-up changes. If the review identifies a runtime or correctness bug in the changed code, you must update the code itself before answering; a metadata-only response is not sufficient. Keep the patch upstream-friendly, use local project helpers and compat APIs when available, avoid awkward control flow when a simpler structure will do, keep the final response gittable, make the maintainer-facing explanation plain and direct, keep `## Evidence Confidence` honest, add short comments for any non-obvious translation or remapping logic, run relevant project-level build/tests from the workspace root before narrower checks when possible, try a bounded independent reproduction command/test if the review or plan shows one is safe, and summarize which review findings you addressed.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Evidence Confidence
Exactly one word: `reproduced`, `observed`, or `inferred`. Use `reproduced` only when you reproduced the failure locally with a command or test, and include that command/test in `## Valida

[truncated 13958 bytes]

Response

## Plan Pass

## Problem
Fixer observed `supervisord` running as `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames, while `strace` showed a repeated supervisor loop of `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)` timeout. The entrypoint is the supervisor script, so the first patch target should be supervisor’s event loop, not Python 3.13.

## Evidence Confidence
observed

## Proposed Subject
Reduce unnecessary idle supervisor wakeups when there are no managed child processes.

## Patch Plan
Inspect and patch `supervisor/supervisord.py` around `Supervisor.runforever()`, keeping existing wrappers: `self.options.poller`, `self.options.waitpid()`, `self.options.logger`, and the existing signal receiver API. Avoid runtime/Python changes.

Change the loop’s fixed one-second poll timeout into a bounded helper or local calculation that still polls promptly while processes exist, but permits a longer idle timeout, likely the smallest tick period of 5 seconds, when `process_groups`/process dispatchers indicate there is no child process work. This directly targets the observed `ECHILD` + one-second poll cadence without claiming to fix the unexplained `PyObject_GC_Del` runtime samples.

Add focused unit coverage in `supervisor/tests/test_supervisord.py` using existing `DummyOptions`, `DummyPoller`, and `DummyProcessGroup` conventions to assert the idle no-process case passes the longer timeout and the active process/dispatcher case preserves the current one-second behavior.

No relevant prior Fixer patch was found. The only applied Debian patch is `py313.patch`, a Python 3.13 test compatibility patch, and it should be left alone.

Reject awkward control flow such as `goto`-style early loop escapes; use a small bounded timeout calculation in the existing loop structure.

## Risks
The evidence does not independently prove a supervisor logic bug or that the one-second wakeup explains the sampled 99.9% CPU. The patch should be framed as a targeted idle-loop mitigation observed by Fixer, not a confirmed runaway CPU fix.

Longer idle polling must not delay signal handling, XML-RPC/control socket activity, process transitions, or tick events. Since signals and fd activity wake `poll()`, the main risk is delaying periodic tick processing beyond the smallest tick period.

## Validation
First run the project test entrypoint from the workspace root: `python3 -m pytest supervisor/tests`.

Then run focused tests: `python3 -m pytest supervisor/tests/test_supervisord.py`.

Bounded independent reproduction attempt: run `supervisord` with the existing no-program fixture or an equivalent temporary config under `strace -f -tt -e poll,wait4` for a few seconds before and after the patch, and compare the idle `wait4(ECHILD)`/`poll()` cadence. Final notes should say the original failure was observed by Fixer and not independently reproduced unless this command actually demonstrates the same failure signal.

## Patch Pass

Subject: Reduce idle supervisord poll wakeups

## Commit Message
When no process groups are configured, `supervisord` has no child process state to advance. In that idle state, keep polling sockets, but let the main loop sleep until the next `Tick5` event instead of waking every second.

This keeps the existing one-second timeout whenever process groups exist, so managed process startup, transitions, and reaping behavior remain unchanged.

## Evidence Confidence
observed

## Issue Connection
Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames. The direct trace signal was an idle loop repeatedly doing `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)`. This failure was observed by Fixer and not independently reproduced as high CPU locally.

The cautious code-level inference is that this is supervisor application-loop behavior, not a proven Python runtime bug: `supervisord` calls its existing poller wrapper with a fixed one-second timeout even when there are no configured process groups and therefore no child process work to transition or reap.

The patch adds a small `_poll_timeout()` helper in `Supervisor`, using the existing `self.options.poller` path. It preserves the one-second timeout when `self.process_groups` is non-empty, and uses the smallest `TickEvent` period, currently 5 seconds, when there are no process groups. A short source comment explains the invariant: with no process groups, only sockets and tick notifications remain, and socket activity wakes `poll()` early.

The expected effect is to reduce unnecessary idle wakeups and the matching no-child `waitpid` checks for no-program supervisor configurations, while keeping normal managed-process behavior prompt.

## Git Add Paths
supervisor/supervisord.py
supervisor/tests/base.py
supervisor/tests/test_supervisord.py

## Validation
`python3 -m pytest supervisor/tests`  
Result: failed due existing environment/version blockers unrelated to this patch: Python 3.13 lacks `unittest.findTestCases`, and the sandbox denies socket creation/bind with `PermissionError: [Errno 1] Operation not permitted`.

`python3 -m pytest supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_uses_tick_timeout_without_process_groups supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_uses_short_timeout_with_process_groups supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_poll_dispatchers`  
Result: passed, 3 tests.

`python3 -m pytest supervisor/tests/test_supervisord.py -k 'not test_suite'`  
Result: passed, 42 tests selected, 1 deselected.

Independent reproduction attempt: `timeout 2s strace -f -tt -e poll,wait4 python3 -m supervisor.supervisord -n -c supervisor/tests/fixtures/donothing.conf`  
Result: blocked by sandbox ptrace restrictions: `PTRACE_TRACEME: Operation not permitted`.

Fallback bounded smoke check recorded the poller timeout for the no-process loop. Original snapshot printed `[1]`; patched workspace printed `[5]`. This confirms the patch changes the collected idle poll cadence, but not the original high-CPU condition, so confidence remains `observed`.

## Review Pass 1

RESULT: fix-needed

## Findings
- `supervisor/supervisord.py:208` and `supervisor/supervisord.py:253`: the longer no-process timeout can delay signal handling from 1 second to 5 seconds. `supervisord` queues signals in `SignalReceiver.receive()` and only processes them after `poll()` returns at `supervisor/supervisord.py:244`. On Python 3.13, a signal handler that returns does not necessarily break `select.poll().poll()` early because interrupted syscalls are retried; I confirmed a local `select.poll().poll(5000)` still waited 5 seconds after a queued `SIGTERM`. This regresses shutdown/reload/log-reopen responsiveness for exactly the no-process configuration being changed. A safer mitigation would target the unnecessary `waitpid(ECHILD)` work directly, or add a real wakeup mechanism before increasing the poll sleep.

- `supervisor/tests/test_supervisord.py:668`: the new tests assert only the timeout value, so they lock in the regression above without covering the signal path that the main loop also owns. Add coverage showing a queued signal is handled with the intended responsiveness, or avoid changing the poll timeout. The author response says normal behavior remains prompt, but the shipped tests do not validate that claim.

The `## Git Add Paths` section matches the changed file set exactly, and the evidence confidence is correctly classified as `observed`.

## Refinement Pass 1

Subject: Avoid idle waitpid calls without process groups

## Commit Message
When `supervisord` has no process groups, it has no managed child processes to reap. Avoid calling the existing `ServerOptions.waitpid()` wrapper from the main loop in that state, so idle no-program configurations do not repeatedly ask the kernel for children that cannot exist.

The poll timeout remains unchanged, preserving existing signal, socket, and tick responsiveness.

## Evidence Confidence
observed

## Issue Connection
Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames. The concrete trace signal was repeated `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)`. This failure was observed by Fixer and not independently reproduced as high CPU locally.

The cautious code-level inference is that the relevant work is in supervisor’s application loop, not the Python runtime: `supervisord` reaps children every loop even when `self.process_groups` is empty. With no process groups, supervisor has no managed children to reap, so `waitpid(-1)` can only return the no-children condition seen in the trace.

The patch keeps the existing one-second poll timeout, addressing the review finding that a longer timeout could delay queued signal handling. It instead gates `self.reap()` on `self.process_groups`, using the existing `ServerOptions.waitpid()` wrapper when reaping is still needed. The test helper now counts `waitpid()` calls, and `test_supervisord` covers both the no-process-group skip and the process-group reap path.

The expected effect is to reduce unnecessary idle `waitpid(ECHILD)` calls for no-program supervisor configurations without changing signal latency or normal managed-process behavior. A short source comment explains why the reap is skipped.

## Git Add Paths
supervisor/supervisord.py
supervisor/tests/base.py
supervisor/tests/test_supervisord.py

## Validation
`python3 -m pytest supervisor/tests`  
Result: failed due existing environment/version blockers: Python 3.13 lacks `unittest.findTestCases`, and this sandbox denies socket creation/bind with `PermissionError: [Errno 1] Operation not permitted`.

`python3 -m pytest supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_skips_reap_without_process_groups supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_reaps_with_process_groups supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_poll_dispatchers`  
Result: passed, 3 tests.

`python3 -m pytest supervisor/tests/test_supervisord.py -k 'not test_suite'`  
Result: passed, 42 selected, 1 deselected.

Independent trace attempt: `timeout 2s strace -f -tt -e poll,wait4 python3 -m supervisor.supervisord -n -c supervisor/tests/fixtures/donothing.conf`  
Result: blocked by sandbox ptrace restrictions: `PTRACE_TRACEME: Operation not permitted`.

Fallback bounded smoke check compared the no-process main-loop path against the original source snapshot: original called `waitpid` once; patched workspace called it zero times.

`git diff --check`  
Result: passed.

## Review Pass 2

RESULT: fix-needed

## Findings
- [supervisor/supervisord.py:244](<./workspace/supervisor/supervisord.py:244>): the condition being added does not follow from the evidence and may not mitigate the observed trace. The evidence proves `wait4(-1, WNOHANG)` returned `ECHILD`, meaning no child processes existed at that moment; it does not prove `self.process_groups` was empty. A supervisor can still have configured process groups while no process is currently running, in which case `self.process_groups` is non-empty and this patch continues to call `waitpid()` every loop, preserving the observed `wait4(ECHILD)`/`poll(1000)` cadence. Use a condition tied to actual reapable managed children, or collect evidence that the observed process truly had no configured process groups before scoping the patch to this case.

- [refine-1-output.txt](<./refine-1-output.txt:14>): the Issue Connection says this is for “no-program supervisor configurations,” but that condition is not present in the evidence bundle. The fallback smoke check only demonstrates the artificial no-process-group path, not the collected failure signal. Reword the patch mail to make this a clearly scoped mitigation if stronger evidence is added, or adjust the patch so it targets the evidenced no-children state rather than an unevidenced no-groups state.

`## Git Add Paths` matches the current changed file set exactly, and the evidence confidence is correctly `observed`.

## Refinement Pass 2

Subject: Skip idle reaping without tracked child PIDs

## Commit Message
`supervisord` tracks live managed child processes in `pidhistory`, a map from child PID to process object. When that map is empty, the main loop has no managed child to collect, so avoid the periodic `waitpid(-1)` sweep that otherwise reports “no children” from the kernel.

This keeps the existing one-second poll timeout, preserving signal, socket, and tick responsiveness. This also changes course from the earlier timeout-based plan because review showed that increasing the poll timeout could delay queued signal handling.

## Evidence Confidence
observed

## Issue Connection
Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames. The concrete trace signal was repeated `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)`. This failure was observed by Fixer and not independently reproduced as high CPU locally.

The cautious code-level inference is that the relevant work is in supervisor’s application loop, not the Python runtime. `supervisord` already maintains `options.pidhistory` for live managed child PIDs, and uses the local `ServerOptions.waitpid()` wrapper for reaping. When `pidhistory` is empty, the periodic managed-child reap has nothing to collect, matching the no-child condition shown in the trace more closely than checking configured process groups.

The patch keeps the existing poll timeout and gates the main-loop `self.reap()` call on `self.options.pidhistory`. The test helper now counts `waitpid()` calls, and `test_supervisord` covers both a configured-but-idle process group with no tracked child PID and a tracked-child case that still reaps.

The expected effect is to reduce unnecessary idle `waitpid(ECHILD)` calls when there are no tracked managed child processes, without changing signal latency or normal reaping when child PIDs are present. A short source comment explains the `pidhistory` invariant.

## Git Add Paths
supervisor/supervisord.py
supervisor/tests/base.py
supervisor/tests/test_supervisord.py

## Validation
Checked contribution/style docs and local subsystem helpers: only `docs/development.rst` contribution guidance was found; the patch follows existing `Supervisor.runforever()`, `options.pidhistory`, `ServerOptions.waitpid()`, and test double conventions.

`python3 -m pytest supervisor/tests`  
Result: failed due existing environment/version blockers: Python 3.13 lacks `unittest.findTestCases`, and this sandbox denies socket creation/bind with `PermissionError: [Errno 1] Operation not permitted`.

`python3 -m pytest supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_skips_reap_without_child_pids supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_reaps_with_child_pids supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_poll_dispatchers`  
Result: passed, 3 tests.

`python3 -m pytest supervisor/tests/test_supervisord.py -k 'not test_suite'`  
Result: passed, 42 selected, 1 deselected.

Independent trace attempt: `timeout 2s strace -f -tt -e poll,wait4 python3 -m supervisor.supervisord -n -c supervisor/tests/fixtures/donothing.conf`  
Result: blocked by sandbox ptrace restrictions: `PTRACE_TRACEME: Operation not permitted`.

Fallback bounded smoke check compared a configured process group with no tracked child PID against the original source snapshot: original called `waitpid` once; patched workspace called it zero times. A tracked-child smoke check in the patched workspace still called `waitpid` once.

`git diff --check`  
Result: passed.

## Review Pass 3

RESULT: ok

No blocking findings. The refined patch now targets `options.pidhistory`, which is the local live-managed-child tracking state, rather than the broader configured process group map. `## Git Add Paths` matches the current changed file set exactly, and the `## Issue Connection` explains the functional change in `supervisord.py` plus the supporting test-helper and test changes. Evidence confidence remains appropriately `observed`, and validation includes the project-level test attempt with concrete blockers plus focused passing tests.

Diff

diff --git a/supervisor/supervisord.py b/supervisor/supervisord.py
index 0a4f3e6..42fdf6b 100755
--- a/supervisor/supervisord.py
+++ b/supervisor/supervisord.py
@@ -241,7 +241,10 @@ class Supervisor:
             for group in pgroups:
                 group.transition()
 
-            self.reap()
+            if self.options.pidhistory:
+                # pidhistory tracks live managed child PIDs; skip the
+                # periodic waitpid(-1) sweep when there are none to collect.
+                self.reap()
             self.handle_signal()
             self.tick()
 
diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py
index f608b2b..1f25b58 100644
--- a/supervisor/tests/base.py
+++ b/supervisor/tests/base.py
@@ -60,6 +60,7 @@ class DummyOptions:
         self.pidfile_written = False
         self.directory = None
         self.waitpid_return = None, None
+        self.waitpid_calls = 0
         self.kills = {}
         self._signal = None
         self.parent_pipes_closed = None
@@ -145,6 +146,7 @@ class DummyOptions:
         self.pidfile_written = True
 
     def waitpid(self):
+        self.waitpid_calls += 1
         return self.waitpid_return
 
     def kill(self, pid, sig):
diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py
index 3d7b4ff..993cffd 100644
--- a/supervisor/tests/test_supervisord.py
+++ b/supervisor/tests/test_supervisord.py
@@ -665,6 +665,25 @@ class SupervisordTests(unittest.TestCase):
         supervisord.runforever()
         self.assertEqual(len(supervisord.ticks), 3)
 
+    def test_runforever_skips_reap_without_child_pids(self):
+        options = DummyOptions()
+        supervisord = self._makeOne(options)
+        gconfig = DummyPGroupConfig(options)
+        supervisord.process_groups = {'foo': DummyProcessGroup(gconfig)}
+        options.test = True
+        supervisord.runforever()
+        self.assertEqual(options.waitpid_calls, 0)
+
+    def test_runforever_reaps_with_child_pids(self):
+        options = DummyOptions()
+        options.pidhistory = {123: DummyProcess(None)}
+        supervisord = self._makeOne(options)
+        gconfig = DummyPGroupConfig(options)
+        supervisord.process_groups = {'foo': DummyProcessGroup(gconfig)}
+        options.test = True
+        supervisord.runforever()
+        self.assertEqual(options.waitpid_calls, 1)
+
     def test_runforever_poll_dispatchers(self):
         options = DummyOptions()
         options.poller.result = [6], [7, 8]

ready patch attempt

patch

python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.

state: readycreated: 2026-05-14 15:13 UTCvalidation: ready
Published session

Prompt

## Plan Pass

You are planning a fixer patch before any edits happen.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. The original pre-edit snapshot is available at `./source` if you need to inspect it. For interpreter processes, plan from the script/application entrypoint evidence first and include the runtime only as a second investigation target unless the evidence proves a runtime bug.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `supervisor` upstream patch, not just a Debian-local workaround. Inspect the relevant code, nearby callers, project contribution docs, and local helper/compat APIs, but do not edit files in this pass.

Return a short markdown plan with these exact sections:

## Problem
## Evidence Confidence
## Proposed Subject
## Patch Plan
## Risks
## Validation

Classify `## Evidence Confidence` as exactly one of `reproduced`, `observed`, or `inferred`. Use `inferred` only for a no-patch diagnosis/report plan unless you can name the extra evidence you will collect before editing; inferred source patches are blocked by Fixer because they are not pull-request-ready. For `observed` source-patch plans, plan to say in the final `## Issue Connection` that the failure was observed by Fixer and not independently reproduced. The plan must explain how the proposed code change addresses the observed issue evidence, call out any prior Fixer patch that should be improved or replaced, reject awkward control flow such as avoidable `goto` if there is a cleaner bounded alternative, name any local helper APIs or maintainer conventions the patch should follow, and keep the intended maintainer-facing explanation clear enough that someone unfamiliar with the local complaint wording can still follow the fix. In `## Validation`, name the reproducible configure/build/test entrypoint you will try from the workspace root before any focused leaf compile or smoke check, and include one bounded independent reproduction attempt for the collected failure signal when it is safe and cheap. Do not plan to claim `reproduced` unless that reproduction command or test can actually show the failure.

## Patch Pass

You are working on a bounded fixer proposal.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Produce the smallest reasonable patch for the target repository, keep the change upstreamable, prefer the clearest control flow available, and do not keep avoidable `goto` when a simpler structure would read better. Before introducing new file, process, allocation, locking, networking, or platform APIs, inspect nearby code and project contribution docs for existing helpers or compatibility wrappers and use those local patterns unless you can explain why they do not fit. Validate from a reproducible workspace-root entrypoint before falling back to focused leaf commands; if a build or test cannot run, report the exact command, the exact blocker, and any narrower check you ran instead. During validation, also try one bounded independent reproduction of the collected failure signal when it is safe and cheap, such as a failing test, smoke command, perf/strace comparison, or before/after runtime check. Only use `reproduced` if that command or test actually reproduced the failure; otherwise keep `observed` and report the reproduction blocker. The final explanation must connect the observed issue evidence to the actual code change, not just paraphrase the diff. Write like a maintainer is going to read the patch mail cold: explain the bug in plain language, define subsystem-specific jargon the first time you need it, and make the causal story obvious. Explicitly classify evidence confidence as `reproduced`, `observed`, or `inferred`: `reproduced` means you reproduced the failure locally; `observed` means Fixer has direct crash/log/trace evidence but you did not independently reproduce it; `inferred` means the source patch is not pull-request-ready, so do not leave a source diff unless you first gather stronger observed/reproduced evidence; otherwise return a no-patch diagnosis/report. For any source-changing `observed` patch, say explicitly in `## Issue Connection` that the failure was observed by Fixer and not independently reproduced. If you introduce non-obvious state translation, index remapping, or backend split logic, add a short source comment that explains the invariant being preserved.

Start by explaining the likely root cause from the collected perf, strace, and /proc evidence. If you cannot land a safe patch, leave a diagnosis that is strong enough for an upstream bug report.

Interpreter process expectation: when the hot process is Python, Perl, shell, Node, Ruby, PHP, Lua, or another interpreter, inspect both the runtime and the script/application entrypoint from `details.interpreter_process`. Prefer the script or application package when evidence points there; do not patch the interpreter runtime merely because the hot binary is `python`, `perl`, `bash`, or similar. Runtime fixes are welcome only after checking the entrypoint and explaining why the runtime mishandles the workload or why running bad code faster/safer is itself the right upstream fix.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `supervisor` upstream patch, not just a Debian-local workaround. 

Keep the change narrowly scoped and summarize validation clearly.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Evidence Confidence
Exactly one word: `reproduced`, `observed`, or `inferred`. Use `reproduced` only when you reproduced the failure locally with a command or test, and include that command/test in `## Validation`. Use `observed` when Fixer has direct crash/log/trace evidence but you did not independently reproduce it. If `## Git Add Paths` lists source files for an `observed` patch, `## Issue Connection` must explicitly say the failure was observed by Fixer and not independently reproduced. Use `inferred` for profiler/strace/indirect evidence; inferred responses may be no-patch diagnoses or reports, but inferred source patches are not pull-request-ready until stronger evidence is gathered.

## Issue Connection
Write this as maintainer-facing patch mail, not as local Fixer notes. Cover four things explicitly in readable sentences: the user-visible symptom or the exact collected signal, the code-level cause or the cautious inference from evidence, the specific change you made, and the expected effect. Do not invent a reproducer, command line, crash, or user-visible failure that is not present in the evidence bundle. If the evidence is direct-but-not-reproduced, say it was observed by Fixer and not independently reproduced. If the evidence is indirect and you did not gather stronger evidence, do not leave a source diff; write a no-patch diagnosis/report instead. Include an explicit effect sentence such as `The expected effect is ...`, `This should reduce ...`, or `This prevents ...` for source patches. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Use `None` only when you intentionally made no source changes. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them. Include the independent reproduction command/test and result when `## Evidence Confidence` is `reproduced`; if reproduction was attempted but blocked, name the exact blocker and keep confidence at `observed` or `inferred`.

Before editing, read the plan at `./plan-output.txt` and follow it unless the code proves part of it wrong. If you change course, say so explicitly in the final write-up instead of silently drifting from the plan.

## Review Pass 1

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the first patch pass. The original pre-edit snapshot is available at `./source` for diffing. The workspace currently changes these repo-relative paths: supervisor/supervisord.py, supervisor/tests/base.py, supervisor/tests/test_supervisord.py. Verify that `## Git Add Paths` matches this exact set and that `## Issue Connection` explains every shipped functional file.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `supervisor` upstream patch, not just a Debian-local workaround. The latest author response is at `./patch-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment. Reject patches that omit `## Evidence Confidence`, use anything other than `reproduced`, `observed`, or `inferred`, or sound more certain than that evidence level allows. Reject patches that claim `reproduced` unless `## Validation` names the concrete reproduction command or test and its result. Reject patches that invent a reproducer, command line, crash, or user-visible failure not present in the evidence bundle; indirect profiler or strace evidence may justify a cautious mitigation, but the author must say it is indirect instead of presenting it as a confirmed upstream bug. Reject patches that introduce generic libc/std APIs when nearby code or project docs provide a local compat/helper API for the same job, unless the author explicitly justifies the exception. Reject validation that only reports a leaf object/syntax build when a project-level configure/build/test entrypoint exists and was not attempted; if a full build cannot run, the author must show the exact failed command and blocker.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

## Refinement Pass 1

You are refining a fixer patch after an explicit code review.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Read the latest author response at `./patch-output.txt`. Read the review report at `./review-1-output.txt`. This is refinement round 1. The original pre-edit snapshot is available at `./source` if you need to compare the current patch against it. Re-read the planning pass at `./plan-output.txt` before editing. The workspace currently changes these repo-relative paths: supervisor/supervisord.py, supervisor/tests/base.py, supervisor/tests/test_supervisord.py. Either keep that exact set synchronized with `## Git Add Paths` and `## Issue Connection`, or revert any unintended file before you answer.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `supervisor` upstream patch, not just a Debian-local workaround. Address the review findings with the smallest reasonable follow-up changes. If the review identifies a runtime or correctness bug in the changed code, you must update the code itself before answering; a metadata-only response is not sufficient. Keep the patch upstream-friendly, use local project helpers and compat APIs when available, avoid awkward control flow when a simpler structure will do, keep the final response gittable, make the maintainer-facing explanation plain and direct, keep `## Evidence Confidence` honest, add short comments for any non-obvious translation or remapping logic, run relevant project-level build/tests from the workspace root before narrower checks when possible, try a bounded independent reproduction command/test if the review or plan shows one is safe, and summarize which review findings you addressed.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Evidence Confidence
Exactly one word: `reproduced`, `observed`, or `inferred`. Use `reproduced` only when you reproduced the failure locally with a command or test, and include that command/test in `## Valida

[truncated 13958 bytes]

Response

## Plan Pass

## Problem
Fixer observed `supervisord` running as `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames, while `strace` showed a repeated supervisor loop of `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)` timeout. The entrypoint is the supervisor script, so the first patch target should be supervisor’s event loop, not Python 3.13.

## Evidence Confidence
observed

## Proposed Subject
Reduce unnecessary idle supervisor wakeups when there are no managed child processes.

## Patch Plan
Inspect and patch `supervisor/supervisord.py` around `Supervisor.runforever()`, keeping existing wrappers: `self.options.poller`, `self.options.waitpid()`, `self.options.logger`, and the existing signal receiver API. Avoid runtime/Python changes.

Change the loop’s fixed one-second poll timeout into a bounded helper or local calculation that still polls promptly while processes exist, but permits a longer idle timeout, likely the smallest tick period of 5 seconds, when `process_groups`/process dispatchers indicate there is no child process work. This directly targets the observed `ECHILD` + one-second poll cadence without claiming to fix the unexplained `PyObject_GC_Del` runtime samples.

Add focused unit coverage in `supervisor/tests/test_supervisord.py` using existing `DummyOptions`, `DummyPoller`, and `DummyProcessGroup` conventions to assert the idle no-process case passes the longer timeout and the active process/dispatcher case preserves the current one-second behavior.

No relevant prior Fixer patch was found. The only applied Debian patch is `py313.patch`, a Python 3.13 test compatibility patch, and it should be left alone.

Reject awkward control flow such as `goto`-style early loop escapes; use a small bounded timeout calculation in the existing loop structure.

## Risks
The evidence does not independently prove a supervisor logic bug or that the one-second wakeup explains the sampled 99.9% CPU. The patch should be framed as a targeted idle-loop mitigation observed by Fixer, not a confirmed runaway CPU fix.

Longer idle polling must not delay signal handling, XML-RPC/control socket activity, process transitions, or tick events. Since signals and fd activity wake `poll()`, the main risk is delaying periodic tick processing beyond the smallest tick period.

## Validation
First run the project test entrypoint from the workspace root: `python3 -m pytest supervisor/tests`.

Then run focused tests: `python3 -m pytest supervisor/tests/test_supervisord.py`.

Bounded independent reproduction attempt: run `supervisord` with the existing no-program fixture or an equivalent temporary config under `strace -f -tt -e poll,wait4` for a few seconds before and after the patch, and compare the idle `wait4(ECHILD)`/`poll()` cadence. Final notes should say the original failure was observed by Fixer and not independently reproduced unless this command actually demonstrates the same failure signal.

## Patch Pass

Subject: Reduce idle supervisord poll wakeups

## Commit Message
When no process groups are configured, `supervisord` has no child process state to advance. In that idle state, keep polling sockets, but let the main loop sleep until the next `Tick5` event instead of waking every second.

This keeps the existing one-second timeout whenever process groups exist, so managed process startup, transitions, and reaping behavior remain unchanged.

## Evidence Confidence
observed

## Issue Connection
Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames. The direct trace signal was an idle loop repeatedly doing `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)`. This failure was observed by Fixer and not independently reproduced as high CPU locally.

The cautious code-level inference is that this is supervisor application-loop behavior, not a proven Python runtime bug: `supervisord` calls its existing poller wrapper with a fixed one-second timeout even when there are no configured process groups and therefore no child process work to transition or reap.

The patch adds a small `_poll_timeout()` helper in `Supervisor`, using the existing `self.options.poller` path. It preserves the one-second timeout when `self.process_groups` is non-empty, and uses the smallest `TickEvent` period, currently 5 seconds, when there are no process groups. A short source comment explains the invariant: with no process groups, only sockets and tick notifications remain, and socket activity wakes `poll()` early.

The expected effect is to reduce unnecessary idle wakeups and the matching no-child `waitpid` checks for no-program supervisor configurations, while keeping normal managed-process behavior prompt.

## Git Add Paths
supervisor/supervisord.py
supervisor/tests/base.py
supervisor/tests/test_supervisord.py

## Validation
`python3 -m pytest supervisor/tests`  
Result: failed due existing environment/version blockers unrelated to this patch: Python 3.13 lacks `unittest.findTestCases`, and the sandbox denies socket creation/bind with `PermissionError: [Errno 1] Operation not permitted`.

`python3 -m pytest supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_uses_tick_timeout_without_process_groups supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_uses_short_timeout_with_process_groups supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_poll_dispatchers`  
Result: passed, 3 tests.

`python3 -m pytest supervisor/tests/test_supervisord.py -k 'not test_suite'`  
Result: passed, 42 tests selected, 1 deselected.

Independent reproduction attempt: `timeout 2s strace -f -tt -e poll,wait4 python3 -m supervisor.supervisord -n -c supervisor/tests/fixtures/donothing.conf`  
Result: blocked by sandbox ptrace restrictions: `PTRACE_TRACEME: Operation not permitted`.

Fallback bounded smoke check recorded the poller timeout for the no-process loop. Original snapshot printed `[1]`; patched workspace printed `[5]`. This confirms the patch changes the collected idle poll cadence, but not the original high-CPU condition, so confidence remains `observed`.

## Review Pass 1

RESULT: fix-needed

## Findings
- `supervisor/supervisord.py:208` and `supervisor/supervisord.py:253`: the longer no-process timeout can delay signal handling from 1 second to 5 seconds. `supervisord` queues signals in `SignalReceiver.receive()` and only processes them after `poll()` returns at `supervisor/supervisord.py:244`. On Python 3.13, a signal handler that returns does not necessarily break `select.poll().poll()` early because interrupted syscalls are retried; I confirmed a local `select.poll().poll(5000)` still waited 5 seconds after a queued `SIGTERM`. This regresses shutdown/reload/log-reopen responsiveness for exactly the no-process configuration being changed. A safer mitigation would target the unnecessary `waitpid(ECHILD)` work directly, or add a real wakeup mechanism before increasing the poll sleep.

- `supervisor/tests/test_supervisord.py:668`: the new tests assert only the timeout value, so they lock in the regression above without covering the signal path that the main loop also owns. Add coverage showing a queued signal is handled with the intended responsiveness, or avoid changing the poll timeout. The author response says normal behavior remains prompt, but the shipped tests do not validate that claim.

The `## Git Add Paths` section matches the changed file set exactly, and the evidence confidence is correctly classified as `observed`.

## Refinement Pass 1

Subject: Avoid idle waitpid calls without process groups

## Commit Message
When `supervisord` has no process groups, it has no managed child processes to reap. Avoid calling the existing `ServerOptions.waitpid()` wrapper from the main loop in that state, so idle no-program configurations do not repeatedly ask the kernel for children that cannot exist.

The poll timeout remains unchanged, preserving existing signal, socket, and tick responsiveness.

## Evidence Confidence
observed

## Issue Connection
Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames. The concrete trace signal was repeated `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)`. This failure was observed by Fixer and not independently reproduced as high CPU locally.

The cautious code-level inference is that the relevant work is in supervisor’s application loop, not the Python runtime: `supervisord` reaps children every loop even when `self.process_groups` is empty. With no process groups, supervisor has no managed children to reap, so `waitpid(-1)` can only return the no-children condition seen in the trace.

The patch keeps the existing one-second poll timeout, addressing the review finding that a longer timeout could delay queued signal handling. It instead gates `self.reap()` on `self.process_groups`, using the existing `ServerOptions.waitpid()` wrapper when reaping is still needed. The test helper now counts `waitpid()` calls, and `test_supervisord` covers both the no-process-group skip and the process-group reap path.

The expected effect is to reduce unnecessary idle `waitpid(ECHILD)` calls for no-program supervisor configurations without changing signal latency or normal managed-process behavior. A short source comment explains why the reap is skipped.

## Git Add Paths
supervisor/supervisord.py
supervisor/tests/base.py
supervisor/tests/test_supervisord.py

## Validation
`python3 -m pytest supervisor/tests`  
Result: failed due existing environment/version blockers: Python 3.13 lacks `unittest.findTestCases`, and this sandbox denies socket creation/bind with `PermissionError: [Errno 1] Operation not permitted`.

`python3 -m pytest supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_skips_reap_without_process_groups supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_reaps_with_process_groups supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_poll_dispatchers`  
Result: passed, 3 tests.

`python3 -m pytest supervisor/tests/test_supervisord.py -k 'not test_suite'`  
Result: passed, 42 selected, 1 deselected.

Independent trace attempt: `timeout 2s strace -f -tt -e poll,wait4 python3 -m supervisor.supervisord -n -c supervisor/tests/fixtures/donothing.conf`  
Result: blocked by sandbox ptrace restrictions: `PTRACE_TRACEME: Operation not permitted`.

Fallback bounded smoke check compared the no-process main-loop path against the original source snapshot: original called `waitpid` once; patched workspace called it zero times.

`git diff --check`  
Result: passed.

## Review Pass 2

RESULT: fix-needed

## Findings
- [supervisor/supervisord.py:244](<./workspace/supervisor/supervisord.py:244>): the condition being added does not follow from the evidence and may not mitigate the observed trace. The evidence proves `wait4(-1, WNOHANG)` returned `ECHILD`, meaning no child processes existed at that moment; it does not prove `self.process_groups` was empty. A supervisor can still have configured process groups while no process is currently running, in which case `self.process_groups` is non-empty and this patch continues to call `waitpid()` every loop, preserving the observed `wait4(ECHILD)`/`poll(1000)` cadence. Use a condition tied to actual reapable managed children, or collect evidence that the observed process truly had no configured process groups before scoping the patch to this case.

- [refine-1-output.txt](<./refine-1-output.txt:14>): the Issue Connection says this is for “no-program supervisor configurations,” but that condition is not present in the evidence bundle. The fallback smoke check only demonstrates the artificial no-process-group path, not the collected failure signal. Reword the patch mail to make this a clearly scoped mitigation if stronger evidence is added, or adjust the patch so it targets the evidenced no-children state rather than an unevidenced no-groups state.

`## Git Add Paths` matches the current changed file set exactly, and the evidence confidence is correctly `observed`.

## Refinement Pass 2

Subject: Skip idle reaping without tracked child PIDs

## Commit Message
`supervisord` tracks live managed child processes in `pidhistory`, a map from child PID to process object. When that map is empty, the main loop has no managed child to collect, so avoid the periodic `waitpid(-1)` sweep that otherwise reports “no children” from the kernel.

This keeps the existing one-second poll timeout, preserving signal, socket, and tick responsiveness. This also changes course from the earlier timeout-based plan because review showed that increasing the poll timeout could delay queued signal handling.

## Evidence Confidence
observed

## Issue Connection
Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames. The concrete trace signal was repeated `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)`. This failure was observed by Fixer and not independently reproduced as high CPU locally.

The cautious code-level inference is that the relevant work is in supervisor’s application loop, not the Python runtime. `supervisord` already maintains `options.pidhistory` for live managed child PIDs, and uses the local `ServerOptions.waitpid()` wrapper for reaping. When `pidhistory` is empty, the periodic managed-child reap has nothing to collect, matching the no-child condition shown in the trace more closely than checking configured process groups.

The patch keeps the existing poll timeout and gates the main-loop `self.reap()` call on `self.options.pidhistory`. The test helper now counts `waitpid()` calls, and `test_supervisord` covers both a configured-but-idle process group with no tracked child PID and a tracked-child case that still reaps.

The expected effect is to reduce unnecessary idle `waitpid(ECHILD)` calls when there are no tracked managed child processes, without changing signal latency or normal reaping when child PIDs are present. A short source comment explains the `pidhistory` invariant.

## Git Add Paths
supervisor/supervisord.py
supervisor/tests/base.py
supervisor/tests/test_supervisord.py

## Validation
Checked contribution/style docs and local subsystem helpers: only `docs/development.rst` contribution guidance was found; the patch follows existing `Supervisor.runforever()`, `options.pidhistory`, `ServerOptions.waitpid()`, and test double conventions.

`python3 -m pytest supervisor/tests`  
Result: failed due existing environment/version blockers: Python 3.13 lacks `unittest.findTestCases`, and this sandbox denies socket creation/bind with `PermissionError: [Errno 1] Operation not permitted`.

`python3 -m pytest supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_skips_reap_without_child_pids supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_reaps_with_child_pids supervisor/tests/test_supervisord.py::SupervisordTests::test_runforever_poll_dispatchers`  
Result: passed, 3 tests.

`python3 -m pytest supervisor/tests/test_supervisord.py -k 'not test_suite'`  
Result: passed, 42 selected, 1 deselected.

Independent trace attempt: `timeout 2s strace -f -tt -e poll,wait4 python3 -m supervisor.supervisord -n -c supervisor/tests/fixtures/donothing.conf`  
Result: blocked by sandbox ptrace restrictions: `PTRACE_TRACEME: Operation not permitted`.

Fallback bounded smoke check compared a configured process group with no tracked child PID against the original source snapshot: original called `waitpid` once; patched workspace called it zero times. A tracked-child smoke check in the patched workspace still called `waitpid` once.

`git diff --check`  
Result: passed.

## Review Pass 3

RESULT: ok

No blocking findings. The refined patch now targets `options.pidhistory`, which is the local live-managed-child tracking state, rather than the broader configured process group map. `## Git Add Paths` matches the current changed file set exactly, and the `## Issue Connection` explains the functional change in `supervisord.py` plus the supporting test-helper and test changes. Evidence confidence remains appropriately `observed`, and validation includes the project-level test attempt with concrete blockers plus focused passing tests.

Diff

diff --git a/supervisor/supervisord.py b/supervisor/supervisord.py
index 0a4f3e6..42fdf6b 100755
--- a/supervisor/supervisord.py
+++ b/supervisor/supervisord.py
@@ -241,7 +241,10 @@ class Supervisor:
             for group in pgroups:
                 group.transition()
 
-            self.reap()
+            if self.options.pidhistory:
+                # pidhistory tracks live managed child PIDs; skip the
+                # periodic waitpid(-1) sweep when there are none to collect.
+                self.reap()
             self.handle_signal()
             self.tick()
 
diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py
index f608b2b..1f25b58 100644
--- a/supervisor/tests/base.py
+++ b/supervisor/tests/base.py
@@ -60,6 +60,7 @@ class DummyOptions:
         self.pidfile_written = False
         self.directory = None
         self.waitpid_return = None, None
+        self.waitpid_calls = 0
         self.kills = {}
         self._signal = None
         self.parent_pipes_closed = None
@@ -145,6 +146,7 @@ class DummyOptions:
         self.pidfile_written = True
 
     def waitpid(self):
+        self.waitpid_calls += 1
         return self.waitpid_return
 
     def kill(self, pid, sig):
diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py
index 3d7b4ff..993cffd 100644
--- a/supervisor/tests/test_supervisord.py
+++ b/supervisor/tests/test_supervisord.py
@@ -665,6 +665,25 @@ class SupervisordTests(unittest.TestCase):
         supervisord.runforever()
         self.assertEqual(len(supervisord.ticks), 3)
 
+    def test_runforever_skips_reap_without_child_pids(self):
+        options = DummyOptions()
+        supervisord = self._makeOne(options)
+        gconfig = DummyPGroupConfig(options)
+        supervisord.process_groups = {'foo': DummyProcessGroup(gconfig)}
+        options.test = True
+        supervisord.runforever()
+        self.assertEqual(options.waitpid_calls, 0)
+
+    def test_runforever_reaps_with_child_pids(self):
+        options = DummyOptions()
+        options.pidhistory = {123: DummyProcess(None)}
+        supervisord = self._makeOne(options)
+        gconfig = DummyPGroupConfig(options)
+        supervisord.process_groups = {'foo': DummyProcessGroup(gconfig)}
+        options.test = True
+        supervisord.runforever()
+        self.assertEqual(options.waitpid_calls, 1)
+
     def test_runforever_poll_dispatchers(self):
         options = DummyOptions()
         options.poller.result = [6], [7, 8]