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
# 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()

View File

@ -7,7 +7,7 @@ import os
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
@ -55,7 +55,7 @@ default_settings: dict[str, Any] = {
# 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
@ -166,9 +166,9 @@ class BuildEnvironment:
# 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)
@ -481,12 +481,14 @@ class BuildEnvironment:
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
@ -497,7 +499,7 @@ class BuildEnvironment:
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
@ -728,3 +730,18 @@ class BuildEnvironment:
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)