Skip to content

Sorted timestamps optimization #507

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

Closed
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
85 changes: 76 additions & 9 deletions contracts/FlowCallbackScheduler.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,67 @@ access(all) contract FlowCallbackScheduler {
}
}

/// SortedTimestamps maintains a sorted array of timestamps for efficient processing
Copy link
Member

Choose a reason for hiding this comment

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

Most of this is pretty generic. I wish we could just put this in an ArrayUtils contract that is separate from this so we don't have all this sorting stuff cluttering up this contract, but that might not really be worth putting together

/// It encapsulates all operations related to maintaining and querying sorted timestamps
access(all) struct SortedTimestamps {
/// Internal sorted array of timestamps
access(self) var timestamps: [UFix64]
access(self) let lowPriorityScheduledTimestamp: UFix64

access(all) init() {
self.timestamps = []
self.lowPriorityScheduledTimestamp = 0.0
}

/// Add a timestamp to the sorted array maintaining sorted order
access(all) fun add(timestamp: UFix64) {
if timestamp == self.lowPriorityScheduledTimestamp {
return
}

var insertIndex = 0
for i, ts in self.timestamps {
if timestamp < ts {
insertIndex = i
break
}
insertIndex = i + 1
}
self.timestamps.insert(at: insertIndex, timestamp)
Copy link
Member

Choose a reason for hiding this comment

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

wow, I didn't even realize this was possible. This is great!

}

/// Remove a timestamp from the sorted array
access(all) fun remove(timestamp: UFix64) {
if timestamp == self.lowPriorityScheduledTimestamp {
return
}

let index = self.timestamps.firstIndex(of: timestamp)
if index != nil {
self.timestamps.remove(at: index!)
}
}

/// Get all timestamps that are in the past (less than or equal to current timestamp)
access(all) fun past(current: UFix64): [UFix64] {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
access(all) fun past(current: UFix64): [UFix64] {
access(all) fun getPast(current: UFix64): [UFix64] {

let pastTimestamps: [UFix64] = []
for timestamp in self.timestamps {
if timestamp <= current {
pastTimestamps.append(timestamp)
} else {
break // No need to check further since array is sorted
}
}
return pastTimestamps
}

/// Check if there are any timestamps that need processing
/// Returns true if processing is needed, false for early exit
access(all) fun check(current: UFix64): Bool {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
access(all) fun check(current: UFix64): Bool {
access(all) fun checkIfTimestampsNeedProcessing(current: UFix64): Bool {

return self.timestamps.length > 0 && self.timestamps[0] <= current
}
}

/// Resources

/// Shared scheduler is a resource that is used as a singleton in the scheduler contract and contains
Expand All @@ -333,6 +394,10 @@ access(all) contract FlowCallbackScheduler {
/// so we use this special value
access(contract) let lowPriorityScheduledTimestamp: UFix64

/// sorted timestamps manager for efficient processing
/// excludes lowPriorityScheduledTimestamp
access(contract) var sortedTimestamps: SortedTimestamps

/// Struct that contains all the configuration details for the callback scheduler protocol
/// Can be updated by the owner of the contract
access(contract) var configurationDetails: {SchedulerConfig}
Expand All @@ -353,6 +418,7 @@ access(all) contract FlowCallbackScheduler {
self.slotQueue = {
self.lowPriorityScheduledTimestamp: {}
}
self.sortedTimestamps = SortedTimestamps()

/* Default slot efforts and limits look like this:

Expand Down Expand Up @@ -692,6 +758,8 @@ access(all) contract FlowCallbackScheduler {
Priority.Medium: 0,
Priority.Low: 0
}

self.sortedTimestamps.add(timestamp: slot)
}

// Add this callback id to the slot
Expand Down Expand Up @@ -726,16 +794,13 @@ access(all) contract FlowCallbackScheduler {

let currentTimestamp = getCurrentBlock().timestamp

// find all timestamps that are in the past
let pastTimestamp = view fun (timestamp: UFix64): Bool {
// don't add low priority timestamp to the past timestamps
if timestamp == lowPriorityTimestamp {
return false
}

return timestamp <= currentTimestamp
// Early exit if no timestamps need processing
if !self.sortedTimestamps.check(current: currentTimestamp) {
return
}
let pastTimestamps = self.slotQueue.keys.filter(pastTimestamp)

// Collect past timestamps efficiently from sorted array
let pastTimestamps = self.sortedTimestamps.past(current: currentTimestamp)

// process all callbacks from timestamps in the past
// and add low priority callbacks to the timestamp if there is space
Expand Down Expand Up @@ -904,6 +969,8 @@ access(all) contract FlowCallbackScheduler {
if callbackQueue.keys.length == 0 {
self.slotQueue.remove(key: slot)
self.slotUsedEffort.remove(key: slot)

self.sortedTimestamps.remove(timestamp: slot)
}
}
}
Expand Down
Loading
Loading