Patchwork D11684: dirstate: Store mtimes with nanosecond precision in memory

login
register
mail settings
Submitter phabricator
Date Oct. 18, 2021, 2:45 p.m.
Message ID <differential-rev-PHID-DREV-t3zeze7higorkz3f6w7y-req@mercurial-scm.org>
Download mbox | patch
Permalink /patch/50008/
State Superseded
Headers show

Comments

phabricator - Oct. 18, 2021, 2:45 p.m.
SimonSapin created this revision.
Herald added a reviewer: hg-reviewers.
Herald added a subscriber: mercurial-patches.

REVISION SUMMARY
  Keep integer seconds since the Unix epoch,
  together with integer nanoseconds in the `0 <= n < 1e9` range.
  
  For now, nanoseconds are still always zero.
  This commit is about data structure changes.

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D11684

AFFECTED FILES
  mercurial/cext/parsers.c
  mercurial/cext/util.h
  mercurial/dirstate.py
  mercurial/dirstatemap.py
  mercurial/dirstateutils/timestamp.py
  mercurial/dirstateutils/v2.py
  mercurial/merge.py
  mercurial/pure/parsers.py
  rust/hg-core/src/dirstate/entry.rs
  rust/hg-core/src/dirstate/parsers.rs
  rust/hg-core/src/dirstate/status.rs
  rust/hg-core/src/dirstate_tree/dirstate_map.rs
  rust/hg-core/src/dirstate_tree/on_disk.rs
  rust/hg-core/src/dirstate_tree/status.rs
  rust/hg-cpython/src/dirstate.rs
  rust/hg-cpython/src/dirstate/dirstate_map.rs
  rust/hg-cpython/src/dirstate/item.rs
  rust/hg-cpython/src/dirstate/status.rs
  rust/rhg/src/commands/status.rs
  tests/fakedirstatewritetime.py

CHANGE DETAILS




To: SimonSapin, #hg-reviewers
Cc: mercurial-patches, mercurial-devel

Patch

diff --git a/tests/fakedirstatewritetime.py b/tests/fakedirstatewritetime.py
--- a/tests/fakedirstatewritetime.py
+++ b/tests/fakedirstatewritetime.py
@@ -15,6 +15,7 @@ 
     policy,
     registrar,
 )
+from mercurial.dirstateutils import timestamp
 from mercurial.utils import dateutil
 
 try:
@@ -40,9 +41,8 @@ 
 def pack_dirstate(fakenow, orig, dmap, copymap, pl, now):
     # execute what original parsers.pack_dirstate should do actually
     # for consistency
-    actualnow = int(now)
     for f, e in dmap.items():
-        if e.need_delay(actualnow):
+        if e.need_delay(now):
             e.set_possibly_dirty()
 
     return orig(dmap, copymap, pl, fakenow)
@@ -62,6 +62,7 @@ 
     # parsing 'fakenow' in YYYYmmddHHMM format makes comparison between
     # 'fakenow' value and 'touch -t YYYYmmddHHMM' argument easy
     fakenow = dateutil.parsedate(fakenow, [b'%Y%m%d%H%M'])[0]
+    fakenow = timestamp.timestamp((fakenow, 0))
 
     if has_rust_dirstate:
         # The Rust implementation does not use public parse/pack dirstate
diff --git a/rust/rhg/src/commands/status.rs b/rust/rhg/src/commands/status.rs
--- a/rust/rhg/src/commands/status.rs
+++ b/rust/rhg/src/commands/status.rs
@@ -11,6 +11,7 @@ 
 use clap::{Arg, SubCommand};
 use hg;
 use hg::config::Config;
+use hg::dirstate::TruncatedTimestamp;
 use hg::errors::HgError;
 use hg::manifest::Manifest;
 use hg::matchers::AlwaysMatcher;
@@ -180,7 +181,7 @@ 
         // hence be stored on dmap. Using a value that assumes we aren't
         // below the time resolution granularity of the FS and the
         // dirstate.
-        last_normal_time: 0,
+        last_normal_time: TruncatedTimestamp::new_truncate(0, 0),
         // we're currently supporting file systems with exec flags only
         // anyway
         check_exec: true,
diff --git a/rust/hg-cpython/src/dirstate/status.rs b/rust/hg-cpython/src/dirstate/status.rs
--- a/rust/hg-cpython/src/dirstate/status.rs
+++ b/rust/hg-cpython/src/dirstate/status.rs
@@ -9,6 +9,7 @@ 
 //! `hg-core` crate. From Python, this will be seen as
 //! `rustext.dirstate.status`.
 
