Skip to content

Commit

Permalink
Use microsecond-resolution timestamps for outdated file detection (#1…
Browse files Browse the repository at this point in the history
…1435)

Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
  • Loading branch information
jayaddison and AA-Turner committed Jul 20, 2023
1 parent d6f1090 commit ecc8613
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 14 deletions.
6 changes: 1 addition & 5 deletions sphinx/builders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,11 +496,7 @@ def read_doc(self, docname: str, *, _cache: bool = True) -> None:
doctree = publisher.document

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

# cleanup
self.env.temp_data.clear()
Expand Down
35 changes: 26 additions & 9 deletions sphinx/environment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pickle
from collections import defaultdict
from copy import copy
from datetime import datetime
from datetime import datetime, timezone
from os import path
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator

Expand Down Expand Up @@ -55,7 +55,7 @@

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

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

# docname -> mtime at the time of reading
# docname -> time of reading (in integer microseconds)
# contains all read docnames
self.all_docs: dict[str, float] = {}
self.all_docs: dict[str, int] = {}
# docname -> set of dependent file
# names, relative to documentation root
self.dependencies: dict[str, set[str]] = defaultdict(set)
Expand Down Expand Up @@ -481,12 +481,14 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str],
continue
# check the mtime of the document
mtime = self.all_docs[docname]
newmtime = path.getmtime(self.doc2path(docname))
newmtime = _last_modified_time(self.doc2path(docname))
if newmtime > mtime:
# convert integer microseconds to floating-point seconds,
# and then to timezone-aware datetime objects.
mtime_dt = datetime.fromtimestamp(mtime / 1_000_000, tz=timezone.utc)
newmtime_dt = datetime.fromtimestamp(mtime / 1_000_000, tz=timezone.utc)
logger.debug('[build target] outdated %r: %s -> %s',
docname,
datetime.utcfromtimestamp(mtime),
datetime.utcfromtimestamp(newmtime))
docname, mtime_dt, newmtime_dt)
changed.add(docname)
continue
# finally, check the mtime of dependencies
Expand All @@ -497,7 +499,7 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str],
if not path.isfile(deppath):
changed.add(docname)
break
depmtime = path.getmtime(deppath)
depmtime = _last_modified_time(deppath)
if depmtime > mtime:
changed.add(docname)
break
Expand Down Expand Up @@ -728,3 +730,18 @@ def check_consistency(self) -> None:
for domain in self.domains.values():
domain.check_consistency()
self.events.emit('env-check-consistency', self)


def _last_modified_time(filename: str | os.PathLike[str]) -> int:
"""Return the last modified time of ``filename``.
The time is returned as integer microseconds.
The lowest common denominator of modern file-systems seems to be
microsecond-level precision.
We prefer to err on the side of re-rendering a file,
so we round up to the nearest microsecond.
"""

# upside-down floor division to get the ceiling
return -(os.stat(filename).st_mtime_ns // -1_000)

0 comments on commit ecc8613

Please sign in to comment.