From 491a27cacdb96504f4428596916c61b5a92c27c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Wed, 26 Jun 2024 15:44:44 +0200 Subject: [PATCH 1/5] Add a .gitignore and README.md ... --- .gitignore | 2 ++ README.md | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9111809 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.xcuserdatad + diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa2626a --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Perf Test for CoreData, SwiftData and Realm + +Comparing the performance characteristics of Realm, SwiftData and CoreData. + +Code accompanying the Emerge Tools blog article: +[SwiftData vs Realm: Performance Comparison](https://www.emergetools.com/blog/posts/swiftdata-vs-realm-performance-comparison) +by Jacob Bartlett. + +Original repositoy: https://github.com/jacobsapps/RealmVsSwiftData + +Michal Tsai's coverage: https://mjtsai.com/blog/2024/06/24/swiftdata-vs-realm-performance-comparison/ + +## Links + +- [Realm](https://www.mongodb.com/docs/atlas/device-sdks/) by MongoDB +- [SwiftData](https://developer.apple.com/documentation/swiftdata) by Apple +- [Core Data](https://developer.apple.com/documentation/coredata/) by Apple + - [ManagedModels](https://github.com/Data-swift/ManagedModels)) by Helge Heß, + access Core Data in a SwiftData like fashion +- [Lighter.swift](https://github.com/Lighter-swift) by Helge Heß, direct SQLite access From 8936bd0219548555ba92f50373c454cde7fbd415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Wed, 26 Jun 2024 15:46:53 +0200 Subject: [PATCH 2/5] Set Realm version to sth that resolves ... --- RealmVsSwiftData.xcodeproj/project.pbxproj | 2 ++ .../xcshareddata/swiftpm/Package.resolved | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/RealmVsSwiftData.xcodeproj/project.pbxproj b/RealmVsSwiftData.xcodeproj/project.pbxproj index 4617990..0fab806 100644 --- a/RealmVsSwiftData.xcodeproj/project.pbxproj +++ b/RealmVsSwiftData.xcodeproj/project.pbxproj @@ -115,9 +115,11 @@ 167973BE2BC804B9002FE4D9 = { isa = PBXGroup; children = ( + E82FB4E72C2C5155000E0FCC /* README.md */, 167973C92BC804B9002FE4D9 /* RealmVsSwiftData */, 167973C82BC804B9002FE4D9 /* Products */, ); + indentWidth = 4; sourceTree = ""; }; 167973C82BC804B9002FE4D9 /* Products */ = { diff --git a/RealmVsSwiftData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/RealmVsSwiftData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5be2cfb..7aea61f 100644 --- a/RealmVsSwiftData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/RealmVsSwiftData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "2174eaf93f622d57be865cd78b51b72620c12528f1f0957292620977c4d69891", "pins" : [ { "identity" : "realm-core", "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-core.git", "state" : { - "revision" : "f3417640021f329480035f432840eb0e5d0e1957", - "version" : "14.5.0" + "revision" : "058ecce712b4be8b2a2384ed893bf83d56a49fc0", + "version" : "14.10.1" } }, { @@ -15,9 +16,9 @@ "location" : "https://github.com/realm/realm-swift", "state" : { "branch" : "master", - "revision" : "2f0274f59a4771d1e778574f4315a320a5ed08a9" + "revision" : "e3258a29f1993bef4fe3d9be801c7f83dc40eef5" } } ], - "version" : 2 + "version" : 3 } From 7aaa80fc2bd77f7e4596c4043471cd7c0b2ea228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Wed, 26 Jun 2024 15:47:21 +0200 Subject: [PATCH 3/5] Set build configuration to Release We want to check performance. --- .../xcshareddata/xcschemes/RealmVsSwiftData.xcscheme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RealmVsSwiftData.xcodeproj/xcshareddata/xcschemes/RealmVsSwiftData.xcscheme b/RealmVsSwiftData.xcodeproj/xcshareddata/xcschemes/RealmVsSwiftData.xcscheme index c93ef05..f9dded5 100644 --- a/RealmVsSwiftData.xcodeproj/xcshareddata/xcschemes/RealmVsSwiftData.xcscheme +++ b/RealmVsSwiftData.xcodeproj/xcshareddata/xcschemes/RealmVsSwiftData.xcscheme @@ -30,7 +30,7 @@ shouldAutocreateTestPlan = "YES"> Date: Thu, 27 Jun 2024 16:20:05 +0200 Subject: [PATCH 4/5] CoreData port of Swift tests To check how CoreData compares to the others. Suggestions welcome. --- README.md | 129 +++++++++++++- RealmVsSwiftData.xcodeproj/project.pbxproj | 70 ++++++++ .../xcshareddata/swiftpm/Package.resolved | 20 ++- .../DB/CoreData/CoreDatabase.swift | 84 +++++++++ .../DB/CoreData/Students/CoreStudent.swift | 168 ++++++++++++++++++ .../DB/CoreData/Students/CoreStudentDB.swift | 22 +++ .../DB/CoreData/Users/CoreUser.swift | 45 +++++ .../DB/CoreData/Users/CoreUserDB.swift | 23 +++ .../CoreStudentsTests.swift | 74 ++++++++ .../PerformanceTesting/CoreUsersTests.swift | 56 ++++++ .../PerformanceTesting/TestUtils.swift | 17 +- 11 files changed, 699 insertions(+), 9 deletions(-) create mode 100644 RealmVsSwiftData/DB/CoreData/CoreDatabase.swift create mode 100644 RealmVsSwiftData/DB/CoreData/Students/CoreStudent.swift create mode 100644 RealmVsSwiftData/DB/CoreData/Students/CoreStudentDB.swift create mode 100644 RealmVsSwiftData/DB/CoreData/Users/CoreUser.swift create mode 100644 RealmVsSwiftData/DB/CoreData/Users/CoreUserDB.swift create mode 100644 RealmVsSwiftData/PerformanceTesting/CoreStudentsTests.swift create mode 100644 RealmVsSwiftData/PerformanceTesting/CoreUsersTests.swift diff --git a/README.md b/README.md index fa2626a..09af741 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,128 @@ Original repositoy: https://github.com/jacobsapps/RealmVsSwiftData Michal Tsai's coverage: https://mjtsai.com/blog/2024/06/24/swiftdata-vs-realm-performance-comparison/ + +## Issues + +- UUID init is superfluous in all but Realm? CD/SQLite can do the primary keys. +- Delete all users could be just a database table drop/recreate? + + ## Links -- [Realm](https://www.mongodb.com/docs/atlas/device-sdks/) by MongoDB -- [SwiftData](https://developer.apple.com/documentation/swiftdata) by Apple -- [Core Data](https://developer.apple.com/documentation/coredata/) by Apple - - [ManagedModels](https://github.com/Data-swift/ManagedModels)) by Helge Heß, - access Core Data in a SwiftData like fashion -- [Lighter.swift](https://github.com/Lighter-swift) by Helge Heß, direct SQLite access +- [SwiftData vs Realm: Performance Comparison](https://www.emergetools.com/blog/posts/swiftdata-vs-realm-performance-comparison) +- [RealmVsSwiftData](https://github.com/jacobsapps/RealmVsSwiftData) original repository +- Storages + - [Realm](https://www.mongodb.com/docs/atlas/device-sdks/) by MongoDB + - [SwiftData](https://developer.apple.com/documentation/swiftdata) by Apple + - [Core Data](https://developer.apple.com/documentation/coredata/) by Apple + - [ManagedModels](https://github.com/Data-swift/ManagedModels)) by [Helge Heß](https://helgehess.eu/), + access Core Data in a SwiftData like fashion + - [Lighter.swift](https://github.com/Lighter-swift) by [Helge Heß](https://helgehess.eu/), direct SQLite access + + +## Numbers + +M2 Mac Mini Pro, Xcode 16 b2, running in simulator + +- SwiftData and CoreData can grow big, e.g. SwiftData >6GB of RAM for the 1m + user inserts, or CoreData >2GB of RAM for the "complex" 100k run. + +### Simple User Objects + +``` +💽 =============== +💽 SwiftData: 1.000.000 Simple Objects +=============== +💽 User instantiation: 6.5249 +💽 Create users: 9913.5297 (2.75h) +207 users named Jane +💽 Fetch users named `Jane` in age order: 1.7491 +207 users renamed to `Wendy` +💽 Rename users named `Jane` to `Wendy`: 133.1398 +💽 DB file size: 101.51 MB +💽 Delete all users: 17.4455 + +💽 =============== +💽 Realm: 1.000.000 Simple Objects +=============== +💽 User instantiation: 1.6368 +💽 Create users: 4.8281 +199 users named Jane +💽 Fetch users named `Jane` in age order: 0.0110 +199 users renamed to `Wendy` +💽 Rename users named `Jane` to `Wendy`: 0.0101 +💽 DB file size: 75.64 MB +💽 Delete all users: 0.0120 + +💽 =============== +💽 CoreData: 1.000.000 Simple Objects +=============== +💽 User instantiation: 1.9408 +💽 Create users: 4270.8671 (~1:15h) +205 users named `Jane` +💽 Fetch users named `Jane` in age order: 0.8116 +205 users named `Jane` being renamed to `Wendy` +205 users renamed to `Wendy` +💽 Rename users named `Jane` to `Wendy`: 1.5780 +💽 DB file size: 72.57 MB +💽 Delete all users: 70.3943 + +💽 =============== +💽 Lighter: 1.000.000 Simple Objects +=============== +💽 User instantiation: 0.4484 +💽 Create users: 7.8463 +💽 Create index: 0.3628 +218 users named Jane +💽 Fetch users named `Jane` in age order: 0.0016 +218 users renamed to `Wendy` +💽 Rename users named `Jane` to `Wendy`: 0.0100 +💽 DB file size: 79.65 MB +💽 Delete all users: 10.0460 +``` + +### Complex Student/School/Grade Objects + +``` +💽 =============== +💽 CoreData: 1.000 Complex Objects +=============== +💽 Student instantiation: 0.0220 +Created 1000 students. +💽 Create students: 0.2168 +43 students with an A* in Physics +💽 Read students with A* in Physics: 0.0051 +De-graded 0 students. +💽 Fail the cheating Maths students: 0.0006 +💽 DB file size: 0.05 MB +💽 Delete all students: 0.0063 +💽 =============== +💽 CoreData: 10.000 Complex Objects +=============== +CoreData: debug: PostSaveMaintenance: incremental_vacuum with freelist_count - 120 and pages_to_free 107 +💽 Student instantiation: 0.1986 +Created 10000 students. +💽 Create students: 16.1095 +385 students with an A* in Physics +💽 Read students with A* in Physics: 0.1160 +De-graded 0 students. +💽 Fail the cheating Maths students: 0.0038 +💽 DB file size: 4.73 MB +💽 Delete all students: 0.0857 +💽 =============== +💽 CoreData: 100.000 Complex Objects +=============== +💽 Student instantiation: 1.8890 +Created 100000 students. +💽 Create students: 1631.6497 ~28min +4032 students with an A* in Physics +💽 Read students with A* in Physics: 9.4956 +De-graded 0 students. +💽 Fail the cheating Maths students: 0.0299 +💽 DB file size: 48.77 MB +CoreData: debug: PostSaveMaintenance: fileSize 49415312 greater than prune threshold +CoreData: debug: PostSaveMaintenance: incremental_vacuum with freelist_count - 12457 and pages_to_free 12429 +CoreData: annotation: PostSaveMaintenance: wal_checkpoint(TRUNCATE) +💽 Delete all students: 29.9509 +``` diff --git a/RealmVsSwiftData.xcodeproj/project.pbxproj b/RealmVsSwiftData.xcodeproj/project.pbxproj index 0fab806..19fc1b7 100644 --- a/RealmVsSwiftData.xcodeproj/project.pbxproj +++ b/RealmVsSwiftData.xcodeproj/project.pbxproj @@ -33,6 +33,14 @@ 16A41EDB2BE3A7DE00C62A21 /* SwiftStudentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A41ED82BE3A7DE00C62A21 /* SwiftStudentsTests.swift */; }; 16A41EDC2BE3A7DE00C62A21 /* RealmStudentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A41ED92BE3A7DE00C62A21 /* RealmStudentsTests.swift */; }; 16A41EDD2BE3A7DE00C62A21 /* SwiftUsersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A41EDA2BE3A7DE00C62A21 /* SwiftUsersTests.swift */; }; + E82FB4D82C2C50D7000E0FCC /* CoreStudentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E82FB4D62C2C50D7000E0FCC /* CoreStudentsTests.swift */; }; + E82FB4D92C2C50D7000E0FCC /* CoreUsersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E82FB4D72C2C50D7000E0FCC /* CoreUsersTests.swift */; }; + E82FB4E22C2C50E7000E0FCC /* CoreUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E82FB4DD2C2C50E7000E0FCC /* CoreUser.swift */; }; + E82FB4E32C2C50E7000E0FCC /* CoreStudentDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E82FB4DB2C2C50E7000E0FCC /* CoreStudentDB.swift */; }; + E82FB4E42C2C50E7000E0FCC /* CoreDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E82FB4E02C2C50E7000E0FCC /* CoreDatabase.swift */; }; + E82FB4E52C2C50E7000E0FCC /* CoreStudent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E82FB4DA2C2C50E7000E0FCC /* CoreStudent.swift */; }; + E82FB4E62C2C50E7000E0FCC /* CoreUserDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E82FB4DE2C2C50E7000E0FCC /* CoreUserDB.swift */; }; + E82FB4EA2C2C537B000E0FCC /* ManagedModels in Frameworks */ = {isa = PBXBuildFile; productRef = E82FB4E92C2C537B000E0FCC /* ManagedModels */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -62,6 +70,14 @@ 16A41ED82BE3A7DE00C62A21 /* SwiftStudentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftStudentsTests.swift; sourceTree = ""; }; 16A41ED92BE3A7DE00C62A21 /* RealmStudentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmStudentsTests.swift; sourceTree = ""; }; 16A41EDA2BE3A7DE00C62A21 /* SwiftUsersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUsersTests.swift; sourceTree = ""; }; + E82FB4D62C2C50D7000E0FCC /* CoreStudentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreStudentsTests.swift; sourceTree = ""; }; + E82FB4D72C2C50D7000E0FCC /* CoreUsersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUsersTests.swift; sourceTree = ""; }; + E82FB4DA2C2C50E7000E0FCC /* CoreStudent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreStudent.swift; sourceTree = ""; }; + E82FB4DB2C2C50E7000E0FCC /* CoreStudentDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreStudentDB.swift; sourceTree = ""; }; + E82FB4DD2C2C50E7000E0FCC /* CoreUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUser.swift; sourceTree = ""; }; + E82FB4DE2C2C50E7000E0FCC /* CoreUserDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUserDB.swift; sourceTree = ""; }; + E82FB4E02C2C50E7000E0FCC /* CoreDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDatabase.swift; sourceTree = ""; }; + E82FB4E72C2C5155000E0FCC /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -69,6 +85,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E82FB4EA2C2C537B000E0FCC /* ManagedModels in Frameworks */, 167973DC2BC8057C002FE4D9 /* RealmSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -158,6 +175,7 @@ 167973E22BC805D6002FE4D9 /* Database.swift */, 167973E12BC805B7002FE4D9 /* SwiftData */, 167973E02BC805B3002FE4D9 /* Realm */, + E82FB4E12C2C50E7000E0FCC /* CoreData */, ); path = DB; sourceTree = ""; @@ -190,6 +208,8 @@ 16A41ED92BE3A7DE00C62A21 /* RealmStudentsTests.swift */, 16A41ED82BE3A7DE00C62A21 /* SwiftStudentsTests.swift */, 16A41EDA2BE3A7DE00C62A21 /* SwiftUsersTests.swift */, + E82FB4D62C2C50D7000E0FCC /* CoreStudentsTests.swift */, + E82FB4D72C2C50D7000E0FCC /* CoreUsersTests.swift */, ); path = PerformanceTesting; sourceTree = ""; @@ -214,6 +234,34 @@ path = Resources; sourceTree = ""; }; + E82FB4DC2C2C50E7000E0FCC /* Students */ = { + isa = PBXGroup; + children = ( + E82FB4DA2C2C50E7000E0FCC /* CoreStudent.swift */, + E82FB4DB2C2C50E7000E0FCC /* CoreStudentDB.swift */, + ); + path = Students; + sourceTree = ""; + }; + E82FB4DF2C2C50E7000E0FCC /* Users */ = { + isa = PBXGroup; + children = ( + E82FB4DD2C2C50E7000E0FCC /* CoreUser.swift */, + E82FB4DE2C2C50E7000E0FCC /* CoreUserDB.swift */, + ); + path = Users; + sourceTree = ""; + }; + E82FB4E12C2C50E7000E0FCC /* CoreData */ = { + isa = PBXGroup; + children = ( + E82FB4DC2C2C50E7000E0FCC /* Students */, + E82FB4DF2C2C50E7000E0FCC /* Users */, + E82FB4E02C2C50E7000E0FCC /* CoreDatabase.swift */, + ); + path = CoreData; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -232,6 +280,7 @@ name = RealmVsSwiftData; packageProductDependencies = ( 167973DB2BC8057C002FE4D9 /* RealmSwift */, + E82FB4E92C2C537B000E0FCC /* ManagedModels */, ); productName = RealmVsSwiftData; productReference = 167973C72BC804B9002FE4D9 /* RealmVsSwiftData.app */; @@ -263,6 +312,7 @@ mainGroup = 167973BE2BC804B9002FE4D9; packageReferences = ( 167973DA2BC8057B002FE4D9 /* XCRemoteSwiftPackageReference "realm-swift" */, + E82FB4E82C2C537B000E0FCC /* XCRemoteSwiftPackageReference "ManagedModels" */, ); productRefGroup = 167973C82BC804B9002FE4D9 /* Products */; projectDirPath = ""; @@ -299,6 +349,11 @@ 167973E92BC805E0002FE4D9 /* SwiftUser.swift in Sources */, 16A41ED52BE38F6600C62A21 /* SchoolLocations.swift in Sources */, 167973E72BC805E0002FE4D9 /* SwiftDatabase.swift in Sources */, + E82FB4E22C2C50E7000E0FCC /* CoreUser.swift in Sources */, + E82FB4E32C2C50E7000E0FCC /* CoreStudentDB.swift in Sources */, + E82FB4E42C2C50E7000E0FCC /* CoreDatabase.swift in Sources */, + E82FB4E52C2C50E7000E0FCC /* CoreStudent.swift in Sources */, + E82FB4E62C2C50E7000E0FCC /* CoreUserDB.swift in Sources */, 16A41ED32BE38F3A00C62A21 /* SchoolNames.swift in Sources */, 16168E342BE0C36300874AC2 /* SwiftStudent.swift in Sources */, 167973FB2BC8063F002FE4D9 /* FirstNames.swift in Sources */, @@ -311,6 +366,8 @@ 16168E362BE0C36A00874AC2 /* RealmStudent.swift in Sources */, 16A41EDD2BE3A7DE00C62A21 /* SwiftUsersTests.swift in Sources */, 167973EF2BC805E4002FE4D9 /* RealmUserDB.swift in Sources */, + E82FB4D82C2C50D7000E0FCC /* CoreStudentsTests.swift in Sources */, + E82FB4D92C2C50D7000E0FCC /* CoreUsersTests.swift in Sources */, 16A41ED72BE3A7C400C62A21 /* RealmUsersTests.swift in Sources */, 167973ED2BC805E4002FE4D9 /* RelamDatabase.swift in Sources */, ); @@ -529,6 +586,14 @@ kind = branch; }; }; + E82FB4E82C2C537B000E0FCC /* XCRemoteSwiftPackageReference "ManagedModels" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Data-swift/ManagedModels.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.8.12; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -537,6 +602,11 @@ package = 167973DA2BC8057B002FE4D9 /* XCRemoteSwiftPackageReference "realm-swift" */; productName = RealmSwift; }; + E82FB4E92C2C537B000E0FCC /* ManagedModels */ = { + isa = XCSwiftPackageProductDependency; + package = E82FB4E82C2C537B000E0FCC /* XCRemoteSwiftPackageReference "ManagedModels" */; + productName = ManagedModels; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 167973BF2BC804B9002FE4D9 /* Project object */; diff --git a/RealmVsSwiftData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/RealmVsSwiftData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7aea61f..7620f83 100644 --- a/RealmVsSwiftData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/RealmVsSwiftData.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "2174eaf93f622d57be865cd78b51b72620c12528f1f0957292620977c4d69891", + "originHash" : "7d9a1b256e63f6d5d8efcb97b0a396631c321eca19f5bb8324f18e19e52d3f3e", "pins" : [ + { + "identity" : "managedmodels", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Data-swift/ManagedModels.git", + "state" : { + "revision" : "82baf8558c9f4c41b8c67bc1351729ea6308f29a", + "version" : "0.8.12" + } + }, { "identity" : "realm-core", "kind" : "remoteSourceControl", @@ -18,6 +27,15 @@ "branch" : "master", "revision" : "e3258a29f1993bef4fe3d9be801c7f83dc40eef5" } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" + } } ], "version" : 3 diff --git a/RealmVsSwiftData/DB/CoreData/CoreDatabase.swift b/RealmVsSwiftData/DB/CoreData/CoreDatabase.swift new file mode 100644 index 0000000..e2a25e8 --- /dev/null +++ b/RealmVsSwiftData/DB/CoreData/CoreDatabase.swift @@ -0,0 +1,84 @@ +// +// CoreDatabase.swift +// RealmVsCoreData +// +// Created by Jacob Bartlett on 03/04/2024. +// + +import Foundation +import ManagedModels + +protocol CoreContextRegistrator: NSManagedObject { + func insertWithAllDependencies(into context: ModelContext) +} + +protocol CoreDatabase: Database +where T: NSManagedObject & CoreContextRegistrator, + Sorting == NSSortDescriptor, + Filtering == NSPredicate { + var container: ModelContainer { get } + static var configuration: ModelConfiguration { get } + func update(execute: ( ModelContext ) throws -> Void) throws +} + +extension CoreDatabase { + + + var fileURL: URL? { + container.configurations.first?.url + } + + func create(_ items: [T]) throws { + let context = ModelContext(container) + items.forEach { $0.insertWithAllDependencies(into: context) } + try context.save() + } + + func create(_ item: T) throws { + let context = ModelContext(container) + item.insertWithAllDependencies(into: context) + try context.save() + } + + func read(predicate: Filtering?, sortBy sortDescriptors: [NSSortDescriptor]) throws -> [T] { + let context = ModelContext(container) + let fetchDescriptor = NSFetchRequest() + fetchDescriptor.entity = T.entity() + if let predicate { + fetchDescriptor.predicate = predicate + } + fetchDescriptor.sortDescriptors = sortDescriptors + return try context.fetch(fetchDescriptor) + } + + func update(execute: ( ModelContext ) throws -> Void) throws { + let context = ModelContext(container) + try execute(context) + try context.save() + } + + func delete(_ item: T) throws { + let context = ModelContext(container) + context.delete(item) + try context.save() + } + + func delete(_ items: [T]) throws { + let context = ModelContext(container) + let deleteRequest = NSBatchDeleteRequest(objectIDs: items.map(\.objectID)) + try context.execute(deleteRequest) + try context.save() + } + + func deleteAll() throws { // no bulk delete in CD? + let context = ModelContext(container) + let deleteRequest = NSBatchDeleteRequest(fetchRequest: T.fetchRequest()) + do { + try context.execute(deleteRequest) + try context.save() + } catch let error as NSError { + print("Could not delete all data. \(error), \(error.userInfo)") + fatalError("Did not delete all data") + } + } +} diff --git a/RealmVsSwiftData/DB/CoreData/Students/CoreStudent.swift b/RealmVsSwiftData/DB/CoreData/Students/CoreStudent.swift new file mode 100644 index 0000000..cf20f31 --- /dev/null +++ b/RealmVsSwiftData/DB/CoreData/Students/CoreStudent.swift @@ -0,0 +1,168 @@ +// +// CoreStudent.swift +// RealmVsCoreData +// +// Created by Jacob Bartlett on 30/04/2024. +// + +import Foundation +import ManagedModels + +enum CoreDataError: Error { + case dataNotFound +} + +@Model +final class CoreStudent: NSManagedObject, User { + + @Attribute(.unique) var id: UUID + var firstName: String + var surname: String + var age: Int + @Relationship(deleteRule: .cascade, inverse: \CoreSchool.students) var school: CoreSchool? + @Relationship(deleteRule: .cascade, inverse: \CoreGrade.student) var grades: [CoreGrade] + + convenience init(school: CoreSchool) throws { + guard let firstName = firstNames.randomElement(), + let surname = surnames.randomElement() else { + throw CoreDataError.dataNotFound + } + self.init(firstName: firstName, surname: surname, + age: Int.random(in: 11..<18), grades: CoreGrade.randomGrades(in: school.modelContext)) + } + + convenience init(id: UUID = UUID(), firstName: String, surname: String, age: Int, grades: [CoreGrade]) { + self.init(context: nil) + self.id = id + self.firstName = firstName + self.surname = surname + self.age = age + self.grades = grades + grades.forEach { $0.student = self } + } +} + +extension CoreStudent: CoreContextRegistrator { + func insertWithAllDependencies(into context: ModelContext) { + context.insert(self) + if let school, !context.insertedObjects.contains(school) { + context.insert(school) + } + precondition(!grades.isEmpty) + grades.forEach { context.insert($0) } + } +} + +@Model +final class CoreSchool: NSManagedObject { + + enum SchoolError: Error { + case dataNotFound + } + + enum SchoolType: String, Codable, CaseIterable { + case comprehensive + case grammar + case `private` + case religious + } + + @Attribute(.unique) var id: UUID + var name: String + var location: String + var type: SchoolType + var students: [CoreStudent] + + convenience init() { + guard let name = schoolNames.randomElement(), + let location = schoolLocations.randomElement(), + let type = SchoolType.allCases.randomElement() else { + //throw CoreDataError.dataNotFound + fatalError("Data not found") + } + self.init(name: name, location: location, type: type) + } + + convenience init(id: UUID = UUID(), name: String, location: String, type: SchoolType, students: [CoreStudent] = []) { + self.init(context: nil) + self.id = id + self.name = name + self.location = location + self.type = type + self.students = students + students.forEach { $0.school = self } + } +} + +@Model +final class CoreGrade: NSManagedObject { + + enum Subject: String, CaseIterable, CustomStringConvertible { + case maths + case english + case literature + case physics + case chemistry + case biology + case history + case geography + case french + case german + case music + case art + case physicalEducation + case religiousStudies + var description: String { rawValue } + } + + enum Grade: String, CaseIterable, CustomStringConvertible { + case aStar + case a + case b + case c + case d + case e + case f + case u + var description: String { rawValue } + } + + enum ExamBoard: String, CaseIterable, CustomStringConvertible { + case aqa + case edexcel + case ocr + var description: String { rawValue } + } + + @Attribute(.unique) var id: UUID + // CoreData macros and predicates don't behave well with enums + var subject: Subject + var grade: Grade + var examBoard: ExamBoard + var student: CoreStudent? + + convenience init() { + guard let subject = Subject.allCases.randomElement(), + let grade = Grade.allCases.randomElement(), + let examBoard = ExamBoard.allCases.randomElement() else { + //throw CoreDataError.dataNotFound + fatalError("Data not found") + } + self.init(subject: subject, examBoard: examBoard, grade: grade) + } + + convenience init(id: UUID = UUID(), subject: Subject, examBoard: ExamBoard, grade: Grade) { + self.init(context: nil) + self.id = id + self.subject = subject + self.examBoard = examBoard + self.grade = grade + } + + static func randomGrades(in ctx: ModelContext?) -> [CoreGrade] { + let max = Int.random(in: 1..<7) + return (0...max).compactMap { _ in + CoreGrade() + } + } +} diff --git a/RealmVsSwiftData/DB/CoreData/Students/CoreStudentDB.swift b/RealmVsSwiftData/DB/CoreData/Students/CoreStudentDB.swift new file mode 100644 index 0000000..8eb8ad6 --- /dev/null +++ b/RealmVsSwiftData/DB/CoreData/Students/CoreStudentDB.swift @@ -0,0 +1,22 @@ +// +// CoreStudentDB.swift +// RealmVsCoreData +// +// Created by Jacob Bartlett on 30/04/2024. +// + +import Foundation +import ManagedModels + +final class CoreStudentDB: CoreDatabase { + + static let schema = Schema.model(for: [ CoreStudent.self, CoreSchool.self, CoreGrade.self ]) + static let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + typealias T = CoreStudent + + let container : ModelContainer + + init() throws { + container = try ModelContainer(for: Self.schema, configurations: Self.configuration) + } +} diff --git a/RealmVsSwiftData/DB/CoreData/Users/CoreUser.swift b/RealmVsSwiftData/DB/CoreData/Users/CoreUser.swift new file mode 100644 index 0000000..3a2fafe --- /dev/null +++ b/RealmVsSwiftData/DB/CoreData/Users/CoreUser.swift @@ -0,0 +1,45 @@ +// +// User.swift +// RealmVsCoreData +// +// Created by Jacob Bartlett on 02/04/2024. +// + +import Foundation +import ManagedModels + +@Model +final class CoreUser: NSManagedObject, User { + + enum UserError: Error { + case nameNotFound + } + + @Attribute(.unique) var id: UUID + var firstName: String + var surname: String + var age: Int + + convenience init() { + guard let firstName = firstNames.randomElement(), + let surname = surnames.randomElement() else { + //throw UserError.nameNotFound + fatalError("Name not found") + } + self.init(firstName: firstName, surname: surname, age: Int.random(in: 0..<99)) + } + + convenience init(firstName: String, surname: String, age: Int) { + self.init(context: nil) + self.id = UUID() + self.firstName = firstName + self.surname = surname + self.age = age + } +} + +extension CoreUser: CoreContextRegistrator { + func insertWithAllDependencies(into context: ModelContext) { + context.insert(self) + } +} diff --git a/RealmVsSwiftData/DB/CoreData/Users/CoreUserDB.swift b/RealmVsSwiftData/DB/CoreData/Users/CoreUserDB.swift new file mode 100644 index 0000000..0cecc85 --- /dev/null +++ b/RealmVsSwiftData/DB/CoreData/Users/CoreUserDB.swift @@ -0,0 +1,23 @@ +// +// CoreUserDB.swift +// RealmVsCoreData +// +// Created by Jacob Bartlett on 02/04/2024. +// + +import Foundation +import ManagedModels + +final class CoreUserDB: CoreDatabase { + + static let schema = Schema([ CoreUser.self ], version: .init(1, 0, 0)) + static let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + + typealias T = CoreUser + + let container : ModelContainer + + init() throws { + container = try ModelContainer(for: Self.schema, configurations: Self.configuration) + } +} diff --git a/RealmVsSwiftData/PerformanceTesting/CoreStudentsTests.swift b/RealmVsSwiftData/PerformanceTesting/CoreStudentsTests.swift new file mode 100644 index 0000000..daf3b76 --- /dev/null +++ b/RealmVsSwiftData/PerformanceTesting/CoreStudentsTests.swift @@ -0,0 +1,74 @@ +// +// CoreStudentsTests.swift +// RealmVsCoreData +// +// Created by Jacob Bartlett on 02/05/2024. +// + +import Foundation +import ManagedModels + +func coreStudentsPerformanceTests(with studentsCount: Int = 10_000) { + + announce("CoreData: \(formatNumberWithCommas(studentsCount)) Complex Objects") + + let db = try! CoreStudentDB() + try! db.deleteAll() + var students = [CoreStudent]() + + logExecutionTime("Student instantiation") { + let schoolCount = 100 + let firstSchool = CoreSchool(name: "Falconwood College", location: "Falconwood", type: .comprehensive) + let schools = [firstSchool] + (1.. 0", + argumentArray: [ physics, aStarGrade ] + ) + let studentsWithAStarInPhysics = try! db.read(predicate: predicate) + print("\(studentsWithAStarInPhysics.count) students with an A* in Physics") + } + + logExecutionTime("Fail the cheating Maths students") { + let maths = CoreGrade.Subject.maths + let predicate = NSPredicate( + format: "school.name == %@ AND SUBQUERY(grades, $grade, $grade.subject = %@).@count > 0", + argumentArray: [ "Falconwood College", maths ] + ) + + try! db.update { context in + let fetchDescriptor = NSFetchRequest() + fetchDescriptor.entity = CoreStudent.entity() + fetchDescriptor.predicate = predicate + let studentsWhoCheatedAtMaths = try context.fetch(fetchDescriptor) + + for student in studentsWhoCheatedAtMaths { + for grade in student.grades where grade.subject == maths { + grade.grade = .f + } + } + try context.save() + print("De-graded \(studentsWhoCheatedAtMaths.count) students.") + } + } + + measureSize(of: db) + + logExecutionTime("Delete all students") { + try! db.deleteAll() + } +} diff --git a/RealmVsSwiftData/PerformanceTesting/CoreUsersTests.swift b/RealmVsSwiftData/PerformanceTesting/CoreUsersTests.swift new file mode 100644 index 0000000..0dab273 --- /dev/null +++ b/RealmVsSwiftData/PerformanceTesting/CoreUsersTests.swift @@ -0,0 +1,56 @@ +// +// CoreUsersTests.swift +// RealmVsCoreData +// +// Created by Jacob Bartlett on 02/05/2024. +// + +import Foundation +import ManagedModels + +func coreUsersPerformanceTests(with usersCount: Int = 100_000) { + + announce("CoreData: \(formatNumberWithCommas(usersCount)) Simple Objects") + + let db = try! CoreUserDB() + + try! db.deleteAll() + + var users = [CoreUser]() + logExecutionTime("User instantiation") { + users = (0..() + fetchDescriptor.entity = CoreUser.entity() + fetchDescriptor.predicate = predicate + let janes = try ctx.fetch(fetchDescriptor) + print("\(janes.count) users named `Jane` being renamed to `Wendy`") + for jane in janes { + jane.firstName = "Wendy" + } + try ctx.save() + print("\(janes.count) users renamed to `Wendy`") + } + } + + measureSize(of: db) + + logExecutionTime("Delete all users") { + try! db.deleteAll() + } +} diff --git a/RealmVsSwiftData/PerformanceTesting/TestUtils.swift b/RealmVsSwiftData/PerformanceTesting/TestUtils.swift index aaf4f75..6db3dc5 100644 --- a/RealmVsSwiftData/PerformanceTesting/TestUtils.swift +++ b/RealmVsSwiftData/PerformanceTesting/TestUtils.swift @@ -8,11 +8,14 @@ import Foundation func runAllPerformanceTests() { - for num in [100, 1_000, 10_000, 100_000, 1_000_000] { //, 2_000_000, 10_000_000] { swiftUsersPerformanceTests(with: num) } + for num in [100, 1_000, 10_000, 100_000, 1_000_000] { //, 2_000_000, 10_000_000] { + coreUsersPerformanceTests(with: num) + } + for num in [100, 1_000, 10_000, 100_000, 1_000_000, 2_000_000] { //, 10_000_000] { realmUsersPerformanceTests(with: num) } @@ -21,10 +24,14 @@ func runAllPerformanceTests() { swiftStudentsPerformanceTests(with: num) } + for num in [100, 1_000, 10_000, 100_000, 200_000] { //, 1_000_000] { + coreStudentsPerformanceTests(with: num) + } + for num in [100, 1_000, 10_000, 100_000, 200_000, 1_000_000] { realmStudentsPerformanceTests(with: num) } - + deleteAllDatabaseFiles() } @@ -35,6 +42,12 @@ func deleteAllDatabaseFiles() { if let swiftStudentDB = try? SwiftStudentDB() { delete(db: swiftStudentDB) } + if let coreUserDB = try? CoreUserDB() { + delete(db: coreUserDB) + } + if let coreStudentDB = try? CoreStudentDB() { + delete(db: coreStudentDB) + } let realmUserDB = RealmUserDB() delete(db: realmUserDB) let realmStudentDB = RealmStudentDB() From e6be90b2cd88d702a437144b470ba83c1187d281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Fri, 28 Jun 2024 13:44:31 +0200 Subject: [PATCH 5/5] Give databases a unique name Otherwise they seem to clash w/ SwiftData? Reporting b0rked SQLite databases. (cherry picked from commit f89184c8fd0c4bd74a192675a0aa61630aae0e81) --- RealmVsSwiftData/DB/CoreData/Students/CoreStudentDB.swift | 2 +- RealmVsSwiftData/DB/CoreData/Users/CoreUserDB.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RealmVsSwiftData/DB/CoreData/Students/CoreStudentDB.swift b/RealmVsSwiftData/DB/CoreData/Students/CoreStudentDB.swift index 8eb8ad6..d8f17b6 100644 --- a/RealmVsSwiftData/DB/CoreData/Students/CoreStudentDB.swift +++ b/RealmVsSwiftData/DB/CoreData/Students/CoreStudentDB.swift @@ -11,7 +11,7 @@ import ManagedModels final class CoreStudentDB: CoreDatabase { static let schema = Schema.model(for: [ CoreStudent.self, CoreSchool.self, CoreGrade.self ]) - static let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + static let configuration = ModelConfiguration(name: "CoreStudentDB", schema: schema, isStoredInMemoryOnly: false) typealias T = CoreStudent let container : ModelContainer diff --git a/RealmVsSwiftData/DB/CoreData/Users/CoreUserDB.swift b/RealmVsSwiftData/DB/CoreData/Users/CoreUserDB.swift index 0cecc85..51066dc 100644 --- a/RealmVsSwiftData/DB/CoreData/Users/CoreUserDB.swift +++ b/RealmVsSwiftData/DB/CoreData/Users/CoreUserDB.swift @@ -11,7 +11,7 @@ import ManagedModels final class CoreUserDB: CoreDatabase { static let schema = Schema([ CoreUser.self ], version: .init(1, 0, 0)) - static let configuration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + static let configuration = ModelConfiguration(name: "CoreUserDB", schema: schema, isStoredInMemoryOnly: false) typealias T = CoreUser