Skip to content

Performance improvements for Calendar.RecurrenceRule #981

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
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions Benchmarks/Benchmarks/Internationalization/BenchmarkCalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ let benchmarks = {
let thanksgivingComponents = DateComponents(month: 11, weekday: 5, weekdayOrdinal: 4)
let cal = Calendar(identifier: .gregorian)
let currentCalendar = Calendar.current
let thanksgivingStart = Date(timeIntervalSinceReferenceDate: 496359355.795410) //2016-09-23T14:35:55-0700
let thanksgivingStart = Date(timeIntervalSince1970: 1474666555.0) //2016-09-23T14:35:55-0700

Benchmark("nextThousandThursdaysInTheFourthWeekOfNovember") { benchmark in
// This benchmark used to be nextThousandThanksgivings, but the name was deceiving since it does not compute the next thousand thanksgivings
Expand All @@ -54,7 +54,7 @@ let benchmarks = {
}

// Only available in Swift 6 for non-Darwin platforms, macOS 15 for Darwin
#if swift(>=6.0)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need Swift 6 mode to run the benchmarks, just a Swift 6 compiler

#if compiler(>=6.0)
if #available(macOS 15, *) {
Benchmark("nextThousandThanksgivingsSequence") { benchmark in
var count = 1000
Expand All @@ -66,7 +66,7 @@ let benchmarks = {
}
}

Benchmark("nextThousandThanksgivingsUsingRecurrenceRule") { benchmark in
Benchmark("RecurrenceRuleThanksgivings") { benchmark in
var rule = Calendar.RecurrenceRule(calendar: cal, frequency: .yearly, end: .afterOccurrences(1000))
rule.months = [11]
rule.weekdays = [.nth(4, .thursday)]
Expand All @@ -77,6 +77,43 @@ let benchmarks = {
}
assert(count == 1000)
}
Benchmark("RecurrenceRuleThanksgivingMeals") { benchmark in
var rule = Calendar.RecurrenceRule(calendar: cal, frequency: .yearly, end: .afterOccurrences(1000))
rule.months = [11]
rule.weekdays = [.nth(4, .thursday)]
rule.hours = [14, 18]
rule.matchingPolicy = .nextTime
for date in rule.recurrences(of: thanksgivingStart) {
Benchmark.blackHole(date)
}
}
Benchmark("RecurrenceRuleLaborDay") { benchmark in
var rule = Calendar.RecurrenceRule(calendar: cal, frequency: .yearly, end: .afterOccurrences(1000))
rule.months = [9]
rule.weekdays = [.nth(1, .monday)]
rule.matchingPolicy = .nextTime
for date in rule.recurrences(of: thanksgivingStart) {
Benchmark.blackHole(date)
}
}
Benchmark("RecurrenceRuleBikeParties") { benchmark in
var rule = Calendar.RecurrenceRule(calendar: cal, frequency: .monthly, end: .afterOccurrences(1000))
rule.weekdays = [.nth(1, .friday), .nth(-1, .friday)]
rule.matchingPolicy = .nextTime
for date in rule.recurrences(of: thanksgivingStart) {
Benchmark.blackHole(date)
}
}
Benchmark("RecurrenceRuleDailyWithTimes") { benchmark in
var rule = Calendar.RecurrenceRule(calendar: cal, frequency: .daily, end: .afterOccurrences(1000))
rule.hours = [9, 10]
rule.minutes = [0, 30]
rule.weekdays = [.every(.monday), .every(.tuesday), .every(.wednesday)]
rule.matchingPolicy = .nextTime
for date in rule.recurrences(of: thanksgivingStart) {
Benchmark.blackHole(date)
}
}
} // #available(macOS 15, *)
#endif // swift(>=6.0)

Expand All @@ -93,7 +130,7 @@ let benchmarks = {

// MARK: - Allocations

let reference = Date(timeIntervalSinceReferenceDate: 496359355.795410) //2016-09-23T14:35:55-0700
let reference = Date(timeIntervalSince1970: 1474666555.0) //2016-09-23T14:35:55-0700

let allocationsConfiguration = Benchmark.Configuration(
metrics: [.cpuTotal, .mallocCountTotal, .peakMemoryResident, .throughput],
Expand Down
62 changes: 37 additions & 25 deletions Sources/FoundationEssentials/Calendar/Calendar_Enumerate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ extension Calendar {
}

internal func _enumerateDates(startingAfter start: Date,
previouslyReturnedMatchDate: Date? = nil,
matching matchingComponents: DateComponents,
matchingPolicy: MatchingPolicy,
repeatedTimePolicy: RepeatedTimePolicy,
Expand All @@ -470,7 +471,7 @@ extension Calendar {
let STOP_EXHAUSTIVE_SEARCH_AFTER_MAX_ITERATIONS = 100

var searchingDate = start
var previouslyReturnedMatchDate: Date? = nil
var previouslyReturnedMatchDate = previouslyReturnedMatchDate
var iterations = -1

repeat {
Expand Down Expand Up @@ -511,14 +512,8 @@ extension Calendar {
matchingPolicy: MatchingPolicy,
repeatedTimePolicy: RepeatedTimePolicy,
direction: SearchDirection,
inSearchingDate: Date,
inSearchingDate searchingDate: Date,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function gets split into two, with the latter _adjustDates being exposed for recurrence enumeration.

previouslyReturnedMatchDate: Date?) throws -> SearchStepResult {
var exactMatch = true
var isLeapDay = false
var searchingDate = inSearchingDate

// NOTE: Several comments reference "isForwardDST" as a way to relate areas in forward DST handling.
var isForwardDST = false

// Step A: Call helper method that does the searching

Expand All @@ -539,8 +534,25 @@ extension Calendar {
// TODO: Check if returning the same searchingDate has any purpose
return SearchStepResult(result: nil, newSearchDate: searchingDate)
}

return try _adjustedDate(unadjustedMatchDate, startingAfter: start, matching: matchingComponents, adjustedMatchingComponents: compsToMatch , matchingPolicy: matchingPolicy, repeatedTimePolicy: repeatedTimePolicy, direction: direction, inSearchingDate: searchingDate, previouslyReturnedMatchDate: previouslyReturnedMatchDate)
}

internal func _adjustedDate(_ unadjustedMatchDate: Date, startingAfter start: Date,
allowStartDate: Bool = false,
matching matchingComponents: DateComponents,
adjustedMatchingComponents compsToMatch: DateComponents,
matchingPolicy: MatchingPolicy,
repeatedTimePolicy: RepeatedTimePolicy,
direction: SearchDirection,
inSearchingDate: Date,
previouslyReturnedMatchDate: Date?) throws -> SearchStepResult {
var exactMatch = true
var isLeapDay = false
var searchingDate = inSearchingDate

// Step B: Couldn't find matching date with a quick and dirty search in the current era, year, etc. Now try in the near future/past and make adjustments for leap situations and non-existent dates
// NOTE: Several comments reference "isForwardDST" as a way to relate areas in forward DST handling.
var isForwardDST = false

// matchDate may be nil, which indicates a need to keep iterating
// Step C: Validate what we found and then run block. Then prepare the search date for the next round of the loop
Expand Down Expand Up @@ -624,7 +636,7 @@ extension Calendar {
}

// If we get a result that is exactly the same as the start date, skip.
if order == .orderedSame {
if !allowStartDate, order == .orderedSame {
return SearchStepResult(result: nil, newSearchDate: searchingDate)
}

Expand Down Expand Up @@ -1393,7 +1405,7 @@ extension Calendar {
}
}

private func dateAfterMatchingEra(startingAt startDate: Date, components: DateComponents, direction: SearchDirection, matchedEra: inout Bool) -> Date? {
internal func dateAfterMatchingEra(startingAt startDate: Date, components: DateComponents, direction: SearchDirection, matchedEra: inout Bool) -> Date? {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some dateAfterMatching* functions are exposed for recurrence enumeration as well.

guard let era = components.era else {
// Nothing to do
return nil
Expand Down Expand Up @@ -1431,7 +1443,7 @@ extension Calendar {
}
}

private func dateAfterMatchingYear(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingYear(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let year = components.year else {
// Nothing to do
return nil
Expand Down Expand Up @@ -1466,7 +1478,7 @@ extension Calendar {
}
}

private func dateAfterMatchingYearForWeekOfYear(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingYearForWeekOfYear(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let yearForWeekOfYear = components.yearForWeekOfYear else {
// Nothing to do
return nil
Expand Down Expand Up @@ -1494,7 +1506,7 @@ extension Calendar {
}
}

private func dateAfterMatchingQuarter(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingQuarter(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let quarter = components.quarter else { return nil }

// Get the beginning of the year we need
Expand Down Expand Up @@ -1530,7 +1542,7 @@ extension Calendar {
}
}

private func dateAfterMatchingWeekOfYear(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingWeekOfYear(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let weekOfYear = components.weekOfYear else {
// Nothing to do
return nil
Expand Down Expand Up @@ -1569,7 +1581,7 @@ extension Calendar {
}

@available(FoundationPreview 0.4, *)
private func dateAfterMatchingDayOfYear(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingDayOfYear(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let dayOfYear = components.dayOfYear else {
// Nothing to do
return nil
Expand Down Expand Up @@ -1606,7 +1618,7 @@ extension Calendar {
return result
}

private func dateAfterMatchingMonth(startingAt startDate: Date, components: DateComponents, direction: SearchDirection, strictMatching: Bool) throws -> Date? {
internal func dateAfterMatchingMonth(startingAt startDate: Date, components: DateComponents, direction: SearchDirection, strictMatching: Bool) throws -> Date? {
guard let month = components.month else {
// Nothing to do
return nil
Expand Down Expand Up @@ -1695,7 +1707,7 @@ extension Calendar {
return result
}

private func dateAfterMatchingWeekOfMonth(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingWeekOfMonth(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let weekOfMonth = components.weekOfMonth else {
// Nothing to do
return nil
Expand Down Expand Up @@ -1784,7 +1796,7 @@ extension Calendar {
return result
}

private func dateAfterMatchingWeekdayOrdinal(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingWeekdayOrdinal(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let weekdayOrdinal = components.weekdayOrdinal else {
// Nothing to do
return nil
Expand Down Expand Up @@ -1887,7 +1899,7 @@ extension Calendar {
return result
}

private func dateAfterMatchingWeekday(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingWeekday(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let weekday = components.weekday else {
// Nothing to do
return nil
Expand Down Expand Up @@ -1944,7 +1956,7 @@ extension Calendar {
return result
}

private func dateAfterMatchingDay(startingAt startDate: Date, originalStartDate: Date, components comps: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingDay(startingAt startDate: Date, originalStartDate: Date, components comps: DateComponents, direction: SearchDirection) throws -> Date? {
guard let day = comps.day else {
// Nothing to do
return nil
Expand Down Expand Up @@ -2045,7 +2057,7 @@ extension Calendar {
return result
}

private func dateAfterMatchingHour(startingAt startDate: Date, originalStartDate: Date, components: DateComponents, direction: SearchDirection, findLastMatch: Bool, isStrictMatching: Bool, matchingPolicy: MatchingPolicy) throws -> Date? {
internal func dateAfterMatchingHour(startingAt startDate: Date, originalStartDate: Date, components: DateComponents, direction: SearchDirection, findLastMatch: Bool, isStrictMatching: Bool, matchingPolicy: MatchingPolicy) throws -> Date? {
guard let hour = components.hour else {
// Nothing to do
return nil
Expand Down Expand Up @@ -2182,7 +2194,7 @@ extension Calendar {
return result
}

private func dateAfterMatchingMinute(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingMinute(startingAt: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let minute = components.minute else {
// Nothing to do
return nil
Expand Down Expand Up @@ -2211,7 +2223,7 @@ extension Calendar {
return result
}

private func dateAfterMatchingSecond(startingAt startDate: Date, originalStartDate: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
internal func dateAfterMatchingSecond(startingAt startDate: Date, originalStartDate: Date, components: DateComponents, direction: SearchDirection) throws -> Date? {
guard let second = components.second else {
// Nothing to do
return nil
Expand Down Expand Up @@ -2277,7 +2289,7 @@ extension Calendar {
return result
}

private func dateAfterMatchingNanosecond(startingAt: Date, components: DateComponents, direction: SearchDirection) -> Date? {
internal func dateAfterMatchingNanosecond(startingAt: Date, components: DateComponents, direction: SearchDirection) -> Date? {
guard let nanosecond = components.nanosecond else {
// Nothing to do
return nil
Expand Down
Loading