+use crate::dirstate::item::timestamp;
 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
 use cpython::exc::OSError;
 use cpython::{
@@ -102,12 +103,13 @@ 
     root_dir: PyObject,
     ignore_files: PyList,
     check_exec: bool,
-    last_normal_time: i64,
+    last_normal_time: (u32, u32),
     list_clean: bool,
     list_ignored: bool,
     list_unknown: bool,
     collect_traversed_dirs: bool,
 ) -> PyResult<PyTuple> {
+    let last_normal_time = timestamp(py, last_normal_time)?;
     let bytes = root_dir.extract::<PyBytes>(py)?;
     let root_dir = get_path_from_bytes(bytes.data(py));
 
diff --git a/rust/hg-cpython/src/dirstate/item.rs b/rust/hg-cpython/src/dirstate/item.rs
--- a/rust/hg-cpython/src/dirstate/item.rs
+++ b/rust/hg-cpython/src/dirstate/item.rs
@@ -8,6 +8,7 @@ 
 use cpython::PythonObject;
 use hg::dirstate::DirstateEntry;
 use hg::dirstate::EntryState;
+use hg::dirstate::TruncatedTimestamp;
 use std::cell::Cell;
 use std::convert::TryFrom;
 
@@ -21,7 +22,7 @@ 
         p2_info: bool = false,
         has_meaningful_data: bool = true,
         has_meaningful_mtime: bool = true,
-        parentfiledata: Option<(u32, u32, u32)> = None,
+        parentfiledata: Option<(u32, u32, (u32, u32))> = None,
 
     ) -> PyResult<DirstateItem> {
         let mut mode_size_opt = None;
@@ -31,7 +32,7 @@ 
                 mode_size_opt = Some((mode, size))
             }
             if has_meaningful_mtime {
-                mtime_opt = Some(mtime)
+                mtime_opt = Some(timestamp(py, mtime)?)
             }
         }
         let entry = DirstateEntry::from_v2_data(
@@ -118,10 +119,19 @@ 
         Ok(mtime)
     }
 
-    def need_delay(&self, now: i32) -> PyResult<bool> {
+    def need_delay(&self, now: (u32, u32)) -> PyResult<bool> {
+        let now = timestamp(py, now)?;
         Ok(self.entry(py).get().mtime_is_ambiguous(now))
     }
 
+    def mtime_likely_equal_to(&self, other: (u32, u32)) -> PyResult<bool> {
+        if let Some(mtime) = self.entry(py).get().truncated_mtime() {
+            Ok(mtime.very_likely_equal(timestamp(py, other)?))
+        } else {
+            Ok(false)
+        }
+    }
+
     @classmethod
     def from_v1_data(
         _cls,
@@ -147,8 +157,9 @@ 
         &self,
         mode: u32,
         size: u32,
-        mtime: u32,
+        mtime: (u32, u32),
     ) -> PyResult<PyNone> {
+        let mtime = timestamp(py, mtime)?;
         self.update(py, |entry| entry.set_clean(mode, size, mtime));
         Ok(PyNone)
     }
@@ -188,3 +199,15 @@ 
         self.entry(py).set(entry)
     }
 }
+
+pub(crate) fn timestamp(
+    py: Python<'_>,
+    (s, ns): (u32, u32),
+) -> PyResult<TruncatedTimestamp> {
+    TruncatedTimestamp::from_already_truncated(s, ns).map_err(|_| {
+        PyErr::new::<exc::ValueError, _>(
+            py,
+            "expected mtime truncated to 31 bits",
+        )
+    })
+}
diff --git a/rust/hg-cpython/src/dirstate/dirstate_map.rs b/rust/hg-cpython/src/dirstate/dirstate_map.rs
--- a/rust/hg-cpython/src/dirstate/dirstate_map.rs
+++ b/rust/hg-cpython/src/dirstate/dirstate_map.rs
@@ -18,11 +18,10 @@ 
 
 use crate::{
     dirstate::copymap::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator},
