From ba8b00e765bc98dc45e9099fac1a0c544192f6e4 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Mon, 2 Sep 2024 18:10:23 +0200 Subject: [PATCH] chore: cache flags, distinct id and anon id in memory to avoid file IO every time (#177) --- CHANGELOG.md | 2 ++ PostHog/PostHogFeatureFlags.swift | 47 +++++++++++++++++++++++------ PostHog/PostHogStorageManager.swift | 46 +++++++++++++++++++++++----- PostHogExample/AppDelegate.swift | 2 +- PostHogExample/ContentView.swift | 2 +- 5 files changed, 80 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d05e301e..55ba7c200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- chore: cache flags, distinct id and anon id in memory to avoid file IO every time ([#177](https://github.com/PostHog/posthog-ios/pull/177)) + ## 3.8.1 - 2024-08-30 - fix: do not clear events when reset is called ([#175](https://github.com/PostHog/posthog-ios/pull/175)) diff --git a/PostHog/PostHogFeatureFlags.swift b/PostHog/PostHogFeatureFlags.swift index 4431b3df5..beca27801 100644 --- a/PostHog/PostHogFeatureFlags.swift +++ b/PostHog/PostHogFeatureFlags.swift @@ -17,6 +17,9 @@ class PostHogFeatureFlags { private var loadingFeatureFlags = false private var sessionReplayFlagActive = false + private var featureFlags: [String: Any]? + private var featureFlagPayloads: [String: Any]? + private let dispatchQueue = DispatchQueue(label: "com.posthog.FeatureFlags", target: .global(qos: .utility)) @@ -94,18 +97,18 @@ class PostHogFeatureFlags { self.featureFlagsLock.withLock { if errorsWhileComputingFlags { - let cachedFeatureFlags = self.storage.getDictionary(forKey: .enabledFeatureFlags) as? [String: Any] ?? [:] - let cachedFeatureFlagsPayloads = self.storage.getDictionary(forKey: .enabledFeatureFlagPayloads) as? [String: Any] ?? [:] + let cachedFeatureFlags = self.getCachedFeatureFlags() ?? [:] + let cachedFeatureFlagsPayloads = self.getCachedFeatureFlagPayload() ?? [:] let newFeatureFlags = cachedFeatureFlags.merging(featureFlags) { _, new in new } let newFeatureFlagsPayloads = cachedFeatureFlagsPayloads.merging(featureFlagPayloads) { _, new in new } // if not all flags were computed, we upsert flags instead of replacing them - self.storage.setDictionary(forKey: .enabledFeatureFlags, contents: newFeatureFlags) - self.storage.setDictionary(forKey: .enabledFeatureFlagPayloads, contents: newFeatureFlagsPayloads) + self.setCachedFeatureFlags(newFeatureFlags) + self.setCachedFeatureFlagPayload(newFeatureFlagsPayloads) } else { - self.storage.setDictionary(forKey: .enabledFeatureFlags, contents: featureFlags) - self.storage.setDictionary(forKey: .enabledFeatureFlagPayloads, contents: featureFlagPayloads) + self.setCachedFeatureFlags(featureFlags) + self.setCachedFeatureFlagPayload(featureFlagPayloads) } } @@ -129,7 +132,7 @@ class PostHogFeatureFlags { func getFeatureFlags() -> [String: Any]? { var flags: [String: Any]? featureFlagsLock.withLock { - flags = self.storage.getDictionary(forKey: .enabledFeatureFlags) as? [String: Any] + flags = self.getCachedFeatureFlags() } return flags @@ -138,7 +141,7 @@ class PostHogFeatureFlags { func isFeatureEnabled(_ key: String) -> Bool { var flags: [String: Any]? featureFlagsLock.withLock { - flags = self.storage.getDictionary(forKey: .enabledFeatureFlags) as? [String: Any] + flags = self.getCachedFeatureFlags() } let value = flags?[key] @@ -158,16 +161,40 @@ class PostHogFeatureFlags { func getFeatureFlag(_ key: String) -> Any? { var flags: [String: Any]? featureFlagsLock.withLock { - flags = self.storage.getDictionary(forKey: .enabledFeatureFlags) as? [String: Any] + flags = self.getCachedFeatureFlags() } return flags?[key] } + private func getCachedFeatureFlagPayload() -> [String: Any]? { + if featureFlagPayloads == nil { + featureFlagPayloads = storage.getDictionary(forKey: .enabledFeatureFlagPayloads) as? [String: Any] + } + return featureFlagPayloads + } + + private func setCachedFeatureFlagPayload(_ featureFlagPayloads: [String: Any]) { + self.featureFlagPayloads = featureFlagPayloads + storage.setDictionary(forKey: .enabledFeatureFlagPayloads, contents: featureFlagPayloads) + } + + private func getCachedFeatureFlags() -> [String: Any]? { + if featureFlags == nil { + featureFlags = storage.getDictionary(forKey: .enabledFeatureFlags) as? [String: Any] + } + return featureFlags + } + + private func setCachedFeatureFlags(_ featureFlags: [String: Any]) { + self.featureFlags = featureFlags + storage.setDictionary(forKey: .enabledFeatureFlags, contents: featureFlags) + } + func getFeatureFlagPayload(_ key: String) -> Any? { var flags: [String: Any]? featureFlagsLock.withLock { - flags = self.storage.getDictionary(forKey: .enabledFeatureFlagPayloads) as? [String: Any] + flags = getCachedFeatureFlagPayload() } let value = flags?[key] diff --git a/PostHog/PostHogStorageManager.swift b/PostHog/PostHogStorageManager.swift index 1cea334b1..9c4da318b 100644 --- a/PostHog/PostHogStorageManager.swift +++ b/PostHog/PostHogStorageManager.swift @@ -14,20 +14,28 @@ class PostHogStorageManager { private let distinctLock = NSLock() private let idGen: (UUID) -> UUID + private var distinctId: String? + private var cachedDistinctId = false + private var anonymousId: String? + init(_ config: PostHogConfig) { storage = PostHogStorage(config) idGen = config.getAnonymousId } public func getAnonymousId() -> String { - var anonymousId: String? anonLock.withLock { - anonymousId = storage.getString(forKey: .anonymousId) - if anonymousId == nil { - let uuid = UUID.v7() - anonymousId = idGen(uuid).uuidString - setAnonId(anonymousId ?? "") + var anonymousId = storage.getString(forKey: .anonymousId) + + if anonymousId == nil { + let uuid = UUID.v7() + anonymousId = idGen(uuid).uuidString + setAnonId(anonymousId ?? "") + } else { + // update the memory value + self.anonymousId = anonymousId + } } } @@ -41,19 +49,40 @@ class PostHogStorageManager { } private func setAnonId(_ id: String) { + anonymousId = id storage.setString(forKey: .anonymousId, contents: id) } public func getDistinctId() -> String { var distinctId: String? distinctLock.withLock { - distinctId = storage.getString(forKey: .distinctId) ?? getAnonymousId() + if self.distinctId == nil { + // since distinctId is nil until its identified, no need to read from + // cache every single time, otherwise anon users will never used the + // cached values + if !cachedDistinctId { + distinctId = storage.getString(forKey: .distinctId) + cachedDistinctId = true + } + + // do this to not assign the AnonymousId to the DistinctId, its just a fallback + if distinctId == nil { + distinctId = getAnonymousId() + } else { + // update the memory value + self.distinctId = distinctId + } + } else { + // read from memory + distinctId = self.distinctId + } } return distinctId ?? "" } public func setDistinctId(_ id: String) { distinctLock.withLock { + distinctId = id storage.setString(forKey: .distinctId, contents: id) } } @@ -61,9 +90,12 @@ class PostHogStorageManager { public func reset() { distinctLock.withLock { storage.remove(key: .distinctId) + distinctId = nil + cachedDistinctId = false } anonLock.withLock { storage.remove(key: .anonymousId) + anonymousId = nil } } } diff --git a/PostHogExample/AppDelegate.swift b/PostHogExample/AppDelegate.swift index 31153eed5..7ca255b69 100644 --- a/PostHogExample/AppDelegate.swift +++ b/PostHogExample/AppDelegate.swift @@ -31,7 +31,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { // PostHogSDK.shared.capture("App started!") // PostHogSDK.shared.reset() - PostHogSDK.shared.identify("Manoel") +// PostHogSDK.shared.identify("Manoel") let defaultCenter = NotificationCenter.default diff --git a/PostHogExample/ContentView.swift b/PostHogExample/ContentView.swift index 8bad4eb24..d02a83952 100644 --- a/PostHogExample/ContentView.swift +++ b/PostHogExample/ContentView.swift @@ -63,7 +63,7 @@ class FeatureFlagsModel: ObservableObject { struct ContentView: View { @State var counter: Int = 0 - @State private var name: String = "Tim" + @State private var name: String = "Max" @State private var showingSheet = false @State private var showingRedactedSheet = false @StateObject var api = Api()