Skip to content

Commit 7fdc5ed

Browse files
authored
Merge pull request insidegui#696 from insidegui/ah/improve-update-performance
Optimize schedule grouping to avoid excessive CPU and memory usage on start up
2 parents 78b29db + 4c38de2 commit 7fdc5ed

File tree

3 files changed

+97
-61
lines changed

3 files changed

+97
-61
lines changed

Packages/ConfCore/ConfCore/Session.swift

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -135,31 +135,38 @@ public class Session: Object, Decodable {
135135
self.assets.append(objectsIn: assets)
136136

137137
other.related.forEach { newRelated in
138-
let effectiveRelated: RelatedResource
139-
140-
if let existingResource = realm.object(ofType: RelatedResource.self, forPrimaryKey: newRelated.identifier) {
141-
effectiveRelated = existingResource
142-
} else {
143-
effectiveRelated = newRelated
144-
}
145-
146-
guard !related.contains(where: { $0.identifier == effectiveRelated.identifier }) else { return }
147-
related.append(effectiveRelated)
138+
realm.add(newRelated, update: .all)
139+
// let effectiveRelated: RelatedResource
140+
//
141+
// if let existingResource = realm.object(ofType: RelatedResource.self, forPrimaryKey: newRelated.identifier) {
142+
// effectiveRelated = existingResource
143+
// } else {
144+
// effectiveRelated = newRelated
145+
// }
146+
//
147+
// guard !related.contains(where: { $0.identifier == effectiveRelated.identifier }) else { return }
148+
// related.append(effectiveRelated)
148149
}
150+
related.removeAll()
151+
related.append(objectsIn: other.related)
149152

150153
other.focuses.forEach { newFocus in
151-
let effectiveFocus: Focus
152-
153-
if let existingFocus = realm.object(ofType: Focus.self, forPrimaryKey: newFocus.name) {
154-
effectiveFocus = existingFocus
155-
} else {
156-
effectiveFocus = newFocus
157-
}
158-
159-
guard !focuses.contains(where: { $0.name == effectiveFocus.name }) else { return }
160-
161-
focuses.append(effectiveFocus)
154+
realm.add(newFocus, update: .all)
155+
// let effectiveFocus: Focus
156+
//
157+
// if let existingFocus = realm.object(ofType: Focus.self, forPrimaryKey: newFocus.name) {
158+
// effectiveFocus = existingFocus
159+
// } else {
160+
// effectiveFocus = newFocus
161+
// }
162+
//
163+
// guard !focuses.contains(where: { $0.name == effectiveFocus.name }) else { return }
164+
//
165+
// focuses.append(effectiveFocus)
162166
}
167+
168+
focuses.removeAll()
169+
focuses.append(objectsIn: other.focuses)
163170
}
164171

165172
// MARK: - Decodable

Packages/ConfCore/ConfCore/SessionInstance.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,14 @@ public class SessionInstance: Object, ConditionallyDecodable {
109109
eventIdentifier = other.eventIdentifier
110110
calendarEventIdentifier = other.calendarEventIdentifier
111111

112+
// This requires a ton of work because there are so many session instances
113+
// And we
112114
if let otherSession = other.session, let session = session {
113115
session.merge(with: otherSession, in: realm)
114116
}
115117

118+
// If we collected all the keywords up front and stored them, it'd be faster than
119+
// querying against individual sessions' keywords because you end up duplicating a lot of work
116120
let otherKeywords = other.keywords.map { newKeyword -> (Keyword) in
117121
if newKeyword.realm == nil,
118122
let existingKeyword = realm.object(ofType: Keyword.self, forPrimaryKey: newKeyword.name) {

Packages/ConfCore/ConfCore/Storage.swift

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ public final class Storage {
9595
return
9696
}
9797

98-
performSerializedBackgroundWrite(writeBlock: { backgroundRealm in
98+
performSerializedBackgroundWrite(disableAutorefresh: true, completionBlock: completion) { backgroundRealm in
99+
var time = Date()
100+
print("Starting sessions: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
99101
contentsResponse.sessions.forEach { newSession in
100102
// Replace any "unknown" resources with their full data
101103
newSession.related.filter({$0.type == RelatedResourceType.unknown.rawValue}).forEach { unknownResource in
@@ -111,7 +113,11 @@ public final class Storage {
111113
backgroundRealm.add(newSession, update: .all)
112114
}
113115
}
116+
print("Ending sessions: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
114117

118+
time = Date()
119+
// TODO: Takes 8+ seconds, several notable opportunities to optimize storage accesses
120+
print("Starting session instances: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
115121
// Merge existing instance data, preserving user-defined data
116122
contentsResponse.instances.forEach { newInstance in
117123
if let existingInstance = backgroundRealm.object(ofType: SessionInstance.self, forPrimaryKey: newInstance.identifier) {
@@ -128,13 +134,19 @@ public final class Storage {
128134
backgroundRealm.add(newInstance, update: .all)
129135
}
130136
}
137+
print("Ending session instances: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
131138

132139
// Save everything
140+
time = Date()
141+
print("Starting save everything: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
133142
backgroundRealm.add(contentsResponse.rooms, update: .all)
134143
backgroundRealm.add(contentsResponse.tracks, update: .all)
135144
backgroundRealm.add(contentsResponse.events, update: .all)
145+
print("Ending save everything: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
136146

137147
// add instances to rooms
148+
time = Date()
149+
print("Starting add instances to room: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
138150
backgroundRealm.objects(Room.self).forEach { room in
139151
let instances = backgroundRealm.objects(SessionInstance.self).filter("roomIdentifier == %@", room.identifier)
140152

@@ -143,8 +155,12 @@ public final class Storage {
143155
room.instances.removeAll()
144156
room.instances.append(objectsIn: instances)
145157
}
158+
print("Ending add instances to room: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
146159

147160
// add instances and sessions to events
161+
// TODO: takes 0.4 seconds, could these List's become LinkingObjects so we don't have to store them and then pull them back out?
162+
time = Date()
163+
print("Starting add instances and sessions to events: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
148164
backgroundRealm.objects(Event.self).forEach { event in
149165
let instances = backgroundRealm.objects(SessionInstance.self).filter("eventIdentifier == %@", event.identifier)
150166
let sessions = backgroundRealm.objects(Session.self).filter("eventIdentifier == %@", event.identifier)
@@ -155,8 +171,12 @@ public final class Storage {
155171
event.sessions.removeAll()
156172
event.sessions.append(objectsIn: sessions)
157173
}
174+
print("Ending add instances and sessions to events: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
158175

159176
// add instances and sessions to tracks
177+
time = Date()
178+
print("Starting add instances and sessions to tracks: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
179+
// TODO: takes 1.5 seconds, could these List's become LinkingObjects so we don't have to store them and then pull them back out?
160180
backgroundRealm.objects(Track.self).forEach { track in
161181
let instances = backgroundRealm.objects(SessionInstance.self).filter("trackIdentifier == %@", track.identifier)
162182
let sessions = backgroundRealm.objects(Session.self).filter("trackIdentifier == %@", track.identifier)
@@ -173,54 +193,59 @@ public final class Storage {
173193
instance.session?.trackName = track.name
174194
}
175195
}
196+
print("Ending add instances and sessions to tracks: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
176197

177198
// add live video assets to sessions
199+
time = Date()
200+
print("Starting add live video assets to sessions: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
178201
backgroundRealm.objects(SessionAsset.self).filter("rawAssetType == %@", SessionAssetType.liveStreamVideo.rawValue).forEach { liveAsset in
179202
if let session = backgroundRealm.objects(Session.self).filter("ANY event.year == %d AND number == %@", liveAsset.year, liveAsset.sessionId).first {
180203
if !session.assets.contains(liveAsset) {
181204
session.assets.append(liveAsset)
182205
}
183206
}
184207
}
208+
print("Ending add live video assets: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
185209

186210
// Associate session resources with Session objects in database
211+
time = Date()
212+
print("Starting Associate session resources with Session objects in database: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
187213
backgroundRealm.objects(RelatedResource.self).filter("type == %@", RelatedResourceType.session.rawValue).forEach { resource in
188214
if let session = backgroundRealm.object(ofType: Session.self, forPrimaryKey: resource.identifier) {
189215
resource.session = session
190216
}
191217
}
218+
print("Ending Associate session resources: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
192219

193220
// Remove tracks that don't include any future session instances nor any sessions with video/live video
221+
time = Date()
222+
print("Starting Remove tracks that don't include any future session instances nor any sessions with video/live video: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
194223
let emptyTracks = backgroundRealm.objects(Track.self)
195224
.filter("SUBQUERY(sessions, $session, ANY $session.assets.rawAssetType = %@ OR ANY $session.assets.rawAssetType = %@).@count == 0", SessionAssetType.streamingVideo.rawValue, SessionAssetType.liveStreamVideo.rawValue)
196225
backgroundRealm.delete(emptyTracks)
226+
print("Ending Remove tracks: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
197227

198228
// Create schedule view
229+
time = Date()
230+
print("Starting Create schedule view: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
199231
backgroundRealm.delete(backgroundRealm.objects(ScheduleSection.self))
200-
201-
let instances = backgroundRealm.objects(SessionInstance.self).sorted(by: SessionInstance.standardSort)
202-
203-
var previousStartTime: Date?
204-
for instance in instances {
205-
guard instance.startTime != previousStartTime else { continue }
206-
207-
autoreleasepool {
208-
let instancesForSection = instances.filter({ $0.startTime == instance.startTime })
209-
210-
let section = ScheduleSection()
211-
212-
section.representedDate = instance.startTime
213-
section.eventIdentifier = instance.eventIdentifier
214-
section.instances.removeAll()
215-
section.instances.append(objectsIn: instancesForSection)
216-
section.identifier = ScheduleSection.identifierFormatter.string(from: instance.startTime)
217-
218-
backgroundRealm.add(section, update: .all)
219-
220-
previousStartTime = instance.startTime
221-
}
232+
let instances = backgroundRealm.objects(SessionInstance.self)
233+
234+
// Group all instances by common start time
235+
// Technically, a secondary grouping on event should be used, in practice we haven't seen
236+
// separate events that overlap in time. Someday this might hurt
237+
Dictionary(grouping: instances, by: \.startTime).forEach { startTime, instances in
238+
let section = ScheduleSection()
239+
section.representedDate = startTime
240+
section.eventIdentifier = instances[0].eventIdentifier // 0 index ok, Dictionary grouping will never give us an empty array
241+
section.instances.removeAll()
242+
section.instances.append(objectsIn: instances)
243+
section.identifier = ScheduleSection.identifierFormatter.string(from: startTime)
244+
245+
backgroundRealm.add(section, update: .all)
222246
}
223-
}, disableAutorefresh: true, completionBlock: completion)
247+
print("Ending Create schedule view: \((Date().timeIntervalSince1970 - time.timeIntervalSince1970).formatted(.number.precision(.fractionLength(2))))")
248+
}
224249
}
225250

226251
internal func store(liveVideosResult: Result<[SessionAsset], APIError>) {
@@ -271,7 +296,7 @@ public final class Storage {
271296
return
272297
}
273298

274-
performSerializedBackgroundWrite(writeBlock: { backgroundRealm in
299+
performSerializedBackgroundWrite(disableAutorefresh: true, completionBlock: completion) { backgroundRealm in
275300
let existingSections = backgroundRealm.objects(FeaturedSection.self)
276301
for section in existingSections {
277302
section.content.forEach { backgroundRealm.delete($0) }
@@ -287,7 +312,7 @@ public final class Storage {
287312
content.session = backgroundRealm.object(ofType: Session.self, forPrimaryKey: content.sessionId)
288313
}
289314
}
290-
}, disableAutorefresh: true, completionBlock: completion)
315+
}
291316
}
292317

293318
internal func store(configResult: Result<ConfigResponse, APIError>, completion: @escaping (Error?) -> Void) {
@@ -304,20 +329,20 @@ public final class Storage {
304329
return
305330
}
306331

307-
performSerializedBackgroundWrite(writeBlock: { backgroundRealm in
332+
performSerializedBackgroundWrite(disableAutorefresh: false, completionBlock: completion) { backgroundRealm in
308333
// We currently only care about whatever the latest event hero is.
309334
let existingHeroData = backgroundRealm.objects(EventHero.self)
310335
backgroundRealm.delete(existingHeroData)
311-
}, disableAutorefresh: false, completionBlock: completion)
336+
}
312337

313338
guard let hero = response.eventHero else {
314339
os_log("Config response didn't contain an event hero", log: self.log, type: .debug)
315340
return
316341
}
317342

318-
performSerializedBackgroundWrite(writeBlock: { backgroundRealm in
343+
performSerializedBackgroundWrite(disableAutorefresh: false, completionBlock: completion) { backgroundRealm in
319344
backgroundRealm.add(hero, update: .all)
320-
}, disableAutorefresh: false, completionBlock: completion)
345+
}
321346
}
322347

323348
private let serialQueue = DispatchQueue(label: "Database Serial", qos: .userInteractive)
@@ -330,11 +355,11 @@ public final class Storage {
330355
/// - createTransaction: Whether the method should create its own write transaction or use the one already in place
331356
/// - notificationTokensToSkip: An array of `NotificationToken` that should not be notified when the write is committed
332357
/// - completionBlock: A block to be called when the operation is completed (called on the main queue)
333-
internal func performSerializedBackgroundWrite(writeBlock: @escaping (Realm) throws -> Void,
334-
disableAutorefresh: Bool = false,
358+
internal func performSerializedBackgroundWrite(disableAutorefresh: Bool = false,
335359
createTransaction: Bool = true,
336360
notificationTokensToSkip: [NotificationToken] = [],
337-
completionBlock: ((Error?) -> Void)? = nil) {
361+
completionBlock: ((Error?) -> Void)? = nil,
362+
writeBlock: @escaping (Realm) throws -> Void) {
338363
if disableAutorefresh { realm.autorefresh = false }
339364

340365
serialQueue.async {
@@ -394,13 +419,13 @@ public final class Storage {
394419
public func modify<T>(_ object: T, with writeBlock: @escaping (T) -> Void) where T: ThreadConfined {
395420
let safeObject = ThreadSafeReference(to: object)
396421

397-
performSerializedBackgroundWrite(writeBlock: { backgroundRealm in
422+
performSerializedBackgroundWrite(createTransaction: false, writeBlock: { backgroundRealm in
398423
guard let resolvedObject = backgroundRealm.resolve(safeObject) else { return }
399424

400425
try backgroundRealm.write {
401426
writeBlock(resolvedObject)
402427
}
403-
}, createTransaction: false)
428+
})
404429
}
405430

406431
/// Gives you an opportunity to update `objects` on a background queue
@@ -416,7 +441,7 @@ public final class Storage {
416441
public func modify<T>(_ objects: [T], with writeBlock: @escaping ([T]) -> Void) where T: ThreadConfined {
417442
let safeObjects = objects.map { ThreadSafeReference(to: $0) }
418443

419-
performSerializedBackgroundWrite(writeBlock: { [weak self] backgroundRealm in
444+
performSerializedBackgroundWrite(createTransaction: false, writeBlock: { [weak self] backgroundRealm in
420445
guard let self = self else { return }
421446

422447
let resolvedObjects = safeObjects.compactMap { backgroundRealm.resolve($0) }
@@ -429,7 +454,7 @@ public final class Storage {
429454
try backgroundRealm.write {
430455
writeBlock(resolvedObjects)
431456
}
432-
}, createTransaction: false)
457+
})
433458
}
434459

435460
public lazy var events: Observable<Results<Event>> = {
@@ -455,9 +480,9 @@ public final class Storage {
455480
}
456481

457482
public func setFavorite(_ isFavorite: Bool, onSessionsWithIDs ids: [String]) {
458-
performSerializedBackgroundWrite(writeBlock: { realm in
483+
performSerializedBackgroundWrite(disableAutorefresh: false, createTransaction: true, writeBlock: { realm in
459484
let sessions = realm.objects(Session.self).filter(NSPredicate(format: "identifier IN %@", ids))
460-
485+
461486
sessions.forEach { session in
462487
if isFavorite {
463488
guard !session.isFavorite else { return }
@@ -466,7 +491,7 @@ public final class Storage {
466491
session.favorites.forEach { $0.isDeleted = true }
467492
}
468493
}
469-
}, disableAutorefresh: false, createTransaction: true)
494+
})
470495
}
471496

472497
public lazy var eventsObservable: Observable<Results<Event>> = {

0 commit comments

Comments
 (0)