-    dirstate::item::DirstateItem,
+    dirstate::item::{timestamp, DirstateItem},
     pybytes_deref::PyBytesDeref,
 };
 use hg::{
-    dirstate::parsers::Timestamp,
     dirstate::StateMapIter,
     dirstate_tree::dirstate_map::DirstateMap as TreeDirstateMap,
     dirstate_tree::on_disk::DirstateV2ParseError,
@@ -195,9 +194,9 @@ 
         &self,
         p1: PyObject,
         p2: PyObject,
-        now: PyObject
+        now: (u32, u32)
     ) -> PyResult<PyBytes> {
-        let now = Timestamp(now.extract(py)?);
+        let now = timestamp(py, now)?;
 
         let mut inner = self.inner(py).borrow_mut();
         let parents = DirstateParents {
@@ -219,10 +218,10 @@ 
     /// instead of written to a new data file (False).
     def write_v2(
         &self,
-        now: PyObject,
+        now: (u32, u32),
         can_append: bool,
     ) -> PyResult<PyObject> {
-        let now = Timestamp(now.extract(py)?);
+        let now = timestamp(py, now)?;
 
         let mut inner = self.inner(py).borrow_mut();
         let result = inner.pack_v2(now, can_append);
diff --git a/rust/hg-cpython/src/dirstate.rs b/rust/hg-cpython/src/dirstate.rs
--- a/rust/hg-cpython/src/dirstate.rs
+++ b/rust/hg-cpython/src/dirstate.rs
@@ -54,7 +54,7 @@ 
                 matcher: PyObject,
                 ignorefiles: PyList,
                 check_exec: bool,
-                last_normal_time: i64,
+                last_normal_time: (u32, u32),
                 list_clean: bool,
                 list_ignored: bool,
                 list_unknown: bool,
diff --git a/rust/hg-core/src/dirstate_tree/status.rs b/rust/hg-core/src/dirstate_tree/status.rs
--- a/rust/hg-core/src/dirstate_tree/status.rs
+++ b/rust/hg-core/src/dirstate_tree/status.rs
@@ -533,7 +533,8 @@ 
         } else {
             let mtime = mtime_seconds(fs_metadata);
             if truncate_i64(mtime) != entry.mtime()
-                || mtime == self.options.last_normal_time
+                || mtime
+                    == self.options.last_normal_time.truncated_seconds() as i64
             {
                 self.outcome
                     .lock()
diff --git a/rust/hg-core/src/dirstate_tree/on_disk.rs b/rust/hg-core/src/dirstate_tree/on_disk.rs
--- a/rust/hg-core/src/dirstate_tree/on_disk.rs
+++ b/rust/hg-core/src/dirstate_tree/on_disk.rs
@@ -310,7 +310,7 @@ 
         &self,
     ) -> Result<dirstate_map::NodeData, DirstateV2ParseError> {
         if self.has_entry() {
-            Ok(dirstate_map::NodeData::Entry(self.assume_entry()))
+            Ok(dirstate_map::NodeData::Entry(self.assume_entry()?))
         } else if let Some(mtime) = self.cached_directory_mtime()? {
             Ok(dirstate_map::NodeData::CachedDirectory { mtime })
         } else {
@@ -346,7 +346,7 @@ 
         file_type | permisions
     }
 
-    fn assume_entry(&self) -> DirstateEntry {
+    fn assume_entry(&self) -> Result<DirstateEntry, DirstateV2ParseError> {
         // TODO: convert through raw bits instead?
         let wdir_tracked = self.flags().contains(Flags::WDIR_TRACKED);
         let p1_tracked = self.flags().contains(Flags::P1_TRACKED);
@@ -357,24 +357,24 @@ 
             None
         };
         let mtime = if self.flags().contains(Flags::HAS_FILE_MTIME) {
-            Some(self.mtime.truncated_seconds.into())
+            Some(self.mtime.try_into()?)
         } else {
             None
         };
-        DirstateEntry::from_v2_data(
+        Ok(DirstateEntry::from_v2_data(
             wdir_tracked,
             p1_tracked,
             p2_info,
             mode_size,
             mtime,
-        )
+        ))
     }
 
     pub(super) fn entry(
         &self,
     ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
         if self.has_entry() {
-            Ok(Some(self.assume_entry()))
+            Ok(Some(self.assume_entry()?))
         } else {
             Ok(None)
         }
@@ -426,10 +426,7 @@ 
         };
         let mtime = if let Some(m) = mtime_opt {
             flags.insert(Flags::HAS_FILE_MTIME);
-            PackedTruncatedTimestamp {
-                truncated_seconds: m.into(),
-                nanoseconds: 0.into(),
-            }
+            m.into()
         } else {
             PackedTruncatedTimestamp::null()
         };
diff --git a/rust/hg-core/src/dirstate_tree/dirstate_map.rs b/rust/hg-core/src/dirstate_tree/dirstate_map.rs
--- a/rust/hg-core/src/dirstate_tree/dirstate_map.rs
+++ b/rust/hg-core/src/dirstate_tree/dirstate_map.rs
@@ -1,7 +1,6 @@ 
 use bytes_cast::BytesCast;
 use micro_timer::timed;
 use std::borrow::Cow;
-use std::convert::TryInto;
 use std::path::PathBuf;
 
 use super::on_disk;
@@ -11,7 +10,6 @@ 
 use crate::dirstate::parsers::pack_entry;
 use crate::dirstate::parsers::packed_entry_size;
 use crate::dirstate::parsers::parse_dirstate_entries;
-use crate::dirstate::parsers::Timestamp;
 use crate::dirstate::CopyMapIter;
 use crate::dirstate::StateMapIter;
 use crate::dirstate::TruncatedTimestamp;
@@ -932,10 +930,9 @@ 
     pub fn pack_v1(
         &mut self,
         parents: DirstateParents,
-        now: Timestamp,
+        now: TruncatedTimestamp,
     ) -> Result<Vec<u8>, DirstateError> {
         let map = self.get_map_mut();
-        let now: i32 = now.0.try_into().expect("time overflow");
         let mut ambiguous_mtimes = Vec::new();
         // Optizimation (to be measured?): pre-compute size to avoid `Vec`
         // reallocations
@@ -981,12 +978,10 @@ 
     #[timed]
     pub fn pack_v2(
         &mut self,
-        now: Timestamp,
+        now: TruncatedTimestamp,
         can_append: bool,
     ) -> Result<(Vec<u8>, Vec<u8>, bool), DirstateError> {
         let map = self.get_map_mut();
-        // TODO: how do we want to handle this in 2038?
-        let now: i32 = now.0.try_into().expect("time overflow");
         let mut paths = Vec::new();
         for node in map.iter_nodes() {
             let node = node?;
diff --git a/rust/hg-core/src/dirstate/status.rs b/rust/hg-core/src/dirstate/status.rs
--- a/rust/hg-core/src/dirstate/status.rs
+++ b/rust/hg-core/src/dirstate/status.rs
@@ -12,6 +12,7 @@ 
 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
 
 use crate::{
+    dirstate::TruncatedTimestamp,
     utils::hg_path::{HgPath, HgPathError},
     PatternError,
 };
@@ -64,7 +65,7 @@ 
     /// Remember the most recent modification timeslot for status, to make
     /// sure we won't miss future size-preserving file content modifications
     /// that happen within the same timeslot.
-    pub last_normal_time: i64,
+    pub last_normal_time: TruncatedTimestamp,
     /// Whether we are on a filesystem with UNIX-like exec flags
     pub check_exec: bool,
     pub list_clean: bool,
diff --git a/rust/hg-core/src/dirstate/parsers.rs b/rust/hg-core/src/dirstate/parsers.rs
--- a/rust/hg-core/src/dirstate/parsers.rs
+++ b/rust/hg-core/src/dirstate/parsers.rs
@@ -135,6 +135,3 @@ 
         packed.extend(source.as_bytes());
     }
 }
-
-/// Seconds since the Unix epoch
-pub struct Timestamp(pub i64);
diff --git a/rust/hg-core/src/dirstate/entry.rs b/rust/hg-core/src/dirstate/entry.rs
--- a/rust/hg-core/src/dirstate/entry.rs
+++ b/rust/hg-core/src/dirstate/entry.rs
@@ -14,14 +14,15 @@ 
     Merged,
 }
 
-/// The C implementation uses all signed types. This will be an issue
-/// either when 4GB+ source files are commonplace or in 2038, whichever
-/// comes first.
-#[derive(Debug, PartialEq, Copy, Clone)]
+/// `size` and `mtime.seconds` are truncated to 31 bits.
+///
+/// TODO: double-check status algorithm correctness for files
+/// larger than 2 GiB or modified after 2038.
+#[derive(Debug, Copy, Clone)]
 pub struct DirstateEntry {
     pub(crate) flags: Flags,
     mode_size: Option<(u32, u32)>,
-    mtime: Option<u32>,
+    mtime: Option<TruncatedTimestamp>,
 }
 
 bitflags! {
@@ -33,7 +34,7 @@ 
 }
 
 /// A Unix timestamp with nanoseconds precision
-#[derive(Copy, Clone)]
+#[derive(Debug, Copy, Clone)]
 pub struct TruncatedTimestamp {
     truncated_seconds: u32,
     /// Always in the `0 .. 1_000_000_000` range.
@@ -178,16 +179,13 @@ 
         p1_tracked: bool,
         p2_info: bool,
         mode_size: Option<(u32, u32)>,
-        mtime: Option<u32>,
+        mtime: Option<TruncatedTimestamp>,
     ) -> Self {
         if let Some((mode, size)) = mode_size {
             // TODO: return an error for out of range values?
             assert!(mode & !RANGE_MASK_31BIT == 0);
             assert!(size & !RANGE_MASK_31BIT == 0);
         }
-        if let Some(mtime) = mtime {
-            assert!(mtime & !RANGE_MASK_31BIT == 0);
-        }
         let mut flags = Flags::empty();
         flags.set(Flags::WDIR_TRACKED, wdir_tracked);
         flags.set(Flags::P1_TRACKED, p1_tracked);
@@ -234,6 +232,9 @@ 
                     let mode = u32::try_from(mode).unwrap();
                     let size = u32::try_from(size).unwrap();
                     let mtime = u32::try_from(mtime).unwrap();
+                    let mtime =
+                        TruncatedTimestamp::from_already_truncated(mtime, 0)
+                            .unwrap();
                     Self {
                         flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
                         mode_size: Some((mode, size)),
@@ -321,7 +322,13 @@ 
     /// Returns `(wdir_tracked, p1_tracked, p2_info, mode_size, mtime)`
     pub(crate) fn v2_data(
         &self,
-    ) -> (bool, bool, bool, Option<(u32, u32)>, Option<u32>) {
+    ) -> (
+        bool,
+        bool,
+        bool,
+        Option<(u32, u32)>,
+        Option<TruncatedTimestamp>,
+    ) {
         if !self.any_tracked() {
             // TODO: return an Option instead?
             panic!("Accessing v1_state of an untracked DirstateEntry")
@@ -395,7 +402,7 @@ 
         } else if !self.flags.contains(Flags::P1_TRACKED) {
             MTIME_UNSET
         } else if let Some(mtime) = self.mtime {
-            i32::try_from(mtime).unwrap()
+            i32::try_from(mtime.truncated_seconds()).unwrap()
         } else {
             MTIME_UNSET
         }
@@ -421,6 +428,10 @@ 
         self.v1_mtime()
     }
 
+    pub fn truncated_mtime(&self) -> Option<TruncatedTimestamp> {
+        self.mtime
+    }
+
     pub fn drop_merge_data(&mut self) {
         if self.flags.contains(Flags::P2_INFO) {
             self.flags.remove(Flags::P2_INFO);
@@ -433,9 +444,13 @@ 
         self.mtime = None
     }
 
-    pub fn set_clean(&mut self, mode: u32, size: u32, mtime: u32) {
+    pub fn set_clean(
+        &mut self,
+        mode: u32,
+        size: u32,
+        mtime: TruncatedTimestamp,
+    ) {
         let size = size & RANGE_MASK_31BIT;
-        let mtime = mtime & RANGE_MASK_31BIT;
         self.flags.insert(Flags::WDIR_TRACKED | Flags::P1_TRACKED);
         self.mode_size = Some((mode, size));
         self.mtime = Some(mtime);
@@ -496,11 +511,15 @@ 
         (self.state().into(), self.mode(), self.size(), self.mtime())
     }
 
-    pub fn mtime_is_ambiguous(&self, now: i32) -> bool {
-        self.state() == EntryState::Normal && self.mtime() == now
+    pub fn mtime_is_ambiguous(&self, now: TruncatedTimestamp) -> bool {
+        if let Some(mtime) = self.mtime {
+            self.state() == EntryState::Normal && mtime.very_likely_equal(now)
+        } else {
+            false
+        }
     }
 
-    pub fn clear_ambiguous_mtime(&mut self, now: i32) -> bool {
+    pub fn clear_ambiguous_mtime(&mut self, now: TruncatedTimestamp) -> bool {
         let ambiguous = self.mtime_is_ambiguous(now);
         if ambiguous {
             // The file was last modified "simultaneously" with the current
diff --git a/mercurial/pure/parsers.py b/mercurial/pure/parsers.py
--- a/mercurial/pure/parsers.py
+++ b/mercurial/pure/parsers.py
@@ -92,7 +92,8 @@ 
     _p2_info = attr.ib()
     _mode = attr.ib()
     _size = attr.ib()
-    _mtime = attr.ib()
+    _mtime_s = attr.ib()
+    _mtime_ns = attr.ib()
 
     def __init__(
         self,
@@ -109,7 +110,8 @@ 
 
         self._mode = None
         self._size = None
-        self._mtime = None
+        self._mtime_s = None
+        self._mtime_ns = None
         if parentfiledata is None:
             has_meaningful_mtime = False
             has_meaningful_data = False
@@ -117,10 +119,10 @@ 
             self._mode = parentfiledata[0]
             self._size = parentfiledata[1]
         if has_meaningful_mtime:
-            self._mtime = parentfiledata[2]
+            self._mtime_s, self._mtime_ns = parentfiledata[2]
 
     @classmethod
-    def from_v2_data(cls, flags, size, mtime):
+    def from_v2_data(cls, flags, size, mtime_s, mtime_ns):
         """Build a new DirstateItem object from V2 data"""
         has_mode_size = bool(flags & DIRSTATE_V2_HAS_MODE_AND_SIZE)
         mode = None
@@ -140,7 +142,7 @@ 
             p2_info=bool(flags & DIRSTATE_V2_P2_INFO),
             has_meaningful_data=has_mode_size,
             has_meaningful_mtime=bool(flags & DIRSTATE_V2_HAS_FILE_MTIME),
-            parentfiledata=(mode, size, mtime),
+            parentfiledata=(mode, size, (mtime_s, mtime_ns)),
         )
 
     @classmethod
@@ -175,13 +177,13 @@ 
                     wc_tracked=True,
                     p1_tracked=True,
                     has_meaningful_mtime=False,
-                    parentfiledata=(mode, size, 42),
+                    parentfiledata=(mode, size, (42, 0)),
                 )
             else:
                 return cls(
                     wc_tracked=True,
                     p1_tracked=True,
-                    parentfiledata=(mode, size, mtime),
+                    parentfiledata=(mode, size, (mtime, 0)),
                 )
         else:
             raise RuntimeError(b'unknown state: %s' % state)
@@ -192,7 +194,8 @@ 
         This means the next status call will have to actually check its content
         to make sure it is correct.
         """
-        self._mtime = None
+        self._mtime_s = None
+        self._mtime_ns = None
 
     def set_clean(self, mode, size, mtime):
         """mark a file as "clean" cancelling potential "possibly dirty call"
@@ -206,7 +209,7 @@ 
         self._p1_tracked = True
         self._mode = mode
         self._size = size
-        self._mtime = mtime
+        self._mtime_s, self._mtime_ns = mtime
 
     def set_tracked(self):
         """mark a file as tracked in the working copy
@@ -218,7 +221,8 @@ 
         # the files as needing lookup
         #
         # Consider dropping this in the future in favor of something less broad.
-        self._mtime = None
+        self._mtime_s = None
+        self._mtime_ns = None
 
     def set_untracked(self):
         """mark a file as untracked in the working copy
@@ -228,7 +232,8 @@ 
         self._wc_tracked = False
         self._mode = None
         self._size = None
-        self._mtime = None
+        self._mtime_s = None
+        self._mtime_ns = None
 
     def drop_merge_data(self):
         """remove all "merge-only" from a DirstateItem
@@ -239,7 +244,8 @@ 
             self._p2_info = False
             self._mode = None
             self._size = None
-            self._mtime = None
+            self._mtime_s = None
+            self._mtime_ns = None
 
     @property
     def mode(self):
@@ -253,6 +259,14 @@ 
     def mtime(self):
         return self.v1_mtime()
 
+    def mtime_likely_equal_to(self, other_mtime):
+        self_sec = self._mtime_s
+        if self_sec is None:
+            return False
+        self_ns = self._mtime_ns
+        other_sec, other_ns = other_mtime
+        return self_sec == other_sec and self_ns == other_ns
+
     @property
     def state(self):
         """
@@ -329,9 +343,9 @@ 
                 flags |= DIRSTATE_V2_MODE_EXEC_PERM
             if stat.S_ISLNK(self.mode):
                 flags |= DIRSTATE_V2_MODE_IS_SYMLINK
-        if self._mtime is not None:
+        if self._mtime_s is not None:
             flags |= DIRSTATE_V2_HAS_FILE_MTIME
-        return (flags, self._size or 0, self._mtime or 0)
+        return (flags, self._size or 0, self._mtime_s or 0, self._mtime_ns or 0)
 
     def v1_state(self):
         """return a "state" suitable for v1 serialization"""
@@ -379,18 +393,18 @@ 
             raise RuntimeError('untracked item')
         elif self.removed:
             return 0
-        elif self._mtime is None:
+        elif self._mtime_s is None:
             return AMBIGUOUS_TIME
         elif self._p2_info:
             return AMBIGUOUS_TIME
         elif not self._p1_tracked:
             return AMBIGUOUS_TIME
         else:
-            return self._mtime
+            return self._mtime_s
 
     def need_delay(self, now):
         """True if the stored mtime would be ambiguous with the current time"""
-        return self.v1_state() == b'n' and self.v1_mtime() == now
+        return self.v1_state() == b'n' and self.mtime_likely_equal_to(now)
 
 
 def gettype(q):
@@ -758,7 +772,6 @@ 
 
 
 def pack_dirstate(dmap, copymap, pl, now):
-    now = int(now)
     cs = stringio()
     write = cs.write
     write(b"".join(pl))
diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -9,13 +9,13 @@ 
 
 import collections
 import errno
-import stat
 import struct
 
 from .i18n import _
 from .node import nullrev
 from .thirdparty import attr
 from .utils import stringutil
+from .dirstateutils import timestamp
 from . import (
     copies,
     encoding,
@@ -1406,8 +1406,9 @@ 
             if wantfiledata:
                 s = wfctx.lstat()
                 mode = s.st_mode
-                mtime = s[stat.ST_MTIME]
-                filedata[f] = (mode, size, mtime)  # for dirstate.normal
+                mtime = timestamp.mtime_of(s)
+                # for dirstate.update_file's parentfiledata argument:
+                filedata[f] = (mode, size, mtime)
             if i == 100:
                 yield False, (i, f)
                 i = 0
diff --git a/mercurial/dirstateutils/v2.py b/mercurial/dirstateutils/v2.py
--- a/mercurial/dirstateutils/v2.py
+++ b/mercurial/dirstateutils/v2.py
@@ -98,13 +98,13 @@ 
             flags,
             size,
             mtime_s,
-            _mtime_ns,
+            mtime_ns,
         ) = NODE.unpack(node_bytes)
 
         # Parse child nodes of this node recursively
         parse_nodes(map, copy_map, data, children_start, children_count)
 
-        item = parsers.DirstateItem.from_v2_data(flags, size, mtime_s)
+        item = parsers.DirstateItem.from_v2_data(flags, size, mtime_s, mtime_ns)
         if not item.any_tracked:
             continue
         path = slice_with_len(data, path_start, path_len)
@@ -144,8 +144,7 @@ 
             copy_source_start = 0
             copy_source_len = 0
         if entry is not None:
-            flags, size, mtime_s = entry.v2_data()
-            mtime_ns = 0
+            flags, size, mtime_s, mtime_ns = entry.v2_data()
         else:
             # There are no mtime-cached directories in the Python implementation
             flags = 0
@@ -246,7 +245,6 @@ 
     written to the docket. Again, see more details on the on-disk format in
     `mercurial/helptext/internals/dirstate-v2`.
     """
-    now = int(now)
     data = bytearray()
     root_nodes_start = 0
     root_nodes_len = 0
diff --git a/mercurial/dirstateutils/timestamp.py b/mercurial/dirstateutils/timestamp.py
new file mode 100644
--- /dev/null
+++ b/mercurial/dirstateutils/timestamp.py
@@ -0,0 +1,53 @@ 
+# Copyright Mercurial Contributors
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import absolute_import
+
+import stat
+
+
+rangemask = 0x7FFFFFFF
+
+
+class timestamp(tuple):
+    """
+    A Unix timestamp with nanoseconds precision,
+    modulo 2**31 seconds.
+
+    A 2-tuple containing:
+
+    `truncated_seconds`: seconds since the Unix epoch,
+    truncated to its lower 31 bits
+
+    `subsecond_nanoseconds`: number of nanoseconds since `truncated_seconds`.
+    """
+
+    def __new__(cls, value):
+        truncated_seconds, subsec_nanos = value
+        value = (truncated_seconds & rangemask, subsec_nanos)
+        return super(timestamp, cls).__new__(cls, value)
+
+
+def zero():
+    """
+    Returns the `timestamp` at the Unix epoch.
+    """
+    return tuple.__new__(timestamp, (0, 0))
+
+
+def mtime_of(stat_result):
+    """
+    Takes an `os.stat_result`-like object and returns a `timestamp` object
+    for its modification time.
+    """
+    # https://docs.python.org/2/library/os.html#os.stat_float_times
+    # "For compatibility with older Python versions,
+    #  accessing stat_result as a tuple always returns integers."
+    secs = stat_result[stat.ST_MTIME]
+
+    # For now
+    subsec_nanos = 0
+
+    return timestamp((secs, subsec_nanos))
diff --git a/mercurial/dirstatemap.py b/mercurial/dirstatemap.py
--- a/mercurial/dirstatemap.py
+++ b/mercurial/dirstatemap.py
@@ -127,7 +127,6 @@ 
     def set_clean(self, filename, mode, size, mtime):
         """mark a file as back to a clean state"""
         entry = self[filename]
-        mtime = mtime & rangemask
         size = size & rangemask
         entry.set_clean(mode, size, mtime)
         self._refresh_entry(filename, entry)
diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstate.py
@@ -31,6 +31,10 @@ 
     util,
 )
 
+from .dirstateutils import (
+    timestamp,
+)
+
 from .interfaces import (
     dirstate as intdirstate,
     util as interfaceutil,
@@ -66,7 +70,7 @@ 
     '''Get "now" timestamp on filesystem'''
     tmpfd, tmpname = vfs.mkstemp()
     try:
-        return os.fstat(tmpfd)[stat.ST_MTIME]
+        return timestamp.mtime_of(os.fstat(tmpfd))
     finally:
         os.close(tmpfd)
         vfs.unlink(tmpname)
@@ -122,7 +126,7 @@ 
         # UNC path pointing to root share (issue4557)
         self._rootdir = pathutil.normasprefix(root)
         self._dirty = False
-        self._lastnormaltime = 0
+        self._lastnormaltime = timestamp.zero()
         self._ui = ui
         self._filecache = {}
         self._parentwriters = 0
@@ -421,7 +425,7 @@ 
         for a in ("_map", "_branch", "_ignore"):
             if a in self.__dict__:
                 delattr(self, a)
-        self._lastnormaltime = 0
+        self._lastnormaltime = timestamp.zero()
         self._dirty = False
         self._parentwriters = 0
         self._origpl = None
@@ -620,7 +624,7 @@ 
         s = os.lstat(self._join(filename))
         mode = s.st_mode
         size = s.st_size
-        mtime = s[stat.ST_MTIME]
+        mtime = timestamp.mtime_of(s)
         return (mode, size, mtime)
 
     def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
@@ -804,7 +808,7 @@ 
         if now is None:
             # use the modification time of the newly created temporary file as the
             # filesystem's notion of 'now'
-            now = util.fstat(st)[stat.ST_MTIME] & _rangemask
+            now = timestamp.mtime_of(util.fstat(st))
 
         # enough 'delaywrite' prevents 'pack_dirstate' from dropping
         # timestamp of each entries in dirstate, because of 'now > mtime'
@@ -821,11 +825,12 @@ 
                     start = int(clock) - (int(clock) % delaywrite)
                     end = start + delaywrite
                     time.sleep(end - clock)
-                    now = end  # trust our estimate that the end is near now
+                    # trust our estimate that the end is near now
+                    now = timestamp.timestamp((end, 0))
                     break
 
         self._map.write(tr, st, now)
-        self._lastnormaltime = 0
+        self._lastnormaltime = timestamp.zero()
         self._dirty = False
 
     def _dirignore(self, f):
@@ -1358,17 +1363,9 @@ 
                     uadd(fn)
                 continue
 
-            # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
-            # written like that for performance reasons. dmap[fn] is not a
-            # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
-            # opcode has fast paths when the value to be unpacked is a tuple or
-            # a list, but falls back to creating a full-fledged iterator in
-            # general. That is much slower than simply accessing and storing the
-            # tuple members one by one.
             t = dget(fn)
             mode = t.mode
             size = t.size
-            time = t.mtime
 
             if not st and t.tracked:
                 dadd(fn)
@@ -1393,12 +1390,9 @@ 
                         ladd(fn)
                     else:
                         madd(fn)
-                elif (
-                    time != st[stat.ST_MTIME]
-                    and time != st[stat.ST_MTIME] & _rangemask
-                ):
+                elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
                     ladd(fn)
-                elif st[stat.ST_MTIME] == lastnormaltime:
+                elif timestamp.mtime_of(st) == lastnormaltime:
                     # fn may have just been marked as normal and it may have
                     # changed in the same second without changing its size.
                     # This can happen if we quickly do multiple commits.
diff --git a/mercurial/cext/util.h b/mercurial/cext/util.h
--- a/mercurial/cext/util.h
+++ b/mercurial/cext/util.h
@@ -27,7 +27,8 @@ 
 	unsigned char flags;
 	int mode;
 	int size;
-	int mtime;
+	int mtime_s;
+	int mtime_ns;
 } dirstateItemObject;
 /* clang-format on */
 
diff --git a/mercurial/cext/parsers.c b/mercurial/cext/parsers.c
--- a/mercurial/cext/parsers.c
+++ b/mercurial/cext/parsers.c
@@ -57,7 +57,8 @@ 
 	int has_meaningful_mtime;
 	int mode;
 	int size;
-	int mtime;
+	int mtime_s;
+	int mtime_ns;
 	PyObject *parentfiledata;
 	static char *keywords_name[] = {
 	    "wc_tracked",
@@ -97,15 +98,10 @@ 
 	}
 
 	if (parentfiledata != Py_None) {
-		if (!PyTuple_CheckExact(parentfiledata)) {
-			PyErr_SetString(
-			    PyExc_TypeError,
-			    "parentfiledata should be a Tuple or None");
+		if (!PyArg_ParseTuple(parentfiledata, "ii(ii)", &mode, &size,
+		                      &mtime_s, &mtime_ns)) {
 			return NULL;
 		}
-		mode = (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 0));
-		size = (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 1));
-		mtime = (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 2));
 	} else {
 		has_meaningful_data = 0;
 		has_meaningful_mtime = 0;
@@ -120,9 +116,11 @@ 
 	}
 	if (has_meaningful_mtime) {
 		t->flags |= dirstate_flag_has_file_mtime;
-		t->mtime = mtime;
+		t->mtime_s = mtime_s;
+		t->mtime_ns = mtime_ns;
 	} else {
-		t->mtime = 0;
+		t->mtime_s = 0;
+		t->mtime_ns = 0;
 	}
 	return (PyObject *)t;
 }
@@ -231,7 +229,7 @@ 
 	           (self->flags & dirstate_flag_p2_info)) {
 		return ambiguous_time;
 	} else {
-		return self->mtime;
+		return self->mtime_s;
 	}
 }
 
@@ -249,7 +247,8 @@ 
 	} else {
 		flags &= ~dirstate_flag_mode_is_symlink;
 	}
-	return Py_BuildValue("Bii", flags, self->size, self->mtime);
+	return Py_BuildValue("Biii", flags, self->size, self->mtime_s,
+	                     self->mtime_ns);
 };
 
 static PyObject *dirstate_item_v1_state(dirstateItemObject *self)
@@ -274,14 +273,31 @@ 
 };
 
 static PyObject *dirstate_item_need_delay(dirstateItemObject *self,
-                                          PyObject *value)
+                                          PyObject *now)
 {
-	long now;
-	if (!pylong_to_long(value, &now)) {
+	int now_s;
+	int now_ns;
+	if (!PyArg_ParseTuple(now, "ii", &now_s, &now_ns)) {
 		return NULL;
 	}
-	if (dirstate_item_c_v1_state(self) == 'n' &&
-	    dirstate_item_c_v1_mtime(self) == now) {
+	if (dirstate_item_c_v1_state(self) == 'n' && self->mtime_s == now_s &&
+	    self->mtime_ns == now_ns) {
+		Py_RETURN_TRUE;
+	} else {
+		Py_RETURN_FALSE;
+	}
+};
+
+static PyObject *dirstate_item_mtime_likely_equal_to(dirstateItemObject *self,
+                                                     PyObject *other)
+{
+	int other_s;
+	int other_ns;
+	if (!PyArg_ParseTuple(other, "ii", &other_s, &other_ns)) {
+		return NULL;
+	}
+	if ((self->flags & dirstate_flag_has_file_mtime) &&
+	    self->mtime_s == other_s && self->mtime_ns == other_ns) {
 		Py_RETURN_TRUE;
 	} else {
 		Py_RETURN_FALSE;
@@ -301,7 +317,8 @@ 
 	t->flags = 0;
 	t->mode = 0;
 	t->size = 0;
-	t->mtime = 0;
+	t->mtime_s = 0;
+	t->mtime_ns = 0;
 
 	if (state == 'm') {
 		t->flags = (dirstate_flag_wc_tracked |
@@ -337,7 +354,7 @@ 
 			            dirstate_flag_has_file_mtime);
 			t->mode = mode;
 			t->size = size;
-			t->mtime = mtime;
+			t->mtime_s = mtime;
 		}
 	} else {
 		PyErr_Format(PyExc_RuntimeError,
@@ -372,7 +389,8 @@ 
 	if (!t) {
 		return NULL;
 	}
-	if (!PyArg_ParseTuple(args, "bii", &t->flags, &t->size, &t->mtime)) {
+	if (!PyArg_ParseTuple(args, "biii", &t->flags, &t->size, &t->mtime_s,
+	                      &t->mtime_ns)) {
 		return NULL;
 	}
 	t->mode = 0;
@@ -403,8 +421,9 @@ 
 static PyObject *dirstate_item_set_clean(dirstateItemObject *self,
                                          PyObject *args)
 {
-	int size, mode, mtime;
-	if (!PyArg_ParseTuple(args, "iii", &mode, &size, &mtime)) {
+	int size, mode, mtime_s, mtime_ns;
+	if (!PyArg_ParseTuple(args, "ii(ii)", &mode, &size, &mtime_s,
+	                      &mtime_ns)) {
 		return NULL;
 	}
 	self->flags = dirstate_flag_wc_tracked | dirstate_flag_p1_tracked |
@@ -412,7 +431,8 @@ 
 	              dirstate_flag_has_file_mtime;
 	self->mode = mode;
 	self->size = size;
-	self->mtime = mtime;
+	self->mtime_s = mtime_s;
+	self->mtime_ns = mtime_ns;
 	Py_RETURN_NONE;
 }
 
@@ -427,8 +447,9 @@ 
 {
 	self->flags &= ~dirstate_flag_wc_tracked;
 	self->mode = 0;
-	self->mtime = 0;
 	self->size = 0;
+	self->mtime_s = 0;
+	self->mtime_ns = 0;
 	Py_RETURN_NONE;
 }
 
@@ -439,8 +460,9 @@ 
 		                 dirstate_flag_has_meaningful_data |
 		                 dirstate_flag_has_file_mtime);
 		self->mode = 0;
-		self->mtime = 0;
 		self->size = 0;
+		self->mtime_s = 0;
+		self->mtime_ns = 0;
 	}
 	Py_RETURN_NONE;
 }
@@ -457,6 +479,8 @@ 
      "return a \"mtime\" suitable for v1 serialization"},
     {"need_delay", (PyCFunction)dirstate_item_need_delay, METH_O,
      "True if the stored mtime would be ambiguous with the current time"},
+    {"mtime_likely_equal_to", (PyCFunction)dirstate_item_mtime_likely_equal_to,
+     METH_O, "True if the stored mtime is likely equal to the given mtime"},
     {"from_v1_data", (PyCFunction)dirstate_item_from_v1_meth,
      METH_VARARGS | METH_CLASS, "build a new DirstateItem object from V1 data"},
     {"from_v2_data", (PyCFunction)dirstate_item_from_v2_meth,
@@ -742,11 +766,12 @@ 
 	Py_ssize_t nbytes, pos, l;
 	PyObject *k, *v = NULL, *pn;
 	char *p, *s;
-	int now;
+	int now_s;
+	int now_ns;
 
-	if (!PyArg_ParseTuple(args, "O!O!O!i:pack_dirstate", &PyDict_Type, &map,
-	                      &PyDict_Type, &copymap, &PyTuple_Type, &pl,
-	                      &now)) {
+	if (!PyArg_ParseTuple(args, "O!O!O!(ii):pack_dirstate", &PyDict_Type,
+	                      &map, &PyDict_Type, &copymap, &PyTuple_Type, &pl,
+	                      &now_s, &now_ns)) {
 		return NULL;
 	}
 
@@ -815,7 +840,8 @@ 
 		mode = dirstate_item_c_v1_mode(tuple);
 		size = dirstate_item_c_v1_size(tuple);
 		mtime = dirstate_item_c_v1_mtime(tuple);
-		if (state == 'n' && mtime == now) {
+		if (state == 'n' && tuple->mtime_s == now_s &&
+		    tuple->mtime_ns == now_ns) {
 			/* See pure/parsers.py:pack_dirstate for why we do
 			 * this. */
 			mtime = -1;