Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
120dd48
Add n_durations config option
user27182 Apr 5, 2025
235b52d
Add write_durations option
user27182 Apr 5, 2025
a5a73b6
Add options and update docs
user27182 Apr 6, 2025
221a25c
Update changes.rst
user27182 Apr 6, 2025
72bfb24
Fix ruff errors
user27182 Apr 6, 2025
23294e8
Fix ruff errors
user27182 Apr 6, 2025
c85a9e2
Fix lint errors
user27182 Apr 6, 2025
84d7fc1
Ruff formatting
user27182 Apr 6, 2025
96f5a65
noqa timezone
user27182 Apr 6, 2025
ba8cbb8
Add tests
user27182 Apr 6, 2025
42d53ac
Add tests
user27182 Apr 6, 2025
04bf8b8
Modify testroots
user27182 Apr 6, 2025
920bbc4
Rename write_durations -> write_json
user27182 Apr 10, 2025
d1bf297
Update n_slowest tests
user27182 Apr 10, 2025
6c5e824
Make options top-level
user27182 Apr 10, 2025
e4b27b8
Update docs and formatting
user27182 Apr 11, 2025
8d754a1
Add option to specify json path
user27182 Apr 11, 2025
25675a5
Fix internal default
user27182 Apr 11, 2025
7cbd32e
Change test root
user27182 Apr 11, 2025
8b90a4e
Merge branch 'master' into feat/durations_options
user27182 Apr 11, 2025
346c729
Fix typing
user27182 Apr 11, 2025
0bf3216
Change test root
user27182 Apr 11, 2025
a7d4ec5
Fix typing
user27182 Apr 11, 2025
9de5011
Fix typing
user27182 Apr 11, 2025
1631065
Change test root
user27182 Apr 11, 2025
1965046
Update docs
user27182 Apr 11, 2025
2131604
Fix ref
user27182 Apr 11, 2025
5cb3637
Add extensions cross-ref
user27182 Apr 11, 2025
61ab2ec
Update docs
user27182 Apr 11, 2025
e1b9f77
Fix line too long
user27182 Apr 11, 2025
85ff9d5
Add duration_limit config option
user27182 Apr 13, 2025
2ec2aa8
Fix typing
user27182 Apr 13, 2025
2dbc395
Use freshenv
user27182 Apr 13, 2025
df33ed3
Version added
user27182 Apr 13, 2025
fa61e37
Merge branch 'master' into feat/durations_options
user27182 May 12, 2025
22ce8e9
Merge main
user27182 Jul 2, 2025
9890296
Merge master
user27182 Sep 29, 2025
b5141d6
Merge branch 'master' into feat/durations_options
user27182 Oct 17, 2025
8627d9c
Merge branch 'master' into feat/durations_options
user27182 Oct 20, 2025
f7a4b99
Merge branch 'master' into feat/durations_options
user27182 Nov 3, 2025
16af372
Merge branch 'master' into feat/durations_options
user27182 Nov 12, 2025
3aca006
Merge branch 'master' into feat/durations_options
user27182 Nov 25, 2025
813f2c7
Merge branch 'master' into feat/durations_options
AA-Turner Nov 26, 2025
5e64fa7
Several fixes
AA-Turner Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Contributors
* Eric Larson -- better error messages
* Eric N. Vander Weele -- autodoc improvements
* Eric Wieser -- autodoc improvements
* Erik Bedard -- config options for :mod:`sphinx.ext.duration`
* Etienne Desautels -- apidoc module
* Ezio Melotti -- collapsible sidebar JavaScript
* Filip Vavera -- napoleon todo directive
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ Features added
Patch by Fazeel Usmani and James Addison.
* #14075: autosummary: Provide more context in import exception stack traces.
Patch by Philipp A.
* #13468: Add config options to :mod:`sphinx.ext.duration`.
Patch by Erik Bedard and Adam Turner.

Bugs fixed
----------
Expand Down
4 changes: 4 additions & 0 deletions doc/usage/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,7 @@ Options for warning control
* ``autosectionlabel.<document name>``
* ``autosummary``
* ``autosummary.import_cycle``
* ``duration``
* ``intersphinx.external``

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

.. versionadded:: 9.0
``duration``.


Builder options
===============
Expand Down
87 changes: 84 additions & 3 deletions doc/usage/extensions/duration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,87 @@

.. versionadded:: 2.4

This extension measures durations of Sphinx processing and show its
result at end of the build. It is useful for inspecting what document
is slowly built.
This extension measures durations of Sphinx processing when reading
documents and is useful for inspecting what document is slowly built.
Durations are printed to console at the end of the build and saved
to a JSON file in the output directory by default.

Enable this extension by adding ``'sphinx.ext.duration'`` to
the :confval:`extensions` list in your :file:`conf.py`:

.. code-block:: python

extensions = [
...
'sphinx.ext.duration',
]


Configuration
=============

.. confval:: duration_print_total
:type: :code-py:`bool`
:default: :code-py:`True`

Show the total reading duration in the build summary, e.g.:

.. code-block:: text

