Skip to content

Commit ecc8613

Browse files
Use microsecond-resolution timestamps for outdated file detection (#11435)
Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
1 parent d6f1090 commit ecc8613

File tree

2 files changed

+27
-14
lines changed

2 files changed

+27
-14
lines changed

sphinx/builders/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -496,11 +496,7 @@ def read_doc(self, docname: str, *, _cache: bool = True) -> None:
496496
doctree = publisher.document
497497

498498
# store time of reading, for outdated files detection
499-
# (Some filesystems have coarse timestamp resolution;
500-
# therefore time.time() can be older than filesystem's timestamp.
501-
# For example, FAT32 has 2sec timestamp resolution.)
502-
self.env.all_docs[docname] = max(time.time(),
503-
path.getmtime(self.env.doc2path(docname)))
499+
self.env.all_docs[docname] = time.time_ns() // 1_000
504500

505501
# cleanup
506502
self.env.temp_data.clear()

sphinx/environment/__init__.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pickle
88
from collections import defaultdict
99
from copy import copy
10-
from datetime import datetime
10+
from datetime import datetime, timezone
1111
from os import path
1212
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator
1313

@@ -55,7 +55,7 @@
5555

5656
# This is increased every time an environment attribute is added
5757
# or changed to properly invalidate pickle files.
58-
ENV_VERSION = 57
58+
ENV_VERSION = 58
5959

6060
# config status
6161
CONFIG_OK = 1
@@ -166,9 +166,9 @@ def __init__(self, app: Sphinx):
166166
# All "docnames" here are /-separated and relative and exclude
167167
# the source suffix.
168168

169-
# docname -> mtime at the time of reading
169+
# docname -> time of reading (in integer microseconds)
170170
# contains all read docnames
171-
self.all_docs: dict[str, float] = {}
171+
self.all_docs: dict[str, int] = {}
172172
# docname -> set of dependent file
173173
# names, relative to documentation root
174174
self.dependencies: dict[str, set[str]] = defaultdict(set)
@@ -481,12 +481,14 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str],
481481
continue
482482
# check the mtime of the document
483483
mtime = self.all_docs[docname]
484-
newmtime = path.getmtime(self.doc2path(docname))
484+
newmtime = _last_modified_time(self.doc2path(docname))
485485
if newmtime > mtime:
486+
# convert integer microseconds to floating-point seconds,
487+
# and then to timezone-aware datetime objects.
488+
mtime_dt = datetime.fromtimestamp(mtime / 1_000_000, tz=timezone.utc)
489+
newmtime_dt = datetime.fromtimestamp(mtime / 1_000_000, tz=timezone.utc)
486490
logger.debug('[build target] outdated %r: %s -> %s',
487-
docname,
488-
datetime.utcfromtimestamp(mtime),
489-
datetime.utcfromtimestamp(newmtime))
491+
docname, mtime_dt, newmtime_dt)
490492
changed.add(docname)
491493
continue
492494
# finally, check the mtime of dependencies
@@ -497,7 +499,7 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str],
497499
if not path.isfile(deppath):
498500
changed.add(docname)
499501
break
500-
depmtime = path.getmtime(deppath)
502+
depmtime = _last_modified_time(deppath)
501503
if depmtime > mtime:
502504
changed.add(docname)
503505
break
@@ -728,3 +730,18 @@ def check_consistency(self) -> None:
728730
for domain in self.domains.values():
729731
domain.check_consistency()
730732
self.events.emit('env-check-consistency', self)
733+
734+
735+
def _last_modified_time(filename: str | os.PathLike[str]) -> int:
736+
"""Return the last modified time of ``filename``.
737+
738+
The time is returned as integer microseconds.
739+
The lowest common denominator of modern file-systems seems to be
740+
microsecond-level precision.
741+
742+
We prefer to err on the side of re-rendering a file,
743+
so we round up to the nearest microsecond.
744+
"""
745+
746+
# upside-down floor division to get the ceiling
747+
return -(os.stat(filename).st_mtime_ns // -1_000)

0 commit comments

Comments
 (0)