22
33from __future__ import annotations
44
5+ import json
56import time
67from itertools import islice
78from operator import itemgetter
9+ from types import NoneType
810from typing import TYPE_CHECKING
911
1012import sphinx
1315from sphinx .util import logging
1416
1517if 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
7995def 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
96151def 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