Opened 4 years ago

Closed 2 years ago

#7473 closed bug (fixed)

getModificationTime gives only second-level resolution

Reported by: duncan Owned by: ekmett
Priority: normal Milestone: 7.10.1
Component: Core Libraries Version: 7.6.1
Keywords: directory Cc: the.dead.shall.rise@…, redneb@…, mail@…, shelarcy@…, ndmitchell@…, core-libraries-committee@…
Operating System: Unknown/Multiple Architecture: Unknown/Multiple
Type of failure: None/Unknown Test Case:
Blocked By: Blocking:
Related Tickets: Differential Rev(s):
Wiki Page:


The System.Directory.getModificationTime function only returns time with second-level granularity. This is no good for applications like ghc --make where we need to compare file timestamps.

The fix should be simple, here's a patch (compiles but not tested) that should fix it for Unix, for Windows we will need a different fix, probably using the Win32 API directly, rather than the mingw C api.

diff --git a/System/Directory.hs b/System/Directory.hs
index cfe7cd9..f27bfc4 100644
--- a/System/Directory.hs
+++ b/System/Directory.hs
@@ -995,14 +995,14 @@ The operation may fail with:
 getModificationTime :: FilePath -> IO UTCTime
 getModificationTime name = do
 #ifdef mingw32_HOST_OS
- -- ToDo: use Win32 API
+ -- ToDo: use Win32 API so we can get sub-second resolution
  withFileStatus "getModificationTime" name $ \ st -> do
  modificationTime st
   stat <- Posix.getFileStatus name
-  let mod_time :: Posix.EpochTime
-      mod_time = Posix.modificationTime stat
-  return $ posixSecondsToUTCTime $ realToFrac mod_time
+  let mod_time :: POSIXTime
+      mod_time = Posix.modificationTimeHiRes stat
+  return $! posixSecondsToUTCTime mod_time
 #endif /* __GLASGOW_HASKELL__ */

Change History (20)

comment:1 Changed 4 years ago by duncan

Status: newpatch

comment:2 Changed 4 years ago by duncan

On the issue of Win32, Malcolm notes:

Open Shake implements the finer resolution timestamps needed using the
Win32 API.  You may want to copy/adapt the code from there:

comment:3 Changed 4 years ago by igloo

difficulty: Unknown

Related: #6160

comment:4 Changed 4 years ago by igloo

Milestone: 7.8.1
Status: patchnew

Applied, thanks. I'll leave the ticket open for the Windows half.

comment:5 Changed 4 years ago by refold

Cc: the.dead.shall.rise@… added

comment:6 Changed 4 years ago by refold

I've added a version of Neil's code to Cabal:

Perhaps it can be reused for directory.

comment:7 Changed 4 years ago by redneb

Cc: redneb@… added

comment:8 Changed 4 years ago by nh2

Cc: mail@… added

comment:9 Changed 3 years ago by shelarcy

Cc: shelarcy@… added

comment:10 Changed 3 years ago by NeilMitchell

Cc: ndmitchell@… added

The GetFileAttributesEx function on Windows returns resolution of 100ns, but if you repeatedly write to a file and read modification times under Windows you will see it change in granularity of 1s, as the Windows NTFS file system omits any modification time update if one was already done in the last second. As a result, getModificationTime isn't going to have the resolution you are after on Windows, even with the new function. I have verified this experimentally on both a Windows XP laptop and a Windows Vista desktop.

comment:11 Changed 3 years ago by redneb

I cannot reproduce that. In my experiments, GetFileAttributesEx works as expected. To test this I ran the following command (in a cygwin shell so that I can use sleep):

echo 1 >> test.txt && ./modtime test.txt && sleep 0.1 && echo 2 >> test.txt && ./modtime test.txt

The output was:

test.txt: 2013-08-29 00:30:02.4667839 UTC
test.txt: 2013-08-29 00:30:02.5869567 UTC

As you can see that's full 100ns resolution.

And here's the code for the modtime program I used above:

import System.Win32
import Data.Time ()
import Data.Time.Clock.POSIX
import System.Environment

main :: IO ()
main = getArgs >>= mapM_ printModTime 

printModTime :: String -> IO ()
printModTime path = do
    fad <- getFileAttributesExStandard path
    let FILETIME ft = fadLastWriteTime fad
        mt = posixSecondsToUTCTime (fromIntegral (ft - win32_epoch_adjust) / 10000000)
    putStrLn (path ++ ": " ++ show mt)

win32_epoch_adjust :: DDWORD
win32_epoch_adjust = 116444736000000000

This requires my patches [1] for win32 to compile.


comment:12 Changed 3 years ago by thoughtpolice


Moving to 7.10.1

comment:13 Changed 2 years ago by thoughtpolice

Component: libraries/directoryCore Libraries
Owner: set to ekmett

Moving over to new owning component 'Core Libraries'.

comment:14 Changed 2 years ago by hvr

Cc: core-libraries-committee@… added
Keywords: directory added

comment:15 Changed 2 years ago by redneb

I have a patch for this issue that is basically ready. The only thing preventing me from submitting it is the fact that when getFileAttributesExStandard was added in Win32 the version number was not bumped properly. Because of that, I cannot use the MIN_VERSION_Win32 macro which is needed in order to provide a version of getModificationTime that works with all version of the Win32 package. If you bump one of the first 3 components of the version number of Win32, then I will finish and submit my patch. See also [1].


comment:16 Changed 2 years ago by ekmett

We should be able to make that happen. We'll need to reach out to Bryan O'Sullivan to get the version bump done; Win32 isn't under core libraries control.

comment:17 Changed 2 years ago by redneb

I just posted a pull request for that on github:

comment:18 Changed 2 years ago by thoughtpolice


Moving to 7.12.1 milestone; if you feel this is an error and should be addressed sooner, please move it back to the 7.10.1 milestone.

comment:19 Changed 2 years ago by redneb

The pull request has been merged so I think this bug can be closed now.

comment:20 Changed 2 years ago by hvr

Resolution: fixed
Status: newclosed
Note: See TracTickets for help on using tickets.