Use microsecond-resolution timestamps for outdated file detection (#11435)

Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
James Addison 2023-07-20 20:06:53 +01:00 committed by GitHub
parent d6f10904a3
commit ecc8613fc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 27 additions and 14 deletions

View File

@ -496,11 +496,7 @@ class Builder:
doctree = publisher.document doctree = publisher.document
# store time of reading, for outdated files detection # store time of reading, for outdated files detection
# (Some filesystems have coarse timestamp resolution; self.env.all_docs[docname] = time.time_ns() // 1_000
# 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)))
# cleanup # cleanup
self.env.temp_data.clear() self.env.temp_data.clear()

View File

@ -7,7 +7,7 @@ import os
import pickle import pickle
from collections import defaultdict from collections import defaultdict
from copy import copy from copy import copy
from datetime import datetime from datetime import datetime, timezone
from os import path from os import path
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator
@ -55,7 +55,7 @@ default_settings: dict[str, Any] = {
# This is increased every time an environment attribute is added # This is increased every time an environment attribute is added
# or changed to properly invalidate pickle files. # or changed to properly invalidate pickle files.
ENV_VERSION = 57 ENV_VERSION = 58
# config status # config status
CONFIG_OK = 1 CONFIG_OK = 1
@ -166,9 +166,9 @@ class BuildEnvironment:
# All "docnames" here are /-separated and relative and exclude # All "docnames" here are /-separated and relative and exclude
# the source suffix. # the source suffix.
# docname -> mtime at the time of reading # docname -> time of reading (in integer microseconds)
# contains all read docnames # contains all read docnames
self.all_docs: dict[str, float] = {} self.all_docs: dict[str, int] = {}
# docname -> set of dependent file # docname -> set of dependent file
# names, relative to documentation root # names, relative to documentation root
self.dependencies: dict[str, set[str]] = defaultdict(set) self.dependencies: dict[str, set[str]] = defaultdict(set)
@ -481,12 +481,14 @@ class BuildEnvironment:
continue continue
# check the mtime of the document # check the mtime of the document
mtime = self.all_docs[docname] mtime = self.all_docs[docname]
newmtime = path.getmtime(self.doc2path(docname)) newmtime = _last_modified_time(self.doc2path(docname))
if newmtime > mtime: 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', logger.debug('[build target] outdated %r: %s -> %s',
docname, docname, mtime_dt, newmtime_dt)
datetime.utcfromtimestamp(mtime),
datetime.utcfromtimestamp(newmtime))
changed.add(docname) changed.add(docname)
continue continue
# finally, check the mtime of dependencies # finally, check the mtime of dependencies
@ -497,7 +499,7 @@ class BuildEnvironment:
if not path.isfile(deppath): if not path.isfile(deppath):
changed.add(docname) changed.add(docname)
break break
depmtime = path.getmtime(deppath) depmtime = _last_modified_time(deppath)
if depmtime > mtime: if depmtime > mtime:
changed.add(docname) changed.add(docname)
break break
@ -728,3 +730,18 @@ class BuildEnvironment:
for domain in self.domains.values(): for domain in self.domains.values():
domain.check_consistency() domain.check_consistency()
self.events.emit('env-check-consistency', self) 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)