Skip to content

Commit

Permalink
VIT-7076: Refactor how resource priority is modelled (#225)
Browse files Browse the repository at this point in the history
  • Loading branch information
andersio authored Aug 2, 2024
1 parent 16546bd commit 4f1d413
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@ public enum VitalResource: Equatable, Hashable, Codable {

@_spi(VitalSDKInternals)
public init?(_ backfillType: BackfillType) {
guard let match = VitalResource.all.first(where: { $0.resourceToBackfillType() == backfillType })
guard let match = VitalResource.all.first(where: { $0.backfillType == backfillType })
else { return nil }

self = match
}

@_spi(VitalSDKInternals)
public func resourceToBackfillType() -> BackfillType {
public var priority: Int {
switch self {
case .activity, .body, .workout, .menstrualCycle, .profile:
return 0
case .sleep, .individual(.vo2Max), .vitals(.bloodOxygen), .vitals(.bloodPressure),
.vitals(.glucose), .vitals(.heartRateVariability),
.nutrition(.water), .nutrition(.caffeine),
.vitals(.mindfulSession), .vitals(.temperature), .vitals(.respiratoryRate):
return 1
case .individual(.distanceWalkingRunning), .individual(.steps), .individual(.floorsClimbed):
return 2
case .vitals(.heartRate), .individual(.activeEnergyBurned), .individual(.basalEnergyBurned):
return 3
case .individual(.exerciseTime), .individual(.weight), .individual(.bodyFat):
return Int.max
}
}

@_spi(VitalSDKInternals)
public var backfillType: BackfillType {
switch self {
case .activity, .individual(.exerciseTime):
return BackfillType.activity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public enum SyncContextTag: Int, Codable {
}

struct BackgroundDeliveryPayload: CustomStringConvertible {
let resources: Set<RemappedVitalResource>
let resources: [RemappedVitalResource]
let completion: (Completion) -> Void
let tags: Set<SyncContextTag>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal struct LocalSyncState: Codable {
let reportingInterval: Double?

func historicalStartDate(for resource: VitalResource) -> Date {
let backfillType = resource.resourceToBackfillType();
let backfillType = resource.backfillType
let daysToBackfill = teamDataPullPreferences?.backfillTypeOverrides?[backfillType]?.historicalDaysToPull ?? teamDataPullPreferences?.historicalDaysToPull;

let calculatedDate = Date.dateAgo(historicalStageAnchor, days: daysToBackfill ?? defaultDaysToBackfill)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ final class SyncProgressStore {
var augmentedTags = id.tags
insertAppStateTags(&augmentedTags)

mutate(CollectionOfOne(id.resource.resourceToBackfillType())) {
mutate(CollectionOfOne(id.resource.backfillType)) {
let now = Date()

let latestSync = $0.syncs.last
Expand Down Expand Up @@ -235,13 +235,13 @@ final class SyncProgressStore {
func recordAsk(_ resources: some Sequence<RemappedVitalResource>) {
let date = Date()

mutate(resources.map { $0.wrapped.resourceToBackfillType() }) {
mutate(resources.map { $0.wrapped.backfillType }) {
$0.firstAsked = date
}
}

func recordSystem(_ resources: some Sequence<RemappedVitalResource>, _ eventType: SyncProgress.SystemEventType) {
mutate(resources.map { $0.wrapped.resourceToBackfillType() }) {
mutate(resources.map { $0.wrapped.backfillType }) {
let now = Date()

// Capture this new event if the event type is different or 2 seconds have elapsed.
Expand Down
106 changes: 46 additions & 60 deletions Sources/VitalHealthKit/HealthKit/VitalHealthKitClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ extension VitalHealthKitClient {

let progress = SyncProgressStore.shared.get()
let unnotifiedResources = state.activeResources.filter { resource in
guard let resourceProgress = progress.backfillTypes[resource.wrapped.resourceToBackfillType()] else {
guard let resourceProgress = progress.backfillTypes[resource.wrapped.backfillType] else {
// Doesn't even show up in `SyncProgress.resources`
return true
}
Expand Down Expand Up @@ -490,28 +490,27 @@ extension VitalHealthKitClient {

let matches = Set(filteredSampleTypes.flatMap(VitalHealthKitStore.sampleTypeToVitalResource(type:)))
let remapped = Set(matches.map(VitalHealthKitStore.remapResource))
.sorted(by: { $0.wrapped.priority < $1.wrapped.priority })

self.scope.task(priority: .userInitiated) { @MainActor in
let isForeground = UIApplication.shared.applicationState != .background

let payload = BackgroundDeliveryPayload(
resources: remapped,
completion: { completion in
if completion == .completed {
handler()
}
},
tags: [.healthKit]
)
VitalLogger.healthKit.info("notified: \(payload)", source: "HealthKit")

continuation.yield(payload)

SyncProgressStore.shared.recordSystem(
remapped,
isForeground ? .healthKitCalloutForeground : .healthKitCalloutBackground
)
}
let payload = BackgroundDeliveryPayload(
resources: remapped,
completion: { completion in
if completion == .completed {
handler()
}
},
tags: [.healthKit]
)
VitalLogger.healthKit.info("notified: \(payload)", source: "HealthKit")

continuation.yield(payload)

SyncProgressStore.shared.recordSystem(
remapped,
AppStateTracker.shared.state.status == .background
? .healthKitCalloutBackground
: .healthKitCalloutForeground
)
}
}

Expand Down Expand Up @@ -550,30 +549,29 @@ extension VitalHealthKitClient {

let matches = Set(VitalHealthKitStore.sampleTypeToVitalResource(type: sampleType))
let remapped = Set(matches.map(VitalHealthKitStore.remapResource))
.sorted(by: { $0.wrapped.priority < $1.wrapped.priority })

VitalLogger.healthKit.info("notified: \(remapped.map(\.wrapped.logDescription).joined(separator: ","))", source: "HealthKit")

self.scope.task(priority: .userInitiated) { @MainActor in
let isForeground = UIApplication.shared.applicationState != .background

let payload = BackgroundDeliveryPayload(
resources: remapped,
completion: { completion in
if completion == .completed {
handler()
}
},
tags: [.healthKit]
)
VitalLogger.healthKit.info("notified: \(payload)", source: "HealthKit")

continuation.yield(payload)

SyncProgressStore.shared.recordSystem(
remapped,
isForeground ? .healthKitCalloutForeground : .healthKitCalloutBackground
)
}
let payload = BackgroundDeliveryPayload(
resources: remapped,
completion: { completion in
if completion == .completed {
handler()
}
},
tags: [.healthKit]
)
VitalLogger.healthKit.info("notified: \(payload)", source: "HealthKit")

continuation.yield(payload)

SyncProgressStore.shared.recordSystem(
remapped,
AppStateTracker.shared.state.status == .background
? .healthKitCalloutBackground
: .healthKitCalloutForeground
)
}

queries.append(query)
Expand Down Expand Up @@ -729,7 +727,7 @@ extension VitalHealthKitClient {
UserSDKHistoricalStageBeginBody(
rangeStart: query.lowerBound,
rangeEnd: query.upperBound,
backfillType: resource.wrapped.resourceToBackfillType()
backfillType: resource.wrapped.backfillType
)
)
}
Expand All @@ -738,27 +736,15 @@ extension VitalHealthKitClient {
}

private func prioritizeSync(_ remappedResource: RemappedVitalResource, _ tags: Set<SyncContextTag>) -> Bool {
let waitForHistoricalDone: Set<VitalResource>

switch remappedResource.wrapped {
case .activity, .workout, .sleep, .menstrualCycle:
// Always sync first
return true

case .individual(.activeEnergyBurned), .individual(.basalEnergyBurned), .vitals(.heartRate):
// These heavy hitters wait until steps are done.
waitForHistoricalDone = [.individual(.steps)]

default:
// Everything else must sync only after the summaries historical are done
waitForHistoricalDone = [.activity, .workout, .sleep, .menstrualCycle]
}
let priority = remappedResource.wrapped.priority
let prerequisites = VitalResource.all.filter { $0.priority < priority }

let state = authorizationState(store: self.store)
let resourcesToCheck = state.activeResources
.intersection(waitForHistoricalDone.map(VitalHealthKitStore.remapResource))
.intersection(prerequisites.map(VitalHealthKitStore.remapResource))

// Deprioritized until all prioritized resources have finished their historical stage.
// If resourcesToCheck is empty, this returns true, i.e., no waiting.
return resourcesToCheck.allSatisfy(storage.historicalStageDone(for:))
}

Expand Down

0 comments on commit 4f1d413

Please sign in to comment.