====================== total reading duration ==========================
Total time reading 31 files: 0m 3.142s

.. versionadded:: 9.0

.. confval:: duration_print_slowest
:type: :code-py:`bool`
:default: :code-py:`True`

Show the slowest durations in the build summary.
The durations are sorted in order from slowest to fastest.
This prints up to :confval:`duration_n_slowest` durations, e.g.:

.. code-block:: text

====================== slowest 5 reading durations =======================
0.012s spam
0.011s ham
0.011s eggs
0.006s lobster
0.005s beans

.. versionadded:: 9.0

.. confval:: duration_n_slowest
:type: :code-py:`int`
:default: :code-py:`5`

Maximum number of slowest durations to show in the build summary
when :confval:`duration_print_slowest` is enabled.
By default, only the ``5`` slowest durations are shown.
Set this to ``0`` to show all durations.

.. versionadded:: 9.0

.. confval:: duration_write_json
:type: :code-py:`str | None`
:default: :code-py:`'sphinx-reading-durations.json'`

Write all reading durations to a JSON file in the output directory
The file contents are a map of the document names to reading durations,
where document names are strings and durations are floats in seconds.
Set this value to an empty string or ``None`` to disable writing the file,
or set it to a relative path to customize it.

This may be useful for testing and setting a limit on reading times.

.. versionadded:: 9.0

.. confval:: duration_limit
:type: :code-py:`float | int | None`
:default: :code-py:`None`

Set a duration limit (in seconds) for reading a document.
If any duration exceeds this value, a warning is emitted.

.. versionadded:: 9.0
78 changes: 73 additions & 5 deletions sphinx/ext/duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from __future__ import annotations

import json
import time
from itertools import islice
from operator import itemgetter
from types import NoneType
from typing import TYPE_CHECKING

import sphinx
Expand All @@ -13,7 +15,8 @@
from sphinx.util import logging

if TYPE_CHECKING:
from collections.abc import Set
from collections.abc import Collection, Set
from pathlib import Path
from typing import TypedDict

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

def warn_reading_duration(self, duration: float, duration_limit: float) -> None:
logger.warning(
__('Reading duration %.3fs exceeded the duration limit %.3fs'),
duration,
duration_limit,
type='duration',
location=self.env.docname,
)

def clear(self) -> None:
self.reading_durations.clear()

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

duration_limit: float | None = app.config.duration_limit
if duration_limit is not None and duration > duration_limit:
domain.warn_reading_duration(duration, duration_limit)


def on_build_finished(app: Sphinx, error: Exception) -> None:
"""Display duration ranking on the current build."""
domain = app.env.domains['duration']
if not domain.reading_durations:
return
durations = sorted(
domain.reading_durations.items(), key=itemgetter(1), reverse=True

# Get default options and update with user-specified values
if app.config.duration_print_total:
_print_total_duration(domain.reading_durations.values())

if app.config.duration_print_slowest:
_print_slowest_durations(
domain.reading_durations, app.config.duration_n_slowest
)

if write_json := app.config.duration_write_json:
_write_json_durations(domain.reading_durations, app.outdir / write_json)


def _print_total_duration(durations: Collection[float]) -> None:
logger.info('')
logger.info(
__('====================== total reading duration ==========================')
)

n_files = len(durations)
s = 's' if n_files != 1 else ''
minutes, seconds = divmod(sum(durations), 60)
logger.info(
__('Total time reading %d file%s: %dm %.3fs'), n_files, s, minutes, seconds
)


def _print_slowest_durations(durations: dict[str, float], n_slowest: int) -> None:
sorted_durations = sorted(durations.items(), key=itemgetter(1), reverse=True)
n_slowest = n_slowest or len(sorted_durations)
n_slowest = min(n_slowest, len(sorted_durations))

logger.info('')
logger.info('')
logger.info(
__('====================== slowest reading durations =======================')
)
for docname, d in islice(durations, 5):
logger.info(f'{d:.3f} {docname}') # NoQA: G004
for docname, duration in islice(sorted_durations, n_slowest):
logger.info(__('%.3fs %s'), duration, docname)

logger.info('')


def _write_json_durations(durations: dict[str, float], out_file: Path) -> None:
durations = {k: round(v, 3) for k, v in durations.items()}
out_file.parent.mkdir(parents=True, exist_ok=True)
durations_json = json.dumps(durations, ensure_ascii=False, indent=4, sort_keys=True)
out_file.write_text(durations_json, encoding='utf-8')


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

app.add_config_value('duration_print_total', True, '', types=frozenset({bool}))
app.add_config_value('duration_print_slowest', True, '', types=frozenset({bool}))
app.add_config_value('duration_n_slowest', 5, '', types=frozenset({int}))
app.add_config_value(
'duration_write_json',
'sphinx-reading-durations.json',
'',
types=frozenset({str, NoneType}),
)
app.add_config_value(
'duration_limit', None, '', types=frozenset({float, int, NoneType})
)

return {
'version': sphinx.__display_version__,
'parallel_read_safe': True,
Expand Down
Loading
Loading