You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
docs: full Sphinx build hangs in typing.get_type_hints() while documenting pcapkit/const/reg #744
A full-tree Sphinx build hangs indefinitely while documenting pcapkit/const/reg, on the repo venv (Python 3.14.7, Sphinx 9.1.0).
Confirmed CPU-bound, not a memory leak.
Hangs inside typing.get_type_hints(), called from autodoc.
Reproduces on both -b dummy and -b html.
Reaches ~32% of reading sources, so it is past most of the tree.
Workaround that gets a build through: -D exclude_patterns=pcapkit/const/reg.rst.
Why this matters beyond the inconvenience
It invalidates the warning-count baseline every changelog round has been measured against.#657's rounds compared against build succeeded, 53 warnings (raw grep -c 55, the two extras being the Scapy content lines use_2_bytes_asn: True and CARP overwrites VRRP !). With reg.rst excluded the same head measures 36 real / 38 raw — a different scope, so the two numbers are not comparable. Any future "no new warnings" check needs either this fixed or the exclusion applied consistently on both sides.
Whether Sphinx 9.1.0 or Python 3.14 is the trigger — typing.get_type_hints() behaviour changed across recent Pythons.
Found while running round 9 of #657. Worth knowing that gh-pages deploys from a docs build, so if this reproduces in CI the docs site is affected too — I have not checked whether the CI build hits it, since CI uses a different install path.
All three questions answered — and this issue's premise is wrong
It does not hang. It completes.AppType alone builds in 666s (11m6s), exit 0, build succeeded;
an earlier 240s timeout is what made it look unbounded. On CI the same stall is 6m18.8s. The title
and body should say "stalls ~6-11 minutes", not "hangs indefinitely".
1. It is AppType specifically, not size.apptype.py:133 — reserved_0: 'AppType' = 0, 'reserved', … — makes every one of the 8,182 members an annotated assignment, so AppType.__annotations__ holds 8,183 forward-ref strings that get_type_hints() re-resolves each time autodoc touches the class.
Controls that refute "size alone": TransType (156 members) ~4s; reg.rst minus only AppType ~5s;
a synthetic 8,182-member enum without that annotation shape ~18s. CPU-bound throughout (PID sampled STAT=R, 100% CPU at 21s / 1m58 / 6m40).
2. Neither Python 3.14 nor Sphinx 9.1.0 is the trigger — it is structural.
combo
wall-clock
py3.14.7 + Sphinx 9.1.0
661s
py3.12.14 + Sphinx 9.1.0
554s
py3.14.7 + Sphinx 8.2.3
676s
py3.14.7 + 9.1.0, nosphinx_autodoc_typehints
645s
Core sphinx.ext.autodoc alone reproduces it. Caveat: one run per combo, so 645/668/676 are noise,
not a ranking.
3. CI takes the identical path and succeeds.deploy-pages.yml is the only docs build: python-version: '3.14' (:93), docs/requirements.txt pins Sphinx==9.1.0, exclude_patterns = [] (conf.py:169), no -D flag, no timeout-minutes. Run 36027311485 (9813aa377): job 16:26:00→16:34:10, success,
with a 6m18.8s gap between [31%] pcapkit/const/pcapng and [32%] pcapkit/const/reg. Recent runs are
green; the cancelled ones are concurrency cancellations superseded within 3-4 min. gh-pages is current.
Relevance to #732, and this is my inference rather than measured: the per-transport split does not
reduce the annotation total — it distributes 8,182 across four classes. If the cost is per-class get_type_hints() work it may improve; if it is per-annotation it will not. Worth measuring on that PR
rather than assuming the split fixes this.
Two by-products.conf.py:19 imports the private sphinx_autodoc_typehints._resolver, which
hard-pins the docs build to Sphinx>=9.1 — no release satisfies both that import and Sphinx<9. Untracked;
I'll file it separately if you want. And unexplained: peak RSS 1.9 GB on Sphinx 8.2.3 against ~139 MB
on 9.1.0 for the same work.
Measured: the #732 split makes this worse, not better — 680.3 s → 759.1 s (1.12×)
The open question here was whether splitting AppType into four per-transport classes would reduce the get_type_hints() cost or merely redistribute it. Measured on the split branch (#754): 680.3 s before,
759.1 s after — 11.6% slower. The harness is faithful: its 680.3 s reproduces this issue's 666 s baseline.
So the annotation total is redistributed across four classes and the per-class overhead is additive rather
than amortised. The split is not a mitigation for this issue, and anyone expecting it to be one — I
did — should stop.
list-table rendering is unaffected: 704/366/122/2/1 rows emit correctly, and reST warnings are identical
either side (6 before, 6 after).
That leaves this issue needing a mitigation on its own terms rather than as a by-product. The candidates
from the earlier investigation are unchanged, and none has been tried: an autodoc option that skips
per-member annotation resolution, a hand-written .. py:class:: stub for the generated registries, or
accepting the 6-11 minute stall as the cost of documenting 8,182 annotated members. Note CI already
absorbs it — deploy-pages.yml has no timeout-minutes and the run goes green — so this is build-time
cost, not breakage.
Two numbers in the body need correcting, re-measured on CI.
The 666 s is local. On CI (run 36027311485, success) the whole Install and Build step is
478 s including pip install, and exactly one gap over 20 s exists in the entire build:
So AppType is the sole stall — nothing else in the build costs even 20 s — at 378.8 s, not
666 s. deploy-pages.yml is the only Sphinx build in the repo and carries no timeout-minutes
on any job or step, which is why it absorbs this and goes green.
Mechanism, settled by call count rather than inference. Sphinx 9.1.0's default autodoc calls get_type_hints(parent, ...) once per member (_dynamic/_loader.py:367 and :419), re-resolving
the whole class's annotations every time. Instrumented on a synthetic reproduction at N=2000:
variant
get_type_hints calls
elapsed
default engine
2000
42.0 s
autodoc_typehints='none' alone
2000
43.3 s
legacy engine + autodoc_typehints='none'
0
4.4 s
autodoc_typehints is never consulted on that path, so setting it to none alone is a dead end.
Marginal cost is 10.0 / 38.0 / 157.2 s at N=1000/2000/4000 — exponent ≈1.97, i.e. quadratic.
Recommendation: drop : {self.NAME!r} from pcapkit/vendor/reg/apptype/apptype.py:717 and
regenerate. The annotation is not load-bearing — member sets are identical with it stripped, __annotations__ collapses 13 → 3, mypy reports the same 96 errors in 33 files, and nothing under const/reg/ reads __annotations__. Removing it also makes the four # mypy: disable-error-code=assignment suppressions redundant. Not yet confirmed by a real build,
and the strip test ran at 10-member scale, not 6,147.
The alternative that does fully work — legacy engine plus autodoc_typehints='none' — is rejected on
cost: it suppresses annotations on 3,390 of 3,390 functions/methods site-wide to fix a defect 80%
concentrated in two generated files. Separately, deploy-pages.yml wants a timeout-minutes
regardless of which fix lands; that hole is the more urgent half.
Addendum — a fourth candidate surfaced after the comment above, and it is cheaper than the recommendation. Adding it rather than leaving the ranking looking exhaustive.
Narrow bind_type_checking_names to skip pcapkit.const.reg.apptype.*.docs/source/conf.py:231 walks every pcapkit module on builder-inited and executes its if TYPE_CHECKING: block. That hook is the necessary condition for the stall: it is what makes the per-member forward refs resolvable, and resolving them is the quadratic part. Its own docstring already describes the alternative — without the binding, typing.get_type_hints() raises NameError for the whole class and Sphinx catches it:
A raw dict read instead of 6,147 forward-ref resolutions per class. Zero generated churn, every member kept, one predicate added to one hook.
It is unmeasured — stated plainly rather than folded into the ranking. Nobody has run it.
It still ranks below the generator fix, on merits rather than speed. Narrowing the hook keeps the annotation, which is : 'TCP' on a member of class TCP — tautological — and keeps the four # mypy: disable-error-code=assignment pragmas that annotation is what forces. It also degrades those members' rendered types to unresolved strings, which is the exact class of problem bind_type_checking_names was added to fix. So it treats the symptom in the docs build while leaving the cause in the generated source.
One corroboration of the O(members²) model, from #754 itself. The split changed member counts such that a sum-of-squares model predicts a 1.13x slowdown (66.9M → 75.5M resolutions); measured was 1.12x (680.3s → 759.1s). That is independent of the synthetic curve and lands within 1%, so the growth model is not an artefact of the reproduction.
Correction to my own accounting above. I said no real build had been run. No run touched the real docs/source tree, which is true, but roughly 1,954s (~33 min) of compute did execute against the real conf.py via synthetic single-page sourcedirs using -c — more wall-clock than two full builds. "Budget untouched" was the wrong framing and I am withdrawing it.
Activity
JarryShaw commentedon Sep 24, 2026
All three questions answered — and this issue's premise is wrong
It does not hang. It completes.
AppTypealone builds in 666s (11m6s), exit 0,build succeeded;an earlier 240s timeout is what made it look unbounded. On CI the same stall is 6m18.8s. The title
and body should say "stalls ~6-11 minutes", not "hangs indefinitely".
1. It is
AppTypespecifically, not size.apptype.py:133—reserved_0: 'AppType' = 0, 'reserved', …— makes every one of the 8,182 members an annotated assignment, soAppType.__annotations__holds8,183 forward-ref strings that
get_type_hints()re-resolves each time autodoc touches the class.Controls that refute "size alone":
TransType(156 members) ~4s;reg.rstminus onlyAppType~5s;a synthetic 8,182-member enum without that annotation shape ~18s. CPU-bound throughout (PID sampled
STAT=R, 100% CPU at 21s / 1m58 / 6m40).2. Neither Python 3.14 nor Sphinx 9.1.0 is the trigger — it is structural.
sphinx_autodoc_typehintsCore
sphinx.ext.autodocalone reproduces it. Caveat: one run per combo, so 645/668/676 are noise,not a ranking.
3. CI takes the identical path and succeeds.
deploy-pages.ymlis the only docs build:python-version: '3.14'(:93),docs/requirements.txtpinsSphinx==9.1.0,exclude_patterns = [](conf.py:169), no-Dflag, notimeout-minutes. Run36027311485(9813aa377): job 16:26:00→16:34:10, success,with a 6m18.8s gap between
[31%] pcapkit/const/pcapngand[32%] pcapkit/const/reg. Recent runs aregreen; the
cancelledones are concurrency cancellations superseded within 3-4 min.gh-pagesis current.Relevance to #732, and this is my inference rather than measured: the per-transport split does not
reduce the annotation total — it distributes 8,182 across four classes. If the cost is per-class
get_type_hints()work it may improve; if it is per-annotation it will not. Worth measuring on that PRrather than assuming the split fixes this.
Two by-products.
conf.py:19imports the privatesphinx_autodoc_typehints._resolver, whichhard-pins the docs build to Sphinx>=9.1 — no release satisfies both that import and Sphinx<9. Untracked;
I'll file it separately if you want. And unexplained: peak RSS 1.9 GB on Sphinx 8.2.3 against ~139 MB
on 9.1.0 for the same work.
JarryShaw commentedon Sep 24, 2026
Measured: the #732 split makes this worse, not better — 680.3 s → 759.1 s (1.12×)
The open question here was whether splitting
AppTypeinto four per-transport classes would reduce theget_type_hints()cost or merely redistribute it. Measured on the split branch (#754): 680.3 s before,759.1 s after — 11.6% slower. The harness is faithful: its 680.3 s reproduces this issue's 666 s baseline.
So the annotation total is redistributed across four classes and the per-class overhead is additive rather
than amortised. The split is not a mitigation for this issue, and anyone expecting it to be one — I
did — should stop.
list-tablerendering is unaffected: 704/366/122/2/1 rows emit correctly, and reST warnings are identicaleither side (6 before, 6 after).
That leaves this issue needing a mitigation on its own terms rather than as a by-product. The candidates
from the earlier investigation are unchanged, and none has been tried: an autodoc option that skips
per-member annotation resolution, a hand-written
.. py:class::stub for the generated registries, oraccepting the 6-11 minute stall as the cost of documenting 8,182 annotated members. Note CI already
absorbs it —
deploy-pages.ymlhas notimeout-minutesand the run goes green — so this is build-timecost, not breakage.
docs: qualify the four bare `dict` :type: fields to the builtin
JarryShaw commentedon Sep 25, 2026
Two numbers in the body need correcting, re-measured on CI.
The 666 s is local. On CI (run
36027311485, success) the wholeInstall and Buildstep is478 s including pip install, and exactly one gap over 20 s exists in the entire build:
So
AppTypeis the sole stall — nothing else in the build costs even 20 s — at 378.8 s, not666 s.
deploy-pages.ymlis the only Sphinx build in the repo and carries notimeout-minuteson any job or step, which is why it absorbs this and goes green.
Mechanism, settled by call count rather than inference. Sphinx 9.1.0's default autodoc calls
get_type_hints(parent, ...)once per member (_dynamic/_loader.py:367and:419), re-resolvingthe whole class's annotations every time. Instrumented on a synthetic reproduction at N=2000:
get_type_hintscallsautodoc_typehints='none'aloneautodoc_typehints='none'autodoc_typehintsis never consulted on that path, so setting it tononealone is a dead end.Marginal cost is 10.0 / 38.0 / 157.2 s at N=1000/2000/4000 — exponent ≈1.97, i.e. quadratic.
Recommendation: drop
: {self.NAME!r}frompcapkit/vendor/reg/apptype/apptype.py:717andregenerate. The annotation is not load-bearing — member sets are identical with it stripped,
__annotations__collapses 13 → 3, mypy reports the same 96 errors in 33 files, and nothing underconst/reg/reads__annotations__. Removing it also makes the four# mypy: disable-error-code=assignmentsuppressions redundant. Not yet confirmed by a real build,and the strip test ran at 10-member scale, not 6,147.
The alternative that does fully work — legacy engine plus
autodoc_typehints='none'— is rejected oncost: it suppresses annotations on 3,390 of 3,390 functions/methods site-wide to fix a defect 80%
concentrated in two generated files. Separately,
deploy-pages.ymlwants atimeout-minutesregardless of which fix lands; that hole is the more urgent half.
JarryShaw commentedon Sep 25, 2026
Addendum — a fourth candidate surfaced after the comment above, and it is cheaper than the recommendation. Adding it rather than leaving the ranking looking exhaustive.
Narrow
bind_type_checking_namesto skippcapkit.const.reg.apptype.*.docs/source/conf.py:231walks everypcapkitmodule onbuilder-initedand executes itsif TYPE_CHECKING:block. That hook is the necessary condition for the stall: it is what makes the per-member forward refs resolvable, and resolving them is the quadratic part. Its own docstring already describes the alternative — without the binding,typing.get_type_hints()raisesNameErrorfor the whole class and Sphinx catches it:A raw dict read instead of 6,147 forward-ref resolutions per class. Zero generated churn, every member kept, one predicate added to one hook.
It is unmeasured — stated plainly rather than folded into the ranking. Nobody has run it.
It still ranks below the generator fix, on merits rather than speed. Narrowing the hook keeps the annotation, which is
: 'TCP'on a member of classTCP— tautological — and keeps the four# mypy: disable-error-code=assignmentpragmas that annotation is what forces. It also degrades those members' rendered types to unresolved strings, which is the exact class of problembind_type_checking_nameswas added to fix. So it treats the symptom in the docs build while leaving the cause in the generated source.One corroboration of the O(members²) model, from #754 itself. The split changed member counts such that a sum-of-squares model predicts a 1.13x slowdown (66.9M → 75.5M resolutions); measured was 1.12x (680.3s → 759.1s). That is independent of the synthetic curve and lands within 1%, so the growth model is not an artefact of the reproduction.
Correction to my own accounting above. I said no real build had been run. No run touched the real
docs/sourcetree, which is true, but roughly 1,954s (~33 min) of compute did execute against the realconf.pyvia synthetic single-page sourcedirs using-c— more wall-clock than two full builds. "Budget untouched" was the wrong framing and I am withdrawing it.perf(reg): drop the per-member annotation and TransportProtocol.get()…
test: fix the review findings on the tests prose pass (#1103)