I've built a sports game in Xcode/Swift using Realm as a base. It's running and working, but realm transactions are getting slower and slower over time, to the point where it's significantly slower than i'd hope. This is particularly evident in this example.
Each week there are a number of matches, with each player having a GameStat object created for every match (about 2,000 new GameStats created each week).
class GameStat: Object {
#objc dynamic var iden = UUID().uuidString
#objc dynamic var playerIden = "" // Links to iden of a Player Object
#objc dynamic var statOne = 0
#objc dynamic var statTwo = 0
#objc dynamic var statThree = 0
etc...
// Primary Key
override static func primaryKey() -> String? {
return "iden"
}
}
To prevent multiple realm writes throughout each game, I create GameStats outside of Realm (store in thisGameGameStats array) and then batch write these GameStats to realm once the game is complete. It is this batch write to Realm which is very slow.
try! realm.write {
for gmStat in thisGameGameStats {
realm.add(gmStat)
}
}
I've measured time taken and I always see the 'for' loop complete after 0.05 seconds. In the first couple of loops, there is no delay for the realm.write function to complete, however after a couple of seasons (c70k records in database) it takes a further 0.50 seconds for the realm.write function to complete.
Is this to be expected, or is there an opportunity to optimise here?
EDIT: By removing the primaryKey, the write becomes significantly quicker and doesn't seem to slow down as the database grows. I'm assuming there is therefore some form of sort by primaryKey that happens on commit.
This isn't exactly an answer but may provide some validatation and illustrate an interesting point.
TL;DR
Writing chunks of objects over time does not have a significant impact on write times - you should not be seeing a significant increase in write times based on the existing volume of data in this use case.
The long version with setup and testing:
When a write has completed, realm will generate a notification so we're using that notification to 'time' the writes.
We start with a class var to store the start time of each write and a notification to track when it's done
var startTime = Date()
var gameStatNotificationToken: NotificationToken?
then we add an observer to realm that will execute when the write has completed that determines the elapsed time between the start time and now
func addObserver {
if let realm = gGetRealm() { //this just gets a realm
self.gameStatNotificationToken = realm.observe { notification, realm in
let elapsed = Date().timeIntervalSince(self.startTime)
print("\(elapsed)")
}
}
}
Then the code to actually write the data
func writeLotsOfData() {
autoreleasepool {
var gameStatArray = [GameStat]()
for index in 0..<10000 {
let stat = GameStat()
stat.playerIden = "\(index)"
gameStatArray.append(stat)
}
if let realm = gGetRealm() {
self.startTime = Date()
try! realm.write {
realm.add(gameStatArray)
}
}
}
}
and then a loop that calls the above code 10 times; writing 10k per loop, 100k total objects.
func looper() {
for i in 0..<10 {
self.writeLotsOfData()
}
}
So the end result shows no increase in writing over time
0.19487500190734863
0.1404169797897339
0.14565002918243408
0.15493690967559814
0.14426898956298828
0.15933406352996826
0.15379595756530762
0.16700804233551025
0.1598280668258667
0.15421009063720703
EDIT
Here's the GameStat object I am using - it's identical to the one in the question
class GameStat: Object {
#objc dynamic var iden = UUID().uuidString
#objc dynamic var playerIden = "" // Links to iden of a Player Object
#objc dynamic var statOne = 0
#objc dynamic var statTwo = 0
#objc dynamic var statThree = 0
// Primary Key
override static func primaryKey() -> String? {
return "iden"
}
}
Related
I have a production-ready app where I need to run some code only on users with the previous versions installed but not on new installations. For instance, if 1.0 is the latest version in the AppStore and 2.0 is the new one that will introduce code that needs to run only on users with version 1.0 but not on new users.
e.g.
if isExistingUser{
// run code
}
What would be the best way to run some code only for existing users? How can one determine whether it's a new or existing user?
Does your app create any data? Maybe files in the Documents directory, or maybe a UserDefaults key? If so, check for the presence of one of those things very early in launch, which will signal to you that this must be an upgrade and you should do your thing.
A lot of people store the app's CFBundleShortVersionString Info.plist key into UserDefaults at launch, which makes it easy to know the last version that was run and let you write the logic of what needs to happen to migrate from that version to the new version. This is something you might want to think about doing in future versions.
I see this often and knowing how to do this can be extremely valuable, especially when doing something like introducing a new feature that changes the experience your previous users had.
There are a few approaches you can take depending on your needs.
Firstly, you could create a boolean variable in your user model class that is set during user registration in the standard user registration flow and indicates that this is a newly created user - and name it something like isNewOnVersionTwo which will indicate this user is a new user on this new version.
Code Example:
class User: Decodable {
var uid: string!
var username: string!
var isNewOnVersionTwo: Bool = false
}
class RegistrationViewController: UIViewController {
var user: User!
var isNewOnVersionTwo: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
user.isNewOnVersionTwo = true
}
}
class HomeViewController: UIViewController {
var user: User!
override func viewDidLoad() {
super.viewDidLoad()
isNewOnVersionTwo == true ? normalInit() : showOldUserAView()
}
func normalInit() {
// Run normal functions for the 'HomeViewController' or do nothing.
}
func showOldUserAView() {
//Do something specific for only old users.
}
}
You can choose whether you want to hold onto this variable permanently or not - it could be useful for tracking the new users you've gained on this version versus the previous versions - and you could send it to your database along with the rest of the data from your user model.
A second and cleaner approach...
Could be to only set the boolean on the very last view controller of the registration flow and pass it to the home view controller when you push the user to the view controller.
Like this:
class ViewControllerOne: UIViewController {
var isNewOnVersionTwo: Bool = false
private func pushToNewViewController() {
let vc = HomeViewController()
vc.isNewOnVersionTwo = true
navigationController?.pushViewController(vc, animated: true)
}
}
class HomeViewController: UIViewController {
var isNewOnVersionTwo: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
isNewOnVersionTwo == true ? normalInit() : showOldUserAView()
}
func normalInit() {
// Run normal functions for the 'HomeViewController' or do nothing.
}
func showOldUserAView() {
//Do something specific for only old users.
}
}
I wouldn't take the approach of using UserDefaults for the few following reasons:
1. Devices occasionally wipe this randomly and it's not as reliable as hardcoding a flag in the source code.
2. UserDefaults isn't available for a few moments on cold app launches / initial startup which can complicate things, varying on when you want to show the older users whatever you want to show them.
Here is what I came up with that I think works for 95-98% of the users. What I'm doing is basically comparing the date when the Documents folder was created and the date when version2 will be released. The only issue I see with this method is for users installing the app between the date you specified as the release date and the actual App Store release date. In other words, if you specify a date of June, 5 as the release date but the app isn't really approved until the 7th, users who installed the app on the 6th will be missed.
Ideally and it's what I will start doing is what #Rudedog suggested, basically saving the versions to keep track of what version a user has.
/// - Returns: Returns true if it's a new user otherwise returns false.
func isExistingUser()->Bool{
var isExistingUser:Bool?
var appInstallationDate:Date?
/// Set the date when the version two is released.
var dateComponents = DateComponents()
dateComponents.year = 2022
dateComponents.month = 06
dateComponents.day = 4
let userCalendar = Calendar.current
let versionsTwoReleaseDate = userCalendar.date(from: dateComponents)
/// Get the date of when the documents folder was created to determine when the app was installed.
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
if let creationDate = (try? documentsDirectory!.resourceValues(forKeys: [.creationDateKey]))?.creationDate{
appInstallationDate = creationDate
}
/// Compare dates
if appInstallationDate! < versionsTwoReleaseDate!{
// print("It's existing user")
isExistingUser = true
}else{
// print("It's NEW user")
isExistingUser = false
}
return isExistingUser!
}
Hi i am trying to use alamofire to download json weather data. Here is my code, the working version:
class WeatherModel {
private var _date: String?
private var _location: String?
private var _weatherType: String?
private var _temperature: Double?
func getWeatherInfoFromAPI(completed: #escaping ()-> ()) {
let url = URL(string: WEATHER_URL)!
Alamofire.request(url).responseJSON(completionHandler: { response in
// Test updating data
self._temperature = 25
self._weatherType = "Clear"
self._location = "Vietnam"
completed()
})
}
}
-> This way, i am able to update the property of the class.
Failing to update class property version of getWeatherInfoFromAPI func:
func getWeatherInfoFromAPI(completed: #escaping ()-> ()) {
let url = URL(string: WEATHER_URL)!
Alamofire.request(url).responseJSON{ response in
// Test updating data
self._temperature = 25
self._weatherType = "Clear"
self._location = "Vietnam"
}
completed()
}
So, i dont know what is the difference between them. Please help me to clarify between 2 ways here.
Alamofire.request(url).responseJSON(completionHandler: { response in })
and
Alamofire.request(url).responseJSON{ response in }
What is the reason that my code does not work? Since i see the Alamofire docs also use like the second way! I am thinking about thread difference between them
Also, how do i know what thread the code is running in responseJSON?
Thanks, i appreciate your time and help!
Those two ways are functionally identical, the second one just uses Swift's trailing closure syntax.
What do you do in completed()? Because in first example, you are calling it upon completion of network call, and in second case you are calling it immediately after you start the network call - the call is not completed yet. You should call if in Alamofire callback, like in first example. In second example, if you're inspecting those properties inside completed, then it's no wonder they're not updated yet.
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 5 years ago.
Improve this question
I was wondering if (and how much) strong/weak references management have an impact on code execution, especially when freeing objects to which many classes might have a weak reference. At first I mistaken this for ARC, but it's not.
There's a similar question on the same topic, however they don't investigate performance impact or try to pull a number out of it.
Let me be clear: I'm not, in any way, suggesting that ARC or strong/weak might have a bad impact on performance, or say "don't use this". I LOVE this. I'm just curious about how efficient it is, and how to size it.
I've put together this piece of code to get an idea of the performance impact of strong/weak references in execution time.
import Foundation
class Experiment1Class {
weak var aClass: Experiment1Class?
}
class Experiment2Class {
var aClass: Experiment2Class?
}
var persistentClass: Experiment1Class? = Experiment1Class()
var nonWeakPersistentClass: Experiment2Class? = Experiment2Class()
var classHolder = [Experiment1Class]()
var nonWeakClassholder = [Experiment2Class]()
for _ in 1...1000 {
let aNewClass = Experiment1Class()
aNewClass.aClass = persistentClass
classHolder.append(aNewClass)
let someNewClass = Experiment2Class()
someNewClass.aClass = nonWeakPersistentClass
nonWeakClassholder.append(someNewClass)
}
let date = Date()
persistentClass = nil
let date2 = Date()
let someDate = Date()
nonWeakPersistentClass = nil
let someDate2 = Date()
let timeExperiment1 = date2.timeIntervalSince(date)
let timeExperiment2 = someDate2.timeIntervalSince(someDate)
print("Time: \(timeExperiment1)")
print("Time: \(timeExperiment2)")
This piece of code only measure the amount of time it takes to free an object and to set to nil all its references.
IF you execute it in Playground (Xcode 8.3.1) you will see a ratio of 10:1 but Playground execution is much slower than real execution, so I also suggest to execute the above code with "Release" build configuration.
If you execute in Release I suggest you set the iteration count to "1000000" at least, the way I did it:
insert the above code into file test.swift
from terminal, run swiftc test.swift
execute ./test
Being this kind of test, I believe the absolute results makes no to little sense, and I believe this has no impact on 99% of the usual apps...
My results so far shows, on Release configuration executed on my Mac:
Time: 3.99351119995117e-06
Time: 0.0
However the same execution, in Release, on my iPhone 7Plus:
Time: 1.4960765838623e-05
Time: 1.01327896118164e-06
Clearly this test shows there should be little to no concern for real impact on typical apps.
Here are my questions:
Can you think of any other way to measure strong/weak references impact on execution time? (I wonder what strategies the system put in place to improve on this)
What other metrics could be important? (like multi threading optimisation or thread locking)
How can I improve this?
EDIT 1
I found this LinkedList test to be very interesting for a few reasons, consider the following code:
//: Playground - noun: a place where people can play
import Foundation
var n = 0
class LinkedList: CustomStringConvertible {
var count = n
weak var previous: LinkedList?
var next: LinkedList?
deinit {
// print("Muorte \(count)")
}
init() {
// print("Crea \(count)")
n += 1
}
var description: String {
get {
return "Node \(count)"
}
}
func recurseDesc() -> String {
return(description + " > " + (next?.recurseDesc() ?? "FIN"))
}
}
func test() {
var controlArray = [LinkedList]()
var measureArray = [LinkedList]()
var currentNode: LinkedList? = LinkedList()
controlArray.append(currentNode!)
measureArray.append(currentNode!)
var startingNode = currentNode
for _ in 1...31000 {
let newNode = LinkedList()
currentNode?.next = newNode
newNode.previous = currentNode!
currentNode = newNode
controlArray.append(newNode)
measureArray.append(newNode)
}
controlArray.removeAll()
measureArray.removeAll()
print("test!")
let date = Date()
currentNode = nil
let date2 = Date()
let someDate = Date()
startingNode = nil
let someDate2 = Date()
let timeExperiment1 = date2.timeIntervalSince(date)
let timeExperiment2 = someDate2.timeIntervalSince(someDate)
print("Time: \(timeExperiment1)")
print("Time: \(timeExperiment2)")
}
test()
I found the following (running in Release configuration):
I couldn't be able to run more than ~32000 iterations on my phone, it's crashing of EXC_BAD_ACCESS during deinit (yes, during DEINIT... isn't this weird)
Timing for ~32000 iteration is 0 vs 0.06 seconds, which is huge!
I think this test is very CPU intensive because nodes are freed one after the other (if you see the code, only the next one is strong, the previous is weak)... so once the first one is freed, the others fall one after the other, but not altogether.
There is really no such thing as automatic memory management. Memory is managed by retain and release regardless of whether you use ARC. The difference is who writes the code, you or the compiler. There is manual memory management code that you write, and there is manual memory management code that ARC writes. In theory, ARC inserts into your code exactly the same retain and release commands that you would have inserted if you had done this correctly. Therefore the difference in performance should be epsilon-tiny.
Here's my problem: I have several variables in my viewController, for example:
var money = 0.0
I want to store this value on the device as soon as the app is going to terminate. To do this, I need to be able to acces the variable in the appDelegate and use it in 'applicationWillTerminate'. I then want it to be stored on the device and when the app restarts I want to be able to acces the stored value again. How do I do this in (swift) Xcode 8.2?
"applicationWillTerminate" as Apple docs say, is no more called in modern app unless You set a specific flag in plist.
The correct approach is to store values in NSUserDefault:
For swift 3.0:
1) let's create a class to manage money, and saving them:
class MoneyManager: NSObject {
private let MONEY_KEY = "money"
static let sharedInstance = MoneyManager()
var money: Float? {
get {
let returnValue = UserDefaults.standard.object(forKey: MONEY_KEY) as? Float
return returnValue
}
set(newValue){
if newValue == nil {
UserDefaults.standard.removeObject(forKey: MONEY_KEY)
} else {
UserDefaults.standard.set(newValue!, forKey: MONEY_KEY)
}
UserDefaults.standard.synchronize()
}
}
}
2) to use it in every controller you need:
let money = MoneyManager.sharedInstance.money
if money == nil{
MoneyManager.sharedInstance.money = 1000
}
As every time we call UserDefaults.standard.synchronize(), we are safe everything is saved.
So I'm working on setting up a background queue that does all realm writes on its own thread. I've run into some strange issues I can't figure out.
Issue #1
I'm not sure if this is related (see post: Xcode debug issues with realm) but I do have an apparent mismatch with my lldbg output as to whether a certain field:
messages element
My DataTypes
OTTOSession
class OTTOSession : Object {
dynamic var messages : MessageList?
dynamic var recordingStart : Double = NSDate().timeIntervalSince1970
func addLocationMessage(msg : dmParsedMessage) -> LocationMessage {
let dmsg : dmLocationMessage = msg as! dmLocationMessage
let locMsg = LocationMessage(locMsg: dmsg)
self.messages!.locationMessages.append(locMsg)
return locMsg;
}
}
MessageList
public class MessageList : Object {
dynamic var date : NSDate = NSDate();
dynamic var test : String = "HI";
let locationMessages = List<LocationMessage>()
let ahrsMessages = List<AHRSMessage>()
// let statusMessages = List<StatusMessageRLM>()
let logMessages = List<LogMessage>()
}
Realm Interactions
In my code I create my new OTTOSession in a code block on my realmQueue
internal var realmQueue = dispatch_queue_create("DataRecorder.realmQueue",
DISPATCH_QUEUE_SERIAL)
All realm calls are done on this realmQueue thread
dispatch_async(realmQueue) {
self.session = OTTOSession()
}
I've also tried different variants such as:
dispatch_async(realmQueue) {
self.session = OTTOSession()
// Directly making a message list
self.session!.messages = MessageList()
//Making a separate message list var
self.messages = MessageList()
self.session!.messages = self.messages
}
The reason I've played around with the MessageList is that I cant tell from the debugger whether the .messages variable is set or not
Recording
Once I signal to my processes I want to start recording I then actually make the write calls into Realm (which I'm not 100% sure i'm doing correctly)
dispatch_async(realmQueue){
// Update some of the data
self.session!.recordingStart = NSDate().timeIntervalSince1970
// Then start writing the objects
try! Realm().write {
// I've tried different variants of:
let session = self.session!
try! Realm().add(self.session!)
// Or
try! Realm().add(self.session!)
// or
let session = self.session!
session.messages = MessageList()
session.messages!.ahrsMessages
try! Realm().add(self.session!)
try! self.session!.messages = Realm().create(MessageList)
try! Realm().add(self.session!.messages!)
print ("Done")
}
}
Basically I've tried various combinations of trying to get the objects into realm.
Question: When adding an object with a one-to-one relationship do I have to add both objects to Realm or will just adding the parent object cause the related object to also be added to realm
Adding Data
Where things start to go awry is when I start adding data to my objects.
Inside my OTTOSession Object I have the following function:
func addLocationMessage(msg : dmParsedMessage) -> LocationMessage {
let dmsg : dmLocationMessage = msg as! dmLocationMessage
let locMsg = LocationMessage(locMsg: dmsg)
// THIS LINE IS CAUSING A 'REALM ACCESSED FROM INCORRECT THREAD ERROR
self.messages!.locationMessages.append(locMsg)
return locMsg;
}
I'm getting my access error on this line:
self.messages!.locationMessages.append(locMsg)
Now the function call itself is wrapped in the following block:
dispatch_async(realmQueue) {
try! Realm().write {
self.session?.addLocationMessage(msg)
}
}
So as far as I can tell by looking at the debugger - and by looking at the code - everything should be running inside the right thread.
My queue is SERIAL so things should be happening one after another. The only thing I can't figure out is when I break at this point the debugger does show that messages is nil but I cant trust that because:
Question
So my question is two fold
1) Is my code for adding an object into the RealmDB correct. i.e. do I need to make two separate Realm().add calls for both the OTTOSession and the MessageList or can I get away with a single call
2) Is there anything that pops out to explain why I'm getting a thread violation here - should doing all my realm writing calls on a single thread be enough ?
1) No, you don't need to make two separate calls to Realm.add(). When you add an object to a Realm all related objects are persisted as well.
2) Your thread violation very likely originates from the fact that dispatch queues make no guarantee over the thread on which they are executed on (beside the main queue). So that means your Realm queue is executed on different threads. You will need to make sure to retrieve your session object from a Realm opened on this thread. You might want to use primary keys for that purpose and share those between queues / threads.