diff options
Diffstat (limited to 'sphinx/ext/duration.py')
-rw-r--r-- | sphinx/ext/duration.py | 92 |
1 files changed, 92 insertions, 0 deletions
diff --git a/sphinx/ext/duration.py b/sphinx/ext/duration.py new file mode 100644 index 0000000..26e197f --- /dev/null +++ b/sphinx/ext/duration.py @@ -0,0 +1,92 @@ +"""Measure document reading durations.""" + +from __future__ import annotations + +import time +from itertools import islice +from operator import itemgetter +from typing import TYPE_CHECKING, cast + +import sphinx +from sphinx.domains import Domain +from sphinx.locale import __ +from sphinx.util import logging + +if TYPE_CHECKING: + from docutils import nodes + + from sphinx.application import Sphinx + +logger = logging.getLogger(__name__) + + +class DurationDomain(Domain): + """A domain for durations of Sphinx processing.""" + name = 'duration' + + @property + def reading_durations(self) -> dict[str, float]: + return self.data.setdefault('reading_durations', {}) + + def note_reading_duration(self, duration: float) -> None: + self.reading_durations[self.env.docname] = duration + + def clear(self) -> None: + self.reading_durations.clear() + + def clear_doc(self, docname: str) -> None: + self.reading_durations.pop(docname, None) + + def merge_domaindata(self, docnames: list[str], otherdata: dict[str, float]) -> None: + for docname, duration in otherdata.items(): + if docname in docnames: + self.reading_durations[docname] = duration + + +def on_builder_inited(app: Sphinx) -> None: + """Initialize DurationDomain on bootstrap. + + This clears the results of the last build. + """ + domain = cast(DurationDomain, app.env.get_domain('duration')) + domain.clear() + + +def on_source_read(app: Sphinx, docname: str, content: list[str]) -> None: + """Start to measure reading duration.""" + app.env.temp_data['started_at'] = time.monotonic() + + +def on_doctree_read(app: Sphinx, doctree: nodes.document) -> None: + """Record a reading duration.""" + started_at = app.env.temp_data['started_at'] + duration = time.monotonic() - started_at + domain = cast(DurationDomain, app.env.get_domain('duration')) + domain.note_reading_duration(duration) + + +def on_build_finished(app: Sphinx, error: Exception) -> None: + """Display duration ranking on the current build.""" + domain = cast(DurationDomain, app.env.get_domain('duration')) + if not domain.reading_durations: + return + durations = sorted(domain.reading_durations.items(), key=itemgetter(1), reverse=True) + + logger.info('') + logger.info(__('====================== slowest reading durations =======================')) + for docname, d in islice(durations, 5): + logger.info(f'{d:.3f} {docname}') # NoQA: G004 + + +def setup(app: Sphinx) -> dict[str, bool | str]: + app.add_domain(DurationDomain) + app.connect('builder-inited', on_builder_inited) + app.connect('source-read', on_source_read) + app.connect('doctree-read', on_doctree_read) + app.connect('build-finished', on_build_finished) + + return { + 'version': sphinx.__display_version__, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } |