Skip to content

Commit 187a2d2

Browse files
user27182AA-Turner
andauthored
Add configuration options to the duration extension (#13469)
Co-authored-by: Adam Turner <[email protected]>
1 parent 828bdca commit 187a2d2

File tree

6 files changed

+305
-10
lines changed

6 files changed

+305
-10
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Contributors
5353
* Eric Larson -- better error messages
5454
* Eric N. Vander Weele -- autodoc improvements
5555
* Eric Wieser -- autodoc improvements
56+
* Erik Bedard -- config options for :mod:`sphinx.ext.duration`
5657
* Etienne Desautels -- apidoc module
5758
* Ezio Melotti -- collapsible sidebar JavaScript
5859
* Filip Vavera -- napoleon todo directive

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ Features added
8484
Patch by Fazeel Usmani and James Addison.
8585
* #14075: autosummary: Provide more context in import exception stack traces.
8686
Patch by Philipp A.
87+
* #13468: Add config options to :mod:`sphinx.ext.duration`.
88+
Patch by Erik Bedard and Adam Turner.
8789

8890
Bugs fixed
8991
----------

doc/usage/configuration.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,7 @@ Options for warning control
14201420
* ``autosectionlabel.<document name>``
14211421
* ``autosummary``
14221422
* ``autosummary.import_cycle``
1423+
* ``duration``
14231424
* ``intersphinx.external``
14241425

14251426
You can choose from these types. You can also give only the first
@@ -1485,6 +1486,9 @@ Options for warning control
14851486
``ref.any``,
14861487
``toc.duplicate_entry``, ``toc.empty_glob``, and ``toc.not_included``.
14871488

1489+
.. versionadded:: 9.0
1490+
``duration``.
1491+
14881492

14891493
Builder options
14901494
===============

doc/usage/extensions/duration.rst

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,87 @@
66

77
.. versionadded:: 2.4
88

9-
This extension measures durations of Sphinx processing and show its
10-
result at end of the build. It is useful for inspecting what document
11-
is slowly built.
9+
This extension measures durations of Sphinx processing when reading
10+
documents and is useful for inspecting what document is slowly built.
11+
Durations are printed to console at the end of the build and saved
12+
to a JSON file in the output directory by default.
13+
14+
Enable this extension by adding ``'sphinx.ext.duration'`` to
15+
the :confval:`extensions` list in your :file:`conf.py`:
16+
17+
.. code-block:: python
18+
19+
extensions = [
20+
...
21+
'sphinx.ext.duration',
22+
]
23+
24+
25+
Configuration
26+
=============
27+
28+
.. confval:: duration_print_total
29+
:type: :code-py:`bool`
30+
:default: :code-py:`True`
31+
32+
Show the total reading duration in the build summary, e.g.:
33+
34+
.. code-block:: text
35+
36+
====================== total reading duration ==========================
37+
Total time reading 31 files: 0m 3.142s
38+
39+
.. versionadded:: 9.0
40+
41+
.. confval:: duration_print_slowest
42+
:type: :code-py:`bool`
43+
:default: :code-py:`True`
44+
45+
Show the slowest durations in the build summary.
46+
The durations are sorted in order from slowest to fastest.
47+
This prints up to :confval:`duration_n_slowest` durations, e.g.:
48+
49+
.. code-block:: text
50+
51+
====================== slowest 5 reading durations =======================
52+
0.012s spam
53+
0.011s ham
54+
0.011s eggs
55+
0.006s lobster
56+
0.005s beans
57+
58+
.. versionadded:: 9.0
59+
60+
.. confval:: duration_n_slowest
61+
:type: :code-py:`int`
62+
:default: :code-py:`5`
63+
64+
Maximum number of slowest durations to show in the build summary
65+
when :confval:`duration_print_slowest` is enabled.
66+
By default, only the ``5`` slowest durations are shown.
67+
Set this to ``0`` to show all durations.
68+
69+
.. versionadded:: 9.0
70+
71+
.. confval:: duration_write_json
72+
:type: :code-py:`str | None`
73+
:default: :code-py:`'sphinx-reading-durations.json'`
74+
75+
Write all reading durations to a JSON file in the output directory
76+
The file contents are a map of the document names to reading durations,
77+
where document names are strings and durations are floats in seconds.
78+
Set this value to an empty string or ``None`` to disable writing the file,
79+
or set it to a relative path to customize it.
80+
81+
This may be useful for testing and setting a limit on reading times.
82+
83+
.. versionadded:: 9.0
84+
85+
.. confval:: duration_limit
86+
:type: :code-py:`float | int | None`
87+
:default: :code-py:`None`
88+
89+
Set a duration limit (in seconds) for reading a document.
90+
If any duration exceeds this value, a warning is emitted.
91+
92+
.. versionadded:: 9.0

sphinx/ext/duration.py

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
from __future__ import annotations
44

5+
import json
56
import time
67
from itertools import islice
78
from operator import itemgetter
9+
from types import NoneType
810
from typing import TYPE_CHECKING
911

1012
import sphinx
@@ -13,7 +15,8 @@
1315
from sphinx.util import logging
1416

1517
if TYPE_CHECKING:
16-
from collections.abc import Set
18+
from collections.abc import Collection, Set
19+
from pathlib import Path
1720
from typing import TypedDict
1821

1922
from docutils import nodes
@@ -39,6 +42,15 @@ def reading_durations(self) -> dict[str, float]:
3942
def note_reading_duration(self, duration: float) -> None:
4043
self.reading_durations[self.env.current_document.docname] = duration
4144

45+
def warn_reading_duration(self, duration: float, duration_limit: float) -> None:
46+
logger.warning(
47+
__('Reading duration %.3fs exceeded the duration limit %.3fs'),
48+
duration,
49+
duration_limit,
50+
type='duration',
51+
location=self.env.docname,
52+
)
53+
4254
def clear(self) -> None:
4355
self.reading_durations.clear()
4456

@@ -75,22 +87,65 @@ def on_doctree_read(app: Sphinx, doctree: nodes.document) -> None:
7587
domain = app.env.domains['duration']
7688
domain.note_reading_duration(duration)
7789

90+
duration_limit: float | None = app.config.duration_limit
91+
if duration_limit is not None and duration > duration_limit:
92+
domain.warn_reading_duration(duration, duration_limit)
93+
7894

7995
def on_build_finished(app: Sphinx, error: Exception) -> None:
8096
"""Display duration ranking on the current build."""
8197
domain = app.env.domains['duration']
8298
if not domain.reading_durations:
8399
return
84-
durations = sorted(
85-
domain.reading_durations.items(), key=itemgetter(1), reverse=True
100+
101+
# Get default options and update with user-specified values
102+
if app.config.duration_print_total:
103+
_print_total_duration(domain.reading_durations.values())
104+
105+
if app.config.duration_print_slowest:
106+
_print_slowest_durations(
107+
domain.reading_durations, app.config.duration_n_slowest
108+
)
109+
110+
if write_json := app.config.duration_write_json:
111+
_write_json_durations(domain.reading_durations, app.outdir / write_json)
112+
113+
114+
def _print_total_duration(durations: Collection[float]) -> None:
115+
logger.info('')
116+
logger.info(
117+
__('====================== total reading duration ==========================')
118+
)
119+
120+
n_files = len(durations)
121+
s = 's' if n_files != 1 else ''
122+
minutes, seconds = divmod(sum(durations), 60)
123+
logger.info(
124+
__('Total time reading %d file%s: %dm %.3fs'), n_files, s, minutes, seconds
86125
)
87126

127+
128+
def _print_slowest_durations(durations: dict[str, float], n_slowest: int) -> None:
129+
sorted_durations = sorted(durations.items(), key=itemgetter(1), reverse=True)
130+
n_slowest = n_slowest or len(sorted_durations)
131+
n_slowest = min(n_slowest, len(sorted_durations))
132+
133+
logger.info('')
88134
logger.info('')
89135
logger.info(
90136
__('====================== slowest reading durations =======================')
91137
)
92-
for docname, d in islice(durations, 5):
93-
logger.info(f'{d:.3f} {docname}') # NoQA: G004
138+
for docname, duration in islice(sorted_durations, n_slowest):
139+
logger.info(__('%.3fs %s'), duration, docname)
140+
141+
logger.info('')
142+
143+
144+
def _write_json_durations(durations: dict[str, float], out_file: Path) -> None:
145+
durations = {k: round(v, 3) for k, v in durations.items()}
146+
out_file.parent.mkdir(parents=True, exist_ok=True)
147+
durations_json = json.dumps(durations, ensure_ascii=False, indent=4, sort_keys=True)
148+
out_file.write_text(durations_json, encoding='utf-8')
94149

95150

96151
def setup(app: Sphinx) -> dict[str, bool | str]:
@@ -100,6 +155,19 @@ def setup(app: Sphinx) -> dict[str, bool | str]:
100155
app.connect('doctree-read', on_doctree_read)
101156
app.connect('build-finished', on_build_finished)
102157

158+
app.add_config_value('duration_print_total', True, '', types=frozenset({bool}))
159+
app.add_config_value('duration_print_slowest', True, '', types=frozenset({bool}))
160+
app.add_config_value('duration_n_slowest', 5, '', types=frozenset({int}))
161+
app.add_config_value(
162+
'duration_write_json',
163+
'sphinx-reading-durations.json',
164+
'',
165+
types=frozenset({str, NoneType}),
166+
)
167+
app.add_config_value(
168+
'duration_limit', None, '', types=frozenset({float, int, NoneType})
169+
)
170+
103171
return {
104172
'version': sphinx.__display_version__,
105173
'parallel_read_safe': True,

0 commit comments

Comments
 (0)