Skip to content

Revert "Adopt utimensat for setting file modification dates (#1324)" #1379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 26, 2025

Conversation

jmschonfeld
Copy link
Contributor

While the utimensat has the ability to set nanosecond-precision dates on files (when supported by the file system), unfortunately the current call site of it loses precision for some input dates (represented as Doubles). Unfortunately I haven't found a good way to convert the Double date representation to the second and nanosecond components without losing precision, and this precision causes issues with some clients expecting dates to roundtrip with full fidelity (even though values like Date.now may only support 1000ns-level precision, the imprecision when calculating nanosecond values can cause round tripping a date to return a different value than the one set).

Resolves rdar://153731450

@jrflat
Copy link
Contributor

jrflat commented Jun 25, 2025

I think the issue stems from losing 1 bit of precision when we go from a Date.now, whose TimeInterval inherently has ~119ns precision to the reference date, to its .timeIntervalSince1970, which only has ~238ns precision due to the larger interval.

If we instead do 2 modf calls,

let (isecs1, fsecs1) = modf(di.timeIntervalSinceReferenceDate)
let (isecs2, fsecs2) = modf(Date.timeIntervalBetween1970AndReferenceDate)

then add the fsecs together, we then have enough nanosecond precision to reconstruct the correct Date with its .init(timeIntervalSinceReferenceDate:) initializer, but notably not its .init(timeIntervalSince1970:) initializer. (We need to do the isecs - Date.timeIntervalBetween1970AndReferenceDate subtraction first to maintain the precision.) Since this is how the Date.init(seconds:nanoSeconds:) used by FileManager works, we should be all good here.

Here's some sample code explaining the issue/solution:

extension Date {
    init(seconds: TimeInterval, nanoSeconds: TimeInterval) {
        self.init(timeIntervalSinceReferenceDate: seconds - Date.timeIntervalBetween1970AndReferenceDate + nanoSeconds / 1_000_000_000.0)
    }
}

var n = 0
var fails = 0
while n < 100000 {
    n += 1
    let di = Date.now
    let (isecs1, fsecs1) = modf(di.timeIntervalSinceReferenceDate)
    let (isecs2, fsecs2) = modf(Date.timeIntervalBetween1970AndReferenceDate)
    var isecs = isecs1 + isecs2
    var nsecs = Int(exactly: round(fsecs1 * 1_000_000_000.0))! + Int(exactly: round(fsecs2 * 1_000_000_000.0))!
    if nsecs >= 1_000_000_000 {
        isecs += 1
        nsecs -= 1_000_000_000
    }

//    // NOTE: Below doesn't work
//    let (isecs, fsecs) = modf(di.timeIntervalSince1970)
//    let nsecs = Int(exactly: round(fsecs * 1_000_000_000.0))!

//    // NOTE: Below does work, at the cost of only microsecond precision
//    let (isecs, fsecs) = modf(di.timeIntervalSince1970)
//    let nsecs = Int(exactly: round(fsecs * 1_000_000.0))! * 1000
    
    let df = Date(seconds: isecs, nanoSeconds: TimeInterval(nsecs))
    if df != di {
        fails += 1
    }
}
print("Failure rate: \(fails)/\(n) = \(Double(fails) / Double(n))\n")

In the failure cases, we fail ~50% of the time, which makes sense due to the 1 bit of precision lost.

@jrflat
Copy link
Contributor

jrflat commented Jun 25, 2025

As a clarification, I think rounding to the nearest microsecond is actually only guaranteed to work for Dates that are explicitly generated with microsecond precision (like Date.now). Otherwise, the round trip could fail intermittently for any Date less than 2^33 sec ~ 272 years from the reference date.

On the other hand, the double modf solution should maintain precision such that any Date further than 2^23 sec ~ 97 days from the reference date can be round-tripped with full fidelity.

@jmschonfeld
Copy link
Contributor Author

@swift-ci please test

@jmschonfeld
Copy link
Contributor Author

Discussed with Jonathan offline - the solution above is a more robust solution for TimeInterval values that can be precisely represented already, but TimeInterval (Double) values unfortunately already lose precision when stored for some values which is a limitation of Date currently and not something we can easily solve on the FileManager side. I'm going to go ahead with this revert for now, and we can revisit this when Date can precisely represent nano-second precision values that can be passed to this FileManager API

@itingliu
Copy link
Contributor

@stephentyrone would be interested in this use case too :D

@jmschonfeld jmschonfeld merged commit 04493a4 into swiftlang:main Jun 26, 2025
16 checks passed
@jmschonfeld jmschonfeld deleted the revert-fmod-nano branch June 26, 2025 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants