iOS client for Grain grain.social
ios photography atproto
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

perf: add AppLaunch signposting and parallelize MainTabView startup fetches

Instruments-ready OSSignposter intervals on every sync init phase
(NukePipelineSetup, SessionRestore, DPoPLoad, ViewedStorageLoad) and
each async phase in MainTabView.task (AvatarFetch, NotificationsFetch,
LabelDefsFetch), with Logger.debug companions for live log stream.

Avatar fetch, notifications count, and label defs previously ran
serially; they now start concurrently via async let, with authContext()
resolved once up front. Nuke DataCache init moved off the main-thread
init path into a detached task.

authored by

Hima Aramona and committed by
Chad Miller
bb19bac7 15c75fad

+86 -7
+16
Grain/API/AuthManager.swift
··· 4 4 import os 5 5 6 6 private let logger = Logger(subsystem: "social.grain.grain", category: "Auth") 7 + private let authSignposter = OSSignposter(subsystem: "social.grain.grain", category: "Auth") 7 8 8 9 /// Manages OAuth + DPoP authentication flow against the hatk server. 9 10 @Observable ··· 29 30 nonisolated static let redirectURI = "grain://oauth/callback" 30 31 31 32 init() { 33 + let spid = authSignposter.makeSignpostID() 34 + let state = authSignposter.beginInterval("SessionRestore", id: spid) 35 + logger.debug("[SessionRestore] begin") 32 36 // Restore session from Keychain — allow expired tokens since we can refresh 33 37 if TokenStorage.accessToken != nil, 34 38 let did = TokenStorage.userDID, ··· 38 42 userDID = did 39 43 userHandle = TokenStorage.userHandle 40 44 userAvatar = TokenStorage.userAvatar 45 + authSignposter.emitEvent("KeychainRead", id: spid, "authenticated=true") 46 + logger.debug("[KeychainRead] authenticated=true") 47 + let dpopSpid = authSignposter.makeSignpostID() 48 + let dpopState = authSignposter.beginInterval("DPoPLoad", id: dpopSpid) 49 + logger.debug("[DPoPLoad] begin") 41 50 dpop = try? DPoP.loadOrCreate() 51 + authSignposter.endInterval("DPoPLoad", dpopState) 52 + logger.debug("[DPoPLoad] end") 53 + } else { 54 + authSignposter.emitEvent("KeychainRead", id: spid, "authenticated=false") 55 + logger.debug("[KeychainRead] authenticated=false") 42 56 } 57 + authSignposter.endInterval("SessionRestore", state) 58 + logger.debug("[SessionRestore] end") 43 59 } 44 60 45 61 /// Start the OAuth login flow. Set `createAccount` to show the sign-up page.
+17 -4
Grain/GrainApp.swift
··· 1 1 import Nuke 2 + import os 2 3 import SwiftUI 3 4 5 + private let appSignposter = OSSignposter(subsystem: "social.grain.grain", category: "AppLaunch") 6 + private let appLogger = Logger(subsystem: "social.grain.grain", category: "AppLaunch") 7 + 4 8 @main 5 9 struct GrainApp: App { 6 10 init() { 7 - var config = ImagePipeline.Configuration.withDataCache 8 - if let dataCache = try? DataCache(name: "social.grain.images") { 9 - config.dataCache = dataCache 11 + // Defer Nuke DataCache setup off the main-thread init path — no images 12 + // load during the ~800ms before MainTabView.task fires, so this is safe. 13 + Task.detached(priority: .userInitiated) { 14 + let spid = appSignposter.makeSignpostID() 15 + let state = appSignposter.beginInterval("NukePipelineSetup", id: spid) 16 + appLogger.debug("[NukePipelineSetup] begin") 17 + var config = ImagePipeline.Configuration.withDataCache 18 + if let dataCache = try? DataCache(name: "social.grain.images") { 19 + config.dataCache = dataCache 20 + } 21 + await MainActor.run { ImagePipeline.shared = ImagePipeline(configuration: config) } 22 + appSignposter.endInterval("NukePipelineSetup", state) 23 + appLogger.debug("[NukePipelineSetup] end") 10 24 } 11 - ImagePipeline.shared = ImagePipeline(configuration: config) 12 25 } 13 26 14 27 @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
+11
Grain/Utilities/ViewedStoryStorage.swift
··· 1 1 import Foundation 2 + import os 3 + 4 + private let storageSignposter = OSSignposter(subsystem: "social.grain.grain", category: "AppLaunch") 5 + private let storageLogger = Logger(subsystem: "social.grain.grain", category: "AppLaunch") 2 6 3 7 @Observable 4 8 @MainActor ··· 87 91 } 88 92 89 93 private func load() { 94 + let spid = storageSignposter.makeSignpostID() 95 + let state = storageSignposter.beginInterval("ViewedStorageLoad", id: spid) 96 + storageLogger.debug("[ViewedStorageLoad] begin") 97 + defer { 98 + storageSignposter.endInterval("ViewedStorageLoad", state) 99 + storageLogger.debug("[ViewedStorageLoad] end uris=\(self.viewedUris.count) authors=\(self.authorLastViewed.count)") 100 + } 90 101 if let data = UserDefaults.standard.data(forKey: Self.urisKey), 91 102 let decoded = try? JSONDecoder().decode(Set<String>.self, from: data) 92 103 {
+42 -3
Grain/Views/MainTabView.swift
··· 1 + import os 1 2 import SwiftUI 3 + 4 + private let launchSignposter = OSSignposter(subsystem: "social.grain.grain", category: "AppLaunch") 5 + private let launchLogger = Logger(subsystem: "social.grain.grain", category: "AppLaunch") 2 6 3 7 private enum AppTab: Hashable { 4 8 case feed, notifications, profile, search ··· 74 78 .tint(Color("AccentColor")) 75 79 .environment(commentPresenter) 76 80 .task { 81 + let taskSpid = launchSignposter.makeSignpostID() 82 + let taskState = launchSignposter.beginInterval("MainTabLaunch", id: taskSpid) 83 + launchLogger.debug("[MainTabLaunch] begin") 84 + 77 85 commentPresenter.configure( 78 86 auth: auth, 79 87 storyStatusCache: storyStatusCache, ··· 82 90 let c = auth.makeClient() 83 91 client = c 84 92 notificationsVM.updateClient(c) 85 - await auth.fetchAvatarIfNeeded() 93 + 94 + // Start avatar fetch immediately — it doesn't need an auth context 95 + let avatarSpid = launchSignposter.makeSignpostID() 96 + let avatarState = launchSignposter.beginInterval("AvatarFetch", id: avatarSpid) 97 + launchLogger.debug("[AvatarFetch] begin") 98 + async let avatarFetch: Void = auth.fetchAvatarIfNeeded() 99 + 100 + // Resolve auth context once (may refresh token) while avatar is in flight 101 + let ctx = await auth.authContext() 102 + 103 + // Kick off notifications + label defs in parallel now that we have ctx 104 + let notifSpid = launchSignposter.makeSignpostID() 105 + let labelsSpid = launchSignposter.makeSignpostID() 106 + let notifState = launchSignposter.beginInterval("NotificationsFetch", id: notifSpid) 107 + launchLogger.debug("[NotificationsFetch] begin") 108 + let labelsState = launchSignposter.beginInterval("LabelDefsFetch", id: labelsSpid) 109 + launchLogger.debug("[LabelDefsFetch] begin") 110 + async let notifFetch: Void = notificationsVM.fetchUnseenCount(auth: ctx) 111 + async let labelsFetch: Void = labelDefsCache.loadIfNeeded(client: c, auth: ctx) 112 + 113 + await avatarFetch 114 + launchSignposter.endInterval("AvatarFetch", avatarState) 115 + launchLogger.debug("[AvatarFetch] end") 86 116 if let uiImage = auth.avatarImage { 87 117 avatarTabImage = circularAvatar(uiImage, size: 26) 88 118 } 89 - await notificationsVM.fetchUnseenCount(auth: auth.authContext()) 90 - await labelDefsCache.loadIfNeeded(client: c, auth: auth.authContext()) 119 + 120 + await notifFetch 121 + launchSignposter.endInterval("NotificationsFetch", notifState) 122 + launchLogger.debug("[NotificationsFetch] end") 123 + 124 + await labelsFetch 125 + launchSignposter.endInterval("LabelDefsFetch", labelsState) 126 + launchLogger.debug("[LabelDefsFetch] end") 127 + 128 + launchSignposter.endInterval("MainTabLaunch", taskState) 129 + launchLogger.debug("[MainTabLaunch] end") 91 130 } 92 131 .onChange(of: auth.avatarImage) { 93 132 if let uiImage = auth.avatarImage {