diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 1d9be48f4f7..d5a23b4859b 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [fixed] Fixed memory leak in `Query.whereField()`. (#13978) + # 11.8.0 - [fixed] Fixed use-after-free bug when internally formatting strings. (#14306) - [changed] Update gRPC dependency to 1.69. diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 7b6e8450bf1..15abfd2adfb 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -259,6 +259,7 @@ 258B372CF33B7E7984BBA659 /* fake_target_metadata_provider.cc in Sources */ = {isa = PBXBuildFile; fileRef = 71140E5D09C6E76F7C71B2FC /* fake_target_metadata_provider.cc */; }; 25A75DFA730BAD21A5538EC5 /* document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D821C2DDC800EFB9CC /* document.pb.cc */; }; 25C167BAA4284FC951206E1F /* FIRFirestoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FAFF203E56F8009C9584 /* FIRFirestoreTests.mm */; }; + 25D74F38A5EE96CC653ABB49 /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; }; 25FE27330996A59F31713A0C /* FIRDocumentReferenceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E049202154AA00B64F25 /* FIRDocumentReferenceTests.mm */; }; 2618255E63631038B64DF3BB /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D8E530B27D5641B9C26A452C /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json */; }; 2620644052E960310DADB298 /* FIRFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */; }; @@ -400,6 +401,7 @@ 3CCABD7BB5ED39DF1140B5F0 /* leveldb_globals_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = FC44D934D4A52C790659C8D6 /* leveldb_globals_cache_test.cc */; }; 3CFFA6F016231446367E3A69 /* listen_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */; }; 3D22F56C0DE7C7256C75DC06 /* tree_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4D20A36DBB00BCEB75 /* tree_sorted_map_test.cc */; }; + 3D6AC48D6197E6539BBBD28F /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; }; 3D9619906F09108E34FF0C95 /* FSTSmokeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07C202154EB00B64F25 /* FSTSmokeTests.mm */; }; 3DBB48F077C97200F32B51A0 /* value_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 40F9D09063A07F710811A84F /* value_util_test.cc */; }; 3DBBC644BE08B140BCC23BD5 /* string_apple_benchmark.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */; }; @@ -432,6 +434,7 @@ 44A8B51C05538A8DACB85578 /* byte_stream_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 432C71959255C5DBDF522F52 /* byte_stream_test.cc */; }; 44C4244E42FFFB6E9D7F28BA /* byte_stream_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 432C71959255C5DBDF522F52 /* byte_stream_test.cc */; }; 44EAF3E6EAC0CC4EB2147D16 /* transform_operation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 33607A3AE91548BD219EC9C6 /* transform_operation_test.cc */; }; + 451EFFB413364E5A420F8B2D /* thread_safe_memoizer_testing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */; }; 4562CDD90F5FF0491F07C5DA /* leveldb_opener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 75860CD13AF47EB1EA39EC2F /* leveldb_opener_test.cc */; }; 457171CE2510EEA46F7D8A30 /* FIRFirestoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FAFF203E56F8009C9584 /* FIRFirestoreTests.mm */; }; 45939AFF906155EA27D281AB /* annotations.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9520B89AAC00B5BCE7 /* annotations.pb.cc */; }; @@ -452,6 +455,7 @@ 479A392EAB42453D49435D28 /* memory_bundle_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB4AB1388538CD3CB19EB028 /* memory_bundle_cache_test.cc */; }; 47B8ED6737A24EF96B1ED318 /* garbage_collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = AAED89D7690E194EF3BA1132 /* garbage_collection_spec_test.json */; }; 4809D7ACAA9414E3192F04FF /* FIRGeoPointTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */; }; + 482D503CC826265FCEAB53DE /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; }; 485CBA9F99771437BA1CB401 /* event_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6F57521E161450FAF89075ED /* event_manager_test.cc */; }; 48720B5768AFA2B2F3E14C04 /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D8E530B27D5641B9C26A452C /* Validation_BloomFilterTest_MD5_500_1_bloom_filter_proto.json */; }; 48926FF55484E996B474D32F /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = DD990FD89C165F4064B4F608 /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json */; }; @@ -783,6 +787,7 @@ 67B8C34BDF0FFD7532D7BE4F /* Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 478DC75A0DCA6249A616DD30 /* Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json */; }; 67BC2B77C1CC47388E79D774 /* FIRSnapshotMetadataTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */; }; 67CF9FAA890307780731E1DA /* task_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 899FC22684B0F7BEEAE13527 /* task_test.cc */; }; + 688AC36AA9D0677E910D5A37 /* thread_safe_memoizer_testing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */; }; 6938575C8B5E6FE0D562547A /* exponential_backoff_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */; }; 6938ABD1891AD4B9FD5FE664 /* document_overlay_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = FFCA39825D9678A03D1845D0 /* document_overlay_cache_test.cc */; }; 69D3AD697D1A7BF803A08160 /* field_index_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BF76A8DA34B5B67B4DD74666 /* field_index_test.cc */; }; @@ -873,6 +878,7 @@ 77C5703230DB77F0540D1F89 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4375BDCDBCA9938C7F086730 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json */; }; 77D38E78F7CCB8504450A8FB /* index.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 395E8B07639E69290A929695 /* index.pb.cc */; }; 77D3CF0BE43BC67B9A26B06D /* FIRFieldPathTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04C202154AA00B64F25 /* FIRFieldPathTests.mm */; }; + 7801E06BFFB08FCE7AB54AD6 /* thread_safe_memoizer_testing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */; }; 784FCB02C76096DACCBA11F2 /* bundle.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = A366F6AE1A5A77548485C091 /* bundle.pb.cc */; }; 78D99CDBB539B0AEE0029831 /* Validation_BloomFilterTest_MD5_50000_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 3841925AA60E13A027F565E6 /* Validation_BloomFilterTest_MD5_50000_1_membership_test_result.json */; }; 78E8DDDBE131F3DA9AF9F8B8 /* index.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 395E8B07639E69290A929695 /* index.pb.cc */; }; @@ -991,6 +997,7 @@ 8C602DAD4E8296AB5EFB962A /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; 8C82D4D3F9AB63E79CC52DC8 /* Pods_Firestore_IntegrationTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */; }; 8D0EF43F1B7B156550E65C20 /* FSTGoogleTestTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */; }; + 8D67BAAD6D2F1913BACA6AC1 /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; }; 8DBA8DC55722ED9D3A1BB2C9 /* Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 1A7D48A017ECB54FD381D126 /* Validation_BloomFilterTest_MD5_5000_1_membership_test_result.json */; }; 8E103A426D6E650DC338F281 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C8FB22BCB9F454DA44BA80C8 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json */; }; 8E41D53C77C30372840B0367 /* Validation_BloomFilterTest_MD5_5000_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 728F617782600536F2561463 /* Validation_BloomFilterTest_MD5_5000_0001_bloom_filter_proto.json */; }; @@ -1114,6 +1121,7 @@ A728A4D7FA17F9F3257E0002 /* Validation_BloomFilterTest_MD5_5000_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C8582DFD74E8060C7072104B /* Validation_BloomFilterTest_MD5_5000_0001_membership_test_result.json */; }; A7309DAD4A3B5334536ECA46 /* remote_event_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 584AE2C37A55B408541A6FF3 /* remote_event_test.cc */; }; A7399FB3BEC50BBFF08EC9BA /* mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3068AA9DFBBA86C1FE2A946E /* mutation_queue_test.cc */; }; + A7669E72BCED7FBADA4B1314 /* thread_safe_memoizer_testing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */; }; A80D38096052F928B17E1504 /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = CCC9BD953F121B9E29F9AA42 /* user_test.cc */; }; A833A216988ADFD4876763CD /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C8FB22BCB9F454DA44BA80C8 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json */; }; A841EEB5A94A271523EAE459 /* Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = A5D9044B72061CAF284BC9E4 /* Validation_BloomFilterTest_MD5_50000_0001_bloom_filter_proto.json */; }; @@ -1268,6 +1276,7 @@ BC8DFBCB023DBD914E27AA7D /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; BCA720A0F54D23654F806323 /* ConditionalConformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3228F51DCDC2E90D5C58F97 /* ConditionalConformanceTests.swift */; }; BCAC9F7A865BD2320A4D8752 /* bloom_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2E6F09AD1EE0A6A452E9A08 /* bloom_filter_test.cc */; }; + BD0882A40BD8AE042629C179 /* thread_safe_memoizer_testing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */; }; BD3A421C9E40C57D25697E75 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BD051DBE754950FEAC7A446 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json */; }; BD6CC8614970A3D7D2CF0D49 /* exponential_backoff_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */; }; BDD2D1812BAD962E3C81A53F /* hashing_test_apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = B69CF3F02227386500B281C8 /* hashing_test_apple.mm */; }; @@ -1284,6 +1293,7 @@ BFEAC4151D3AA8CE1F92CC2D /* FSTSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03020213FFC00B64F25 /* FSTSpecTests.mm */; }; C02A969BF4BB63ABCB531B4B /* create_noop_connectivity_monitor.cc in Sources */ = {isa = PBXBuildFile; fileRef = CF39535F2C41AB0006FA6C0E /* create_noop_connectivity_monitor.cc */; }; C06E54352661FCFB91968640 /* mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3068AA9DFBBA86C1FE2A946E /* mutation_queue_test.cc */; }; + C099AEC05D44976755BA32A2 /* thread_safe_memoizer_testing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */; }; C09BDBA73261578F9DA74CEE /* firebase_auth_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = F869D85E900E5AF6CD02E2FC /* firebase_auth_credentials_provider_test.mm */; }; C0AD8DB5A84CAAEE36230899 /* status_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352C20A3B3D7003E0143 /* status_test.cc */; }; C0EFC5FB79517679C377C252 /* schedule_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9B0B005A79E765AF02793DCE /* schedule_test.cc */; }; @@ -1358,6 +1368,7 @@ CE2962775B42BDEEE8108567 /* leveldb_lru_garbage_collector_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B629525F7A1AAC1AB765C74F /* leveldb_lru_garbage_collector_test.cc */; }; CE411D4B70353823DE63C0D5 /* bundle_loader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A853C81A6A5A51C9D0389EDA /* bundle_loader_test.cc */; }; CEA91CE103B42533C54DBAD6 /* memory_remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1CA9800A53669EFBFFB824E3 /* memory_remote_document_cache_test.cc */; }; + CF18D52A88F4F6F62C5495EF /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; }; CF1FB026CCB901F92B4B2C73 /* watch_change_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2D7472BC70C024D736FF74D9 /* watch_change_test.cc */; }; CF5DE1ED21DD0A9783383A35 /* CodableIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */; }; CFA4A635ECD105D2044B3692 /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3355BE9391CC4857AF0BDAE3 /* DatabaseTests.swift */; }; @@ -1409,6 +1420,7 @@ D756A1A63E626572EE8DF592 /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; D77941FD93DBE862AEF1F623 /* FSTTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07B202154EB00B64F25 /* FSTTransactionTests.mm */; }; D91D86B29B86A60C05879A48 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; }; + D928302820891CCCAD0437DD /* thread_safe_memoizer_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */; }; D9366A834BFF13246DC3AF9E /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; }; D94A1862B8FB778225DB54A1 /* filesystem_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F51859B394D01C0C507282F1 /* filesystem_test.cc */; }; D98430EA4FAA357D855FA50F /* orderby_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */; }; @@ -1725,6 +1737,7 @@ 214877F52A705012D6720CA0 /* object_value_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = object_value_test.cc; sourceTree = ""; }; 2220F583583EFC28DE792ABE /* Pods_Firestore_IntegrationTests_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2286F308EFB0534B1BDE05B9 /* memory_target_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_target_cache_test.cc; sourceTree = ""; }; + 26DDBA115DEB88631B93F203 /* thread_safe_memoizer_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = thread_safe_memoizer_testing.h; sourceTree = ""; }; 277EAACC4DD7C21332E8496A /* lru_garbage_collector_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = lru_garbage_collector_test.cc; sourceTree = ""; }; 28B45B2104E2DAFBBF86DBB7 /* logic_utils_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = logic_utils_test.cc; sourceTree = ""; }; 29D9C76922DAC6F710BC1EF4 /* memory_document_overlay_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_document_overlay_cache_test.cc; sourceTree = ""; }; @@ -1932,6 +1945,7 @@ 69E6C311558EC77729A16CF1 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; sourceTree = ""; }; 6A7A30A2DB3367E08939E789 /* bloom_filter.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = bloom_filter.pb.h; sourceTree = ""; }; 6AE927CDFC7A72BF825BE4CB /* Pods-Firestore_Tests_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_tvOS/Pods-Firestore_Tests_tvOS.release.xcconfig"; sourceTree = ""; }; + 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = thread_safe_memoizer_testing.cc; sourceTree = ""; }; 6E8302DE210222ED003E1EA3 /* FSTFuzzTestFieldPath.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTFuzzTestFieldPath.h; sourceTree = ""; }; 6E8302DF21022309003E1EA3 /* FSTFuzzTestFieldPath.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTFuzzTestFieldPath.mm; sourceTree = ""; }; 6EA39FDD20FE820E008D461F /* FSTFuzzTestSerializer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTFuzzTestSerializer.mm; sourceTree = ""; }; @@ -2109,6 +2123,7 @@ E42355285B9EF55ABD785792 /* Pods_Firestore_Example_macOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_macOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E592181BFD7C53C305123739 /* Pods-Firestore_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.debug.xcconfig"; sourceTree = ""; }; E76F0CDF28E5FA62D21DE648 /* leveldb_target_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_target_cache_test.cc; sourceTree = ""; }; + EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = thread_safe_memoizer_testing_test.cc; sourceTree = ""; }; ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRVectorValueTests.mm; sourceTree = ""; }; EF6C285029E462A200A7D4F1 /* FIRAggregateTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRAggregateTests.mm; sourceTree = ""; }; @@ -2412,6 +2427,9 @@ 899FC22684B0F7BEEAE13527 /* task_test.cc */, A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */, 1A8141230C7E3986EACEF0B6 /* thread_safe_memoizer_test.cc */, + 6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */, + 26DDBA115DEB88631B93F203 /* thread_safe_memoizer_testing.h */, + EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */, B68B1E002213A764008977EF /* to_string_apple_test.mm */, B696858D2214B53900271095 /* to_string_test.cc */, ); @@ -4327,6 +4345,8 @@ 9B2C6A48A4DBD36080932B4E /* testing_hooks_test.cc in Sources */, 32A95242C56A1A230231DB6A /* testutil.cc in Sources */, 51018EA27CF914DD1CC79CB3 /* thread_safe_memoizer_test.cc in Sources */, + 482D503CC826265FCEAB53DE /* thread_safe_memoizer_testing.cc in Sources */, + 451EFFB413364E5A420F8B2D /* thread_safe_memoizer_testing_test.cc in Sources */, 5497CB78229DECDE000FB92F /* time_testing.cc in Sources */, ACC9369843F5ED3BD2284078 /* timestamp_test.cc in Sources */, 2AAEABFD550255271E3BAC91 /* to_string_apple_test.mm in Sources */, @@ -4548,6 +4568,8 @@ 24B75C63BDCD5551B2F69901 /* testing_hooks_test.cc in Sources */, 8388418F43042605FB9BFB92 /* testutil.cc in Sources */, 5BB33F0BC7960D26062B07D3 /* thread_safe_memoizer_test.cc in Sources */, + 3D6AC48D6197E6539BBBD28F /* thread_safe_memoizer_testing.cc in Sources */, + 7801E06BFFB08FCE7AB54AD6 /* thread_safe_memoizer_testing_test.cc in Sources */, 5497CB79229DECDE000FB92F /* time_testing.cc in Sources */, 26CB3D7C871BC56456C6021E /* timestamp_test.cc in Sources */, 5BE49546D57C43DDFCDB6FBD /* to_string_apple_test.mm in Sources */, @@ -4793,6 +4815,8 @@ D0DA42DC66C4FE508A63B269 /* testing_hooks_test.cc in Sources */, 409C0F2BFC2E1BECFFAC4D32 /* testutil.cc in Sources */, B31B5E0D4EA72C5916CC71F5 /* thread_safe_memoizer_test.cc in Sources */, + 25D74F38A5EE96CC653ABB49 /* thread_safe_memoizer_testing.cc in Sources */, + 688AC36AA9D0677E910D5A37 /* thread_safe_memoizer_testing_test.cc in Sources */, 6300709ECDE8E0B5A8645F8D /* time_testing.cc in Sources */, 0CEE93636BA4852D3C5EC428 /* timestamp_test.cc in Sources */, 95DCD082374F871A86EF905F /* to_string_apple_test.mm in Sources */, @@ -5038,6 +5062,8 @@ F6738D3B72352BBEFB87172C /* testing_hooks_test.cc in Sources */, A17DBC8F24127DA8A381F865 /* testutil.cc in Sources */, 09B83B26E47B6F6668DF54B8 /* thread_safe_memoizer_test.cc in Sources */, + CF18D52A88F4F6F62C5495EF /* thread_safe_memoizer_testing.cc in Sources */, + A7669E72BCED7FBADA4B1314 /* thread_safe_memoizer_testing_test.cc in Sources */, A25FF76DEF542E01A2DF3B0E /* time_testing.cc in Sources */, 1E42CD0F60EB22A5D0C86D1F /* timestamp_test.cc in Sources */, F9705E595FC3818F13F6375A /* to_string_apple_test.mm in Sources */, @@ -5269,6 +5295,8 @@ F184E5367DF3CA158EDE8532 /* testing_hooks_test.cc in Sources */, 54A0352A20A3B3BD003E0143 /* testutil.cc in Sources */, 20A93AC59CD5A7AC41F10412 /* thread_safe_memoizer_test.cc in Sources */, + 8D67BAAD6D2F1913BACA6AC1 /* thread_safe_memoizer_testing.cc in Sources */, + BD0882A40BD8AE042629C179 /* thread_safe_memoizer_testing_test.cc in Sources */, 5497CB77229DECDE000FB92F /* time_testing.cc in Sources */, ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */, B68B1E012213A765008977EF /* to_string_apple_test.mm in Sources */, @@ -5533,6 +5561,8 @@ 5360D52DCAD1069B1E4B0B9D /* testing_hooks_test.cc in Sources */, CA989C0E6020C372A62B7062 /* testutil.cc in Sources */, 6DFD49CCE2281CE243FEBB63 /* thread_safe_memoizer_test.cc in Sources */, + D928302820891CCCAD0437DD /* thread_safe_memoizer_testing.cc in Sources */, + C099AEC05D44976755BA32A2 /* thread_safe_memoizer_testing_test.cc in Sources */, 2D220B9ABFA36CD7AC43D0A7 /* time_testing.cc in Sources */, D91D86B29B86A60C05879A48 /* timestamp_test.cc in Sources */, 60260A06871DCB1A5F3448D3 /* to_string_apple_test.mm in Sources */, diff --git a/Firestore/core/src/core/composite_filter.cc b/Firestore/core/src/core/composite_filter.cc index 7bbb81abc57..02186989330 100644 --- a/Firestore/core/src/core/composite_filter.cc +++ b/Firestore/core/src/core/composite_filter.cc @@ -17,6 +17,7 @@ #include "Firestore/core/src/core/composite_filter.h" #include +#include #include #include "Firestore/core/src/core/field_filter.h" @@ -141,16 +142,14 @@ const FieldFilter* CompositeFilter::Rep::FindFirstMatchingFilter( return nullptr; } -const std::vector& CompositeFilter::Rep::GetFlattenedFilters() - const { - return memoized_flattened_filters_->memoize([&]() { - std::vector flattened_filters; - for (const auto& filter : filters()) - std::copy(filter.GetFlattenedFilters().begin(), - filter.GetFlattenedFilters().end(), - std::back_inserter(flattened_filters)); - return flattened_filters; - }); +std::shared_ptr> +CompositeFilter::Rep::CalculateFlattenedFilters() const { + auto flattened_filters = std::make_shared>(); + for (const auto& filter : filters()) + std::copy(filter.GetFlattenedFilters().begin(), + filter.GetFlattenedFilters().end(), + std::back_inserter(*flattened_filters)); + return flattened_filters; } } // namespace core diff --git a/Firestore/core/src/core/composite_filter.h b/Firestore/core/src/core/composite_filter.h index 24671d3e44e..2858271a358 100644 --- a/Firestore/core/src/core/composite_filter.h +++ b/Firestore/core/src/core/composite_filter.h @@ -138,7 +138,8 @@ class CompositeFilter : public Filter { return filters_.empty(); } - const std::vector& GetFlattenedFilters() const override; + std::shared_ptr> CalculateFlattenedFilters() + const override; std::vector GetFilters() const override { return filters(); diff --git a/Firestore/core/src/core/field_filter.cc b/Firestore/core/src/core/field_filter.cc index b68956c8a52..2867e3ba8ba 100644 --- a/Firestore/core/src/core/field_filter.cc +++ b/Firestore/core/src/core/field_filter.cc @@ -16,6 +16,7 @@ #include "Firestore/core/src/core/field_filter.h" +#include #include #include "Firestore/core/src/core/array_contains_any_filter.h" @@ -122,12 +123,12 @@ FieldFilter::FieldFilter(std::shared_ptr rep) : Filter(std::move(rep)) { } -const std::vector& FieldFilter::Rep::GetFlattenedFilters() const { +std::shared_ptr> +FieldFilter::Rep::CalculateFlattenedFilters() const { // This is already a field filter, so we return a vector of size one. - return memoized_flattened_filters_->memoize([&]() { - return std::vector{ - FieldFilter(std::make_shared(*this))}; - }); + auto filters = std::make_shared>(); + filters->push_back(FieldFilter(std::make_shared(*this))); + return filters; } std::vector FieldFilter::Rep::GetFilters() const { diff --git a/Firestore/core/src/core/field_filter.h b/Firestore/core/src/core/field_filter.h index 48219f222f2..2f03254e1a1 100644 --- a/Firestore/core/src/core/field_filter.h +++ b/Firestore/core/src/core/field_filter.h @@ -117,8 +117,6 @@ class FieldFilter : public Filter { return false; } - const std::vector& GetFlattenedFilters() const override; - std::vector GetFilters() const override; protected: @@ -140,6 +138,9 @@ class FieldFilter : public Filter { bool MatchesComparison(util::ComparisonResult comparison) const; + std::shared_ptr> CalculateFlattenedFilters() + const override; + private: friend class FieldFilter; diff --git a/Firestore/core/src/core/filter.cc b/Firestore/core/src/core/filter.cc index a77ccc55e34..853c15b31ba 100644 --- a/Firestore/core/src/core/filter.cc +++ b/Firestore/core/src/core/filter.cc @@ -35,12 +35,6 @@ std::ostream& operator<<(std::ostream& os, const Filter& filter) { return os << filter.ToString(); } -Filter::Rep::Rep() - : memoized_flattened_filters_( - std::make_shared< - util::ThreadSafeMemoizer>>()) { -} - } // namespace core } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/core/filter.h b/Firestore/core/src/core/filter.h index ec20120daf6..bab79599cc6 100644 --- a/Firestore/core/src/core/filter.h +++ b/Firestore/core/src/core/filter.h @@ -17,6 +17,7 @@ #ifndef FIRESTORE_CORE_SRC_CORE_FILTER_H_ #define FIRESTORE_CORE_SRC_CORE_FILTER_H_ +#include #include #include #include @@ -114,7 +115,7 @@ class Filter { protected: class Rep { public: - Rep(); + Rep() = default; virtual ~Rep() = default; @@ -147,20 +148,23 @@ class Filter { virtual bool IsEmpty() const = 0; - virtual const std::vector& GetFlattenedFilters() const = 0; + virtual const std::vector& GetFlattenedFilters() const { + const auto func = std::bind(&Rep::CalculateFlattenedFilters, this); + return memoized_flattened_filters_.value(func); + } virtual std::vector GetFilters() const = 0; + protected: + virtual std::shared_ptr> + CalculateFlattenedFilters() const = 0; + + private: /** * Memoized list of all field filters that can be found by * traversing the tree of filters contained in this composite filter. - * - * Use a `std::shared_ptr` rather than using - * `ThreadSafeMemoizer` directly so that this class is copyable - * (`ThreadSafeMemoizer` is not copyable because of its `std::once_flag` - * member variable, which is not copyable). */ - mutable std::shared_ptr>> + mutable util::ThreadSafeMemoizer> memoized_flattened_filters_; }; diff --git a/Firestore/core/src/core/query.cc b/Firestore/core/src/core/query.cc index 5b70ebc322f..1c8394748d9 100644 --- a/Firestore/core/src/core/query.cc +++ b/Firestore/core/src/core/query.cc @@ -17,6 +17,7 @@ #include "Firestore/core/src/core/query.h" #include +#include #include #include "Firestore/core/src/core/bound.h" @@ -91,44 +92,42 @@ absl::optional Query::FindOpInsideFilters( return absl::nullopt; } -const std::vector& Query::normalized_order_bys() const { - return memoized_normalized_order_bys_->memoize([&]() { - // Any explicit order by fields should be added as is. - std::vector result = explicit_order_bys_; - std::set fieldsNormalized; - for (const OrderBy& order_by : explicit_order_bys_) { - fieldsNormalized.insert(order_by.field()); - } +std::shared_ptr> Query::CalculateNormalizedOrderBys() + const { + // Any explicit order by fields should be added as is. + auto result = std::make_shared>(explicit_order_bys_); + std::set fieldsNormalized; + for (const OrderBy& order_by : explicit_order_bys_) { + fieldsNormalized.insert(order_by.field()); + } - // The order of the implicit ordering always matches the last explicit order - // by. - Direction last_direction = explicit_order_bys_.empty() - ? Direction::Ascending - : explicit_order_bys_.back().direction(); - - // Any inequality fields not explicitly ordered should be implicitly ordered - // in a lexicographical order. When there are multiple inequality filters on - // the same field, the field should be added only once. Note: - // `std::set` sorts the key field before other fields. - // However, we want the key field to be sorted last. - const std::set inequality_fields = - InequalityFilterFields(); - - for (const model::FieldPath& field : inequality_fields) { - if (fieldsNormalized.find(field) == fieldsNormalized.end() && - !field.IsKeyFieldPath()) { - result.push_back(OrderBy(field, last_direction)); - } + // The order of the implicit ordering always matches the last explicit order + // by. + Direction last_direction = explicit_order_bys_.empty() + ? Direction::Ascending + : explicit_order_bys_.back().direction(); + + // Any inequality fields not explicitly ordered should be implicitly ordered + // in a lexicographical order. When there are multiple inequality filters on + // the same field, the field should be added only once. Note: + // `std::set` sorts the key field before other fields. + // However, we want the key field to be sorted last. + const std::set inequality_fields = InequalityFilterFields(); + + for (const model::FieldPath& field : inequality_fields) { + if (fieldsNormalized.find(field) == fieldsNormalized.end() && + !field.IsKeyFieldPath()) { + result->push_back(OrderBy(field, last_direction)); } + } - // Add the document key field to the last if it is not explicitly ordered. - if (fieldsNormalized.find(FieldPath::KeyFieldPath()) == - fieldsNormalized.end()) { - result.push_back(OrderBy(FieldPath::KeyFieldPath(), last_direction)); - } + // Add the document key field to the last if it is not explicitly ordered. + if (fieldsNormalized.find(FieldPath::KeyFieldPath()) == + fieldsNormalized.end()) { + result->push_back(OrderBy(FieldPath::KeyFieldPath(), last_direction)); + } - return result; - }); + return result; } LimitType Query::limit_type() const { @@ -296,14 +295,12 @@ std::string Query::ToString() const { return absl::StrCat("Query(canonical_id=", CanonicalId(), ")"); } -const Target& Query::ToTarget() const& { - return memoized_target_->memoize( - [&]() { return ToTarget(normalized_order_bys()); }); +std::shared_ptr Query::CalculateTarget() const { + return std::make_shared(ToTarget(normalized_order_bys())); } -const Target& Query::ToAggregateTarget() const& { - return memoized_aggregate_target_->memoize( - [&]() { return ToTarget(explicit_order_bys_); }); +std::shared_ptr Query::CalculateAggregateTarget() const { + return std::make_shared(ToTarget(explicit_order_bys_)); } Target Query::ToTarget(const std::vector& order_bys) const { diff --git a/Firestore/core/src/core/query.h b/Firestore/core/src/core/query.h index 23351a4de56..d2b7b3247ff 100644 --- a/Firestore/core/src/core/query.h +++ b/Firestore/core/src/core/query.h @@ -17,6 +17,7 @@ #ifndef FIRESTORE_CORE_SRC_CORE_QUERY_H_ #define FIRESTORE_CORE_SRC_CORE_QUERY_H_ +#include #include #include #include @@ -148,7 +149,10 @@ class Query { * This might include additional sort orders added implicitly to match the * backend behavior. */ - const std::vector& normalized_order_bys() const; + const std::vector& normalized_order_bys() const { + const auto func = std::bind(&Query::CalculateNormalizedOrderBys, this); + return memoized_normalized_order_bys_.value(func); + } bool has_limit() const { return limit_ != Target::kNoLimit; @@ -246,7 +250,10 @@ class Query { * Returns a `Target` instance this query will be mapped to in backend * and local store. */ - const Target& ToTarget() const&; + const Target& ToTarget() const& { + const auto func = std::bind(&Query::CalculateTarget, this); + return memoized_target_.value(func); + } /** * Returns a `Target` instance this query will be mapped to in backend @@ -254,7 +261,10 @@ class Query { * for non-aggregate queries, aggregate query targets do not contain * normalized order-bys, they only contain explicit order-bys. */ - const Target& ToAggregateTarget() const&; + const Target& ToAggregateTarget() const& { + const auto func = std::bind(&Query::CalculateAggregateTarget, this); + return memoized_aggregate_target_.value(func); + } friend std::ostream& operator<<(std::ostream& os, const Query& query); @@ -295,20 +305,19 @@ class Query { // member variable, which is not copyable). // The memoized list of sort orders. - mutable std::shared_ptr>> - memoized_normalized_order_bys_{ - std::make_shared>>()}; + std::shared_ptr> CalculateNormalizedOrderBys() const; + mutable util::ThreadSafeMemoizer> + memoized_normalized_order_bys_; // The corresponding Target of this Query instance. - mutable std::shared_ptr> memoized_target_{ - std::make_shared>()}; + std::shared_ptr CalculateTarget() const; + mutable util::ThreadSafeMemoizer memoized_target_; // The corresponding aggregate Target of this Query instance. Unlike targets // for non-aggregate queries, aggregate query targets do not contain // normalized order-bys, they only contain explicit order-bys. - mutable std::shared_ptr> - memoized_aggregate_target_{ - std::make_shared>()}; + std::shared_ptr CalculateAggregateTarget() const; + mutable util::ThreadSafeMemoizer memoized_aggregate_target_; }; bool operator==(const Query& lhs, const Query& rhs); diff --git a/Firestore/core/src/util/thread_safe_memoizer.h b/Firestore/core/src/util/thread_safe_memoizer.h index d2a991d9371..18526031a01 100644 --- a/Firestore/core/src/util/thread_safe_memoizer.h +++ b/Firestore/core/src/util/thread_safe_memoizer.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,10 @@ #define FIRESTORE_CORE_SRC_UTIL_THREAD_SAFE_MEMOIZER_H_ #include -#include -#include +#include +#include + +#include "Firestore/core/src/util/hard_assert.h" namespace firebase { namespace firestore { @@ -28,51 +30,135 @@ namespace util { /** * Stores a memoized value in a manner that is safe to be shared between * multiple threads. - * - * TODO(b/299933587) Make `ThreadSafeMemoizer` copyable and moveable. */ template class ThreadSafeMemoizer { public: - ThreadSafeMemoizer() = default; - - ~ThreadSafeMemoizer() { - // Call `std::call_once` in order to synchronize with the "active" - // invocation of `memoize()`. Without this synchronization, there is a data - // race between this destructor, which "reads" `memoized_value_` to destroy - // it, and the write to `memoized_value_` done by the "active" invocation of - // `memoize()`. - std::call_once(once_, [&]() {}); + /** + * Creates a new ThreadSafeMemoizer with no memoized value. + */ + ThreadSafeMemoizer() { + std::atomic_store(&memoized_, std::shared_ptr()); + } + + /** + * Copy constructor: creates a new ThreadSafeMemoizer object with the same + * memoized value as the ThreadSafeMemoizer object referred to by the given + * reference. + * + * The runtime performance of this function is O(1). + */ + ThreadSafeMemoizer(const ThreadSafeMemoizer& other) { + operator=(other); } - // This class cannot be copied or moved, because it has `std::once_flag` - // member. - ThreadSafeMemoizer(const ThreadSafeMemoizer&) = delete; - ThreadSafeMemoizer(ThreadSafeMemoizer&&) = delete; - ThreadSafeMemoizer& operator=(const ThreadSafeMemoizer&) = delete; - ThreadSafeMemoizer& operator=(ThreadSafeMemoizer&&) = delete; + /** + * Copy assignment operator: replaces this object's memoized value with the + * memoized value of the ThreadSafeMemoizer object referred to by the given + * reference. + * + * The runtime performance of this function is O(1). + */ + ThreadSafeMemoizer& operator=(const ThreadSafeMemoizer& other) { + if (&other == this) { + return *this; + } + + std::atomic_store(&memoized_, std::atomic_load(&other.memoized_)); + return *this; + } /** - * Memoize a value. + * Move constructor: creates a new ThreadSafeMemoizer object with the same + * memoized value as the ThreadSafeMemoizer object referred to by the given + * reference, also clearing its memoized value. * - * The std::function object specified by the first invocation of this - * function (the "active" invocation) will be invoked synchronously. - * None of the std::function objects specified by the subsequent - * invocations of this function (the "passive" invocations) will be - * invoked. All invocations, both "active" and "passive", will return a - * reference to the std::vector created by copying the return value from - * the std::function specified by the "active" invocation. It is, - * therefore, the "active" invocation's job to return the std::vector - * to memoize. + * The runtime performance of this function is O(1). */ - const T& memoize(std::function func) { - std::call_once(once_, [&]() { memoized_value_ = func(); }); - return memoized_value_; + ThreadSafeMemoizer(ThreadSafeMemoizer&& other) noexcept { + operator=(std::move(other)); + } + + /** + * Move assignment operator: replaces this object's memoized value with the + * memoized value of the ThreadSafeMemoizer object referred to by the given + * reference, also clearing its memoized value. + * + * The runtime performance of this function is O(1). + */ + ThreadSafeMemoizer& operator=(ThreadSafeMemoizer&& other) noexcept { + std::atomic_store(&memoized_, std::atomic_load(&other.memoized_)); + std::atomic_store(&other.memoized_, std::shared_ptr()); + return *this; + } + + /** + * Return the memoized value, calculating it with the given function if + * needed. + * + * If this object _does_ have a memoized value then this function simply + * returns a reference to it and does _not_ call the given function. + * + * On the other hand, if this object does _not_ have a memoized value then + * the given function is called to calculate the value to memoize. The value + * returned by the function is stored internally as the "memoized value" and + * then returned. If multiple threads race in calls to this function then + * more than one of them may have their functions called but only one of them + * will be memoized, with the others being discarded. + * + * The given function will be called synchronously by this function either + * zero times or one time. No reference to the given function is retained by + * this object. + * + * The given function _must_ return an initialized `std::shared_ptr`; that + * is, the returned `std::shared_ptr` _must_ evaluate to `true` when + * converted to `bool`. It is undefined behavior if the returned + * `std::shared_ptr` does _not_ satisfy this requirement. + * + * This function is thread-safe and may be called concurrently by multiple + * threads. + * + * The returned reference is "valid" only as long as this `ThreadSafeMemoizer` + * object is alive; namely, once this `ThreadSafeMemoizer` object's destructor + * starts running, the reference returned by this function is invalid and + * using it is undefined behavior. + */ + const T& value(const std::function()>& func) { + std::shared_ptr old_memoized = std::atomic_load(&memoized_); + + std::shared_ptr new_memoized; + bool new_memoized_is_initialized = false; + + while (true) { + if (old_memoized) { + return *old_memoized; + } + + if (!new_memoized_is_initialized) { + new_memoized = func(); + new_memoized_is_initialized = true; + HARD_ASSERT(new_memoized); + } + + if (std::atomic_compare_exchange_weak(&memoized_, &old_memoized, + new_memoized)) { + return *new_memoized; + } + } } private: - std::once_flag once_; - T memoized_value_; + // NOTE: Always use the std::atomic_XXX() functions to access the memoized_ + // std::shared_ptr to ensure thread safety. + // See https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic. + + // TODO(c++20): Use std::atomic> instead of a bare + // std::shared_ptr and the std::atomic_XXX() functions. The + // std::atomic_XXX() free functions are deprecated in C++20, and are also + // more error-prone than their std::atomic> member + // function counterparts. + + std::shared_ptr memoized_; }; } // namespace util diff --git a/Firestore/core/test/unit/util/thread_safe_memoizer_test.cc b/Firestore/core/test/unit/util/thread_safe_memoizer_test.cc index 86c73d9c207..de984c181f0 100644 --- a/Firestore/core/test/unit/util/thread_safe_memoizer_test.cc +++ b/Firestore/core/test/unit/util/thread_safe_memoizer_test.cc @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,40 +16,140 @@ #include "Firestore/core/src/util/thread_safe_memoizer.h" +#include +#include #include +#include + +#include "Firestore/core/test/unit/util/thread_safe_memoizer_testing.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" -namespace firebase { -namespace firestore { -namespace util { +namespace { + +using namespace std::literals::string_literals; +using firebase::firestore::testing::CountDownLatch; +using firebase::firestore::testing::CountingFunc; +using firebase::firestore::testing::FST_RE_DIGIT; +using firebase::firestore::testing::GenerateRandomBool; +using firebase::firestore::testing::max_practical_parallel_threads_for_testing; +using firebase::firestore::testing::SetOnDestructor; +using firebase::firestore::util::ThreadSafeMemoizer; +using testing::MatchesRegex; +using testing::StartsWith; + +/** + * Performs a copy or move assignment (chosen randomly) on the given memoizer + * and then ensure that it behaves as expected. This is useful for testing the + * "move" logic because a moved-from object, according to the C++ standard, is + * in a "valid, but unspecified" state and the only operations it is guaranteed + * to support are assignment and destruction. + */ +void VerifyWorksAfterBeingAssigned(ThreadSafeMemoizer& memoizer); + +TEST(ThreadSafeMemoizerTest, DefaultConstructor) { + ThreadSafeMemoizer memoizer; + auto func = [] { return std::make_shared(42); }; + EXPECT_EQ(memoizer.value(func), 42); +} + +TEST(ThreadSafeMemoizerTest, Value_ShouldReturnComputedValueOnFirstInvocation) { + ThreadSafeMemoizer memoizer; + CountingFunc counter("rztsygzy5z"); + EXPECT_EQ(memoizer.value(counter.func()), "rztsygzy5z"); +} + +TEST(ThreadSafeMemoizerTest, + Value_ShouldReturnMemoizedValueOnSubsequentInvocations) { + ThreadSafeMemoizer memoizer; + CountingFunc counter("tfj6v4kdxn_%s"); + auto func = counter.func(); + + memoizer.value(func); + + ASSERT_EQ(memoizer.value(func), "tfj6v4kdxn_0"); + for (int i = 0; i < 100; i++) { + SCOPED_TRACE("iteration i=" + std::to_string(i)); + ASSERT_EQ(memoizer.value(func), "tfj6v4kdxn_0"); + } +} -TEST(ThreadSafeMemoizerTest, MultiThreadedMemoization) { - std::atomic global_int{77}; +TEST(ThreadSafeMemoizerTest, Value_ShouldOnlyInvokeFunctionOnFirstInvocation) { + ThreadSafeMemoizer memoizer; + CountingFunc counter; + auto func = counter.func(); + memoizer.value(func); - auto expensive_lambda = [&]() { - // Simulate an expensive operation - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - // If the lambda gets executed multiple times, threads will see incremented - // `global_int`. - global_int++; - return global_int.load(); - }; + for (int i = 0; i < 100; i++) { + SCOPED_TRACE("iteration i=" + std::to_string(i)); + memoizer.value(func); + EXPECT_EQ(counter.invocation_count(), 1); + } +} - const int num_threads = 5; - const int expected_result = 78; +TEST(ThreadSafeMemoizerTest, Value_ShouldNotInvokeTheFunctionAfterMemoizing) { + ThreadSafeMemoizer memoizer; + CountingFunc counter("jhvyg8aym4_invocation=%s_thread=%c"); - // Create a thread safe memoizer and multiple threads. - util::ThreadSafeMemoizer memoized_result; + const int num_threads = max_practical_parallel_threads_for_testing(); std::vector threads; + CountDownLatch latch(num_threads); + std::atomic has_memoized_value{false}; + + for (auto i = num_threads; i > 0; i--) { + threads.emplace_back([&, i] { + // Create a std::function that increments a local count when invoked. + const std::string thread_id = std::to_string(i); + int my_func_invocation_count = 0; + auto func = [&, wrapped_func = counter.func(thread_id)] { + my_func_invocation_count++; + return wrapped_func(); + }; - for (int i = 0; i < num_threads; ++i) { - threads.emplace_back( - [&memoized_result, expected_result, &expensive_lambda]() { - const int& actual_result = memoized_result.memoize(expensive_lambda); + // Wait for all the other threads to get here before proceeding, to + // maximize concurrent access to the ThreadSafeMemoizer object. + latch.arrive_and_wait(); - // Verify that all threads get the same memoized result. - EXPECT_EQ(actual_result, expected_result); - }); + // Give all the threads a chance to exit the busy wait in + // arrive_and_wait() to reduce the chance of unintended "happens-before" + // relationships between threads being established. + for (auto k = num_threads * 2; k > 0; --k) { + std::this_thread::yield(); + } + + // Make an initial invocation of memoizer.value(). If some other thread + // is known to have already set the memoized value then ensure that our + // local function is _not_ invoked; otherwise, announce to the other + // threads that there is _now_ a memoized value. + const int expected_func_invocation_count = [&] { + const bool had_memoized_value = + has_memoized_value.load(std::memory_order_acquire); + auto memoized_value = memoizer.value(func); + + SCOPED_TRACE("thread i=" + thread_id + " had_memoized_value=" + + std::to_string(had_memoized_value) + + " memoized_value=" + memoized_value); + if (had_memoized_value) { + EXPECT_EQ(my_func_invocation_count, 0); + return 0; + } else { + has_memoized_value.store(true, std::memory_order_release); + EXPECT_GE(my_func_invocation_count, 0); + EXPECT_LE(my_func_invocation_count, 1); + return my_func_invocation_count; + } + }(); + + // Make subsequent invocations of memoizer.value() and ensure that our + // local function is _not_ invoked, since we are guaranteed that a value + // was already memoized, either by us or by some other thread. + for (int j = 0; j < 100; j++) { + auto memoized_value = memoizer.value(func); + SCOPED_TRACE("thread i=" + thread_id + " j=" + std::to_string(j) + + " memoized_value=" + memoized_value); + EXPECT_EQ(my_func_invocation_count, expected_func_invocation_count); + } + }); } for (auto& thread : threads) { @@ -57,6 +157,377 @@ TEST(ThreadSafeMemoizerTest, MultiThreadedMemoization) { } } -} // namespace util -} // namespace firestore -} // namespace firebase +TEST(ThreadSafeMemoizerTest, + CopyConstructor_NoMemoizedValue_OriginalMemoizesFirst) { + CountingFunc memoizer_counter("aaa"), memoizer_copy_dest_counter("bbb"); + ThreadSafeMemoizer memoizer; + ThreadSafeMemoizer memoizer_copy_dest(memoizer); + + EXPECT_EQ(memoizer.value(memoizer_counter.func()), "aaa"); + EXPECT_EQ(memoizer_copy_dest.value(memoizer_copy_dest_counter.func()), "bbb"); + + EXPECT_EQ(memoizer_counter.invocation_count(), 1); + EXPECT_EQ(memoizer_copy_dest_counter.invocation_count(), 1); +} + +TEST(ThreadSafeMemoizerTest, + CopyConstructor_NoMemoizedValue_CopyMemoizesFirst) { + CountingFunc memoizer_counter("aaa"), memoizer_copy_dest_counter("bbb"); + ThreadSafeMemoizer memoizer; + ThreadSafeMemoizer memoizer_copy_dest(memoizer); + + EXPECT_EQ(memoizer_copy_dest.value(memoizer_copy_dest_counter.func()), "bbb"); + EXPECT_EQ(memoizer.value(memoizer_counter.func()), "aaa"); + + EXPECT_EQ(memoizer_counter.invocation_count(), 1); + EXPECT_EQ(memoizer_copy_dest_counter.invocation_count(), 1); +} + +TEST(ThreadSafeMemoizerTest, CopyConstructor_MemoizedValue) { + CountingFunc memoizer_counter("aaa"), memoizer_copy_dest_counter("bbb"); + ThreadSafeMemoizer memoizer; + memoizer.value(memoizer_counter.func()); + ThreadSafeMemoizer memoizer_copy_dest(memoizer); + + EXPECT_EQ(memoizer_copy_dest.value(memoizer_copy_dest_counter.func()), "aaa"); + + EXPECT_EQ(memoizer_copy_dest_counter.invocation_count(), 0); +} + +TEST(ThreadSafeMemoizerTest, MoveConstructor_NoMemoizedValue) { + CountingFunc memoizer_move_dest_counter("bbb"); + ThreadSafeMemoizer memoizer; + ThreadSafeMemoizer memoizer_move_dest(std::move(memoizer)); + + EXPECT_EQ(memoizer_move_dest.value(memoizer_move_dest_counter.func()), "bbb"); + + EXPECT_EQ(memoizer_move_dest_counter.invocation_count(), 1); + VerifyWorksAfterBeingAssigned(memoizer); +} + +TEST(ThreadSafeMemoizerTest, MoveConstructor_MemoizedValue) { + CountingFunc memoizer_counter("aaa"), memoizer_move_dest_counter("bbb"); + ThreadSafeMemoizer memoizer; + memoizer.value(memoizer_counter.func()); + ThreadSafeMemoizer memoizer_move_dest(std::move(memoizer)); + + EXPECT_EQ(memoizer_move_dest.value(memoizer_move_dest_counter.func()), "aaa"); + + EXPECT_EQ(memoizer_move_dest_counter.invocation_count(), 0); + VerifyWorksAfterBeingAssigned(memoizer); +} + +TEST(ThreadSafeMemoizerTest, + CopyAssignment_NoMemoizedValueToNoMemoizedValue_OriginalMemoizesFirst) { + CountingFunc memoizer_counter("aaa"), memoizer_copy_dest_counter("bbb"); + ThreadSafeMemoizer memoizer; + ThreadSafeMemoizer memoizer_copy_dest; + + memoizer_copy_dest = memoizer; + + EXPECT_EQ(memoizer.value(memoizer_counter.func()), "aaa"); + EXPECT_EQ(memoizer_copy_dest.value(memoizer_copy_dest_counter.func()), "bbb"); +} + +TEST(ThreadSafeMemoizerTest, + CopyAssignment_NoMemoizedValueToNoMemoizedValue_CopyMemoizesFirst) { + CountingFunc memoizer_counter("aaa"), memoizer_copy_dest_counter("bbb"); + ThreadSafeMemoizer memoizer; + ThreadSafeMemoizer memoizer_copy_dest; + + memoizer_copy_dest = memoizer; + + EXPECT_EQ(memoizer_copy_dest.value(memoizer_copy_dest_counter.func()), "bbb"); + EXPECT_EQ(memoizer.value(memoizer_counter.func()), "aaa"); +} + +TEST(ThreadSafeMemoizerTest, CopyAssignment_MemoizedValueToNoMemoizedValue) { + CountingFunc memoizer_counter("aaa"), memoizer_copy_dest_counter("bbb"); + ThreadSafeMemoizer memoizer; + memoizer.value(memoizer_counter.func()); + ThreadSafeMemoizer memoizer_copy_dest; + + memoizer_copy_dest = memoizer; + + EXPECT_EQ(memoizer_copy_dest.value(memoizer_copy_dest_counter.func()), "aaa"); + EXPECT_EQ(memoizer.value(memoizer_counter.func()), "aaa"); + EXPECT_EQ(memoizer_counter.invocation_count(), 1); + EXPECT_EQ(memoizer_copy_dest_counter.invocation_count(), 0); +} + +TEST(ThreadSafeMemoizerTest, CopyAssignment_NoMemoizedValueToMemoizedValue) { + CountingFunc memoizer_counter("aaa"), memoizer_copy_dest_counter1("bbb1"), + memoizer_copy_dest_counter2("bbb2"); + ThreadSafeMemoizer memoizer; + ThreadSafeMemoizer memoizer_copy_dest; + memoizer_copy_dest.value(memoizer_copy_dest_counter1.func()); + + memoizer_copy_dest = memoizer; + + EXPECT_EQ(memoizer_copy_dest.value(memoizer_copy_dest_counter2.func()), + "bbb2"); + EXPECT_EQ(memoizer.value(memoizer_counter.func()), "aaa"); +} + +TEST(ThreadSafeMemoizerTest, CopyAssignment_MemoizedValueToMemoizedValue) { + CountingFunc memoizer_counter1("aaa1"), memoizer_counter2("aaa2"), + memoizer_copy_dest_counter1("bbb1"), memoizer_copy_dest_counter2("bbb2"); + ThreadSafeMemoizer memoizer; + memoizer.value(memoizer_counter1.func()); + ThreadSafeMemoizer memoizer_copy_dest; + memoizer_copy_dest.value(memoizer_copy_dest_counter1.func()); + + memoizer_copy_dest = memoizer; + + EXPECT_EQ(memoizer_copy_dest.value(memoizer_copy_dest_counter2.func()), + "aaa1"); + EXPECT_EQ(memoizer.value(memoizer_counter2.func()), "aaa1"); + EXPECT_EQ(memoizer_counter1.invocation_count(), 1); + EXPECT_EQ(memoizer_copy_dest_counter1.invocation_count(), 1); +} + +TEST(ThreadSafeMemoizerTest, CopyAssignment_CopyToSelf_NoMemoizedValue) { + CountingFunc memoizer_counter("aaa"); + ThreadSafeMemoizer memoizer; + auto& looks_like_another_memoizer = memoizer; + ASSERT_EQ(&memoizer, &looks_like_another_memoizer); + + memoizer = looks_like_another_memoizer; + + EXPECT_EQ(memoizer.value(memoizer_counter.func()), "aaa"); + EXPECT_EQ(memoizer_counter.invocation_count(), 1); +} + +TEST(ThreadSafeMemoizerTest, CopyAssignment_CopyToSelf_MemoizedValue) { + CountingFunc memoizer_counter("aaa_%s"); + auto func = memoizer_counter.func(); + ThreadSafeMemoizer memoizer; + auto& looks_like_another_memoizer = memoizer; + ASSERT_EQ(&memoizer, &looks_like_another_memoizer); + memoizer.value(func); + + memoizer = looks_like_another_memoizer; + + EXPECT_EQ(memoizer.value(func), "aaa_0"); + EXPECT_EQ(memoizer_counter.invocation_count(), 1); +} + +TEST(ThreadSafeMemoizerTest, MoveAssignment_MemoizedValueToNoMemoizedValue) { + CountingFunc memoizer_counter("aaa"), memoizer_move_dest_counter("bbb"); + ThreadSafeMemoizer memoizer; + memoizer.value(memoizer_counter.func()); + ThreadSafeMemoizer memoizer_move_dest; + + memoizer_move_dest = std::move(memoizer); + + EXPECT_EQ(memoizer_move_dest.value(memoizer_move_dest_counter.func()), "aaa"); + EXPECT_EQ(memoizer_move_dest_counter.invocation_count(), 0); + VerifyWorksAfterBeingAssigned(memoizer); +} + +TEST(ThreadSafeMemoizerTest, MoveAssignment_NoMemoizedValueToMemoizedValue) { + CountingFunc memoizer_counter("aaa"), memoizer_move_dest_counter1("bbb1"), + memoizer_move_dest_counter2("bbb2"); + ThreadSafeMemoizer memoizer; + ThreadSafeMemoizer memoizer_move_dest; + memoizer_move_dest.value(memoizer_move_dest_counter1.func()); + + memoizer_move_dest = std::move(memoizer); + + EXPECT_EQ(memoizer_move_dest.value(memoizer_move_dest_counter2.func()), + "bbb2"); + VerifyWorksAfterBeingAssigned(memoizer); +} + +TEST(ThreadSafeMemoizerTest, MoveAssignment_MemoizedValueToMemoizedValue) { + CountingFunc memoizer_counter1("aaa1"), memoizer_counter2("aaa2"), + memoizer_move_dest_counter1("bbb1"), memoizer_move_dest_counter2("bbb2"); + ThreadSafeMemoizer memoizer; + memoizer.value(memoizer_counter1.func()); + ThreadSafeMemoizer memoizer_move_dest; + memoizer_move_dest.value(memoizer_move_dest_counter1.func()); + + memoizer_move_dest = std::move(memoizer); + + EXPECT_EQ(memoizer_move_dest.value(memoizer_move_dest_counter2.func()), + "aaa1"); + EXPECT_EQ(memoizer_counter1.invocation_count(), 1); + EXPECT_EQ(memoizer_move_dest_counter1.invocation_count(), 1); + VerifyWorksAfterBeingAssigned(memoizer); +} + +TEST(ThreadSafeMemoizerTest, MoveAssignment_MoveToSelf_NoMemoizedValue) { + CountingFunc memoizer_counter("aaa"); + ThreadSafeMemoizer memoizer; + auto& looks_like_another_memoizer = memoizer; + ASSERT_EQ(&memoizer, &looks_like_another_memoizer); + + memoizer = std::move(looks_like_another_memoizer); + + VerifyWorksAfterBeingAssigned(memoizer); +} + +TEST(ThreadSafeMemoizerTest, MoveAssignment_MoveToSelf_MemoizedValue) { + CountingFunc memoizer_counter("aaa_%s"); + auto func = memoizer_counter.func(); + ThreadSafeMemoizer memoizer; + auto& looks_like_another_memoizer = memoizer; + ASSERT_EQ(&memoizer, &looks_like_another_memoizer); + memoizer.value(func); + + memoizer = std::move(looks_like_another_memoizer); + + VerifyWorksAfterBeingAssigned(memoizer); +} + +TEST(ThreadSafeMemoizerTest, + CopyConstructor_CopySourceKeepsMemoizedValueAlive) { + CountingFunc memoizer_counter; + std::atomic destroyed{false}; + auto memoizer = std::make_unique>(); + memoizer->value([&] { return std::make_shared(destroyed); }); + + auto memoizer_copy_dest = + std::make_unique>(*memoizer); + + ASSERT_FALSE(destroyed.load()); + memoizer_copy_dest.reset(); + ASSERT_FALSE(destroyed.load()); + memoizer.reset(); + ASSERT_TRUE(destroyed.load()); +} + +TEST(ThreadSafeMemoizerTest, CopyAssignment_CopySourceKeepsMemoizedValueAlive) { + CountingFunc memoizer_counter; + std::atomic destroyed{false}; + auto memoizer = std::make_unique>(); + memoizer->value([&] { return std::make_shared(destroyed); }); + auto memoizer_copy_dest = + std::make_unique>(); + + *memoizer_copy_dest = *memoizer; + + ASSERT_FALSE(destroyed.load()); + memoizer_copy_dest.reset(); + ASSERT_FALSE(destroyed.load()); + memoizer.reset(); + ASSERT_TRUE(destroyed.load()); +} + +TEST(ThreadSafeMemoizerTest, + MoveConstructor_MoveSourceDoesNotKeepMemoizedValueAlive) { + CountingFunc memoizer_counter; + std::atomic destroyed{false}; + ThreadSafeMemoizer memoizer; + memoizer.value([&] { return std::make_shared(destroyed); }); + + auto memoizer_move_dest = + std::make_unique>( + std::move(memoizer)); + + ASSERT_FALSE(destroyed.load()); + memoizer_move_dest.reset(); + ASSERT_TRUE(destroyed.load()); +} + +TEST(ThreadSafeMemoizerTest, + MoveAssignment_MoveSourceDoesNotKeepMemoizedValueAlive) { + CountingFunc memoizer_counter; + std::atomic destroyed{false}; + ThreadSafeMemoizer memoizer; + memoizer.value([&] { return std::make_shared(destroyed); }); + auto memoizer_move_dest = + std::make_unique>(); + + *memoizer_move_dest = std::move(memoizer); + + ASSERT_FALSE(destroyed.load()); + memoizer_move_dest.reset(); + ASSERT_TRUE(destroyed.load()); +} + +TEST(ThreadSafeMemoizerTest, TSAN_ConcurrentCallsToValueShouldNotDataRace) { + ThreadSafeMemoizer memoizer; + const auto num_threads = max_practical_parallel_threads_for_testing(); + CountDownLatch latch(num_threads); + std::vector threads; + for (auto i = num_threads; i > 0; --i) { + threads.emplace_back([i, num_threads, &latch, &memoizer] { + latch.arrive_and_wait(); + + for (auto k = num_threads; k > 0; --k) { + std::this_thread::yield(); + } + + memoizer.value([i] { return std::make_shared(i); }); + }); + } + for (auto&& thread : threads) { + thread.join(); + } +} + +TEST(ThreadSafeMemoizerTest, TSAN_ValueInACopyShouldNotDataRace) { + ThreadSafeMemoizer memoizer; + memoizer.value([&] { return std::make_shared(1111); }); + std::unique_ptr> memoizer_copy; + // NOTE: Always use std::memory_order_relaxed when loading from and storing + // into this variable to avoid creating a happens-before relationship, which + // would defeat the purpose of this test. + std::atomic*> memoizer_copy_atomic(nullptr); + + std::thread thread1([&] { + memoizer_copy = std::make_unique>(memoizer); + memoizer_copy_atomic.store(memoizer_copy.get(), std::memory_order_relaxed); + }); + std::thread thread2([&] { + ThreadSafeMemoizer* memoizer_ptr = nullptr; + while (true) { + memoizer_ptr = memoizer_copy_atomic.load(std::memory_order_relaxed); + if (memoizer_ptr) { + break; + } + std::this_thread::yield(); + } + memoizer_ptr->value([&] { return std::make_shared(2222); }); + }); + + thread1.join(); + thread2.join(); + + const auto memoizer_copy_value = + memoizer_copy->value([&] { return std::make_shared(3333); }); + EXPECT_EQ(memoizer_copy_value, 1111); +} + +void VerifyWorksAfterBeingAssigned(ThreadSafeMemoizer& memoizer) { + ThreadSafeMemoizer other_memoizer; + CountingFunc other_memoizer_counter("sx22pz64dn_%s"); + + // Randomly select whether the original memoizer had a memoized value. + const bool other_memoizer_has_memoized_value = GenerateRandomBool(); + if (other_memoizer_has_memoized_value) { + other_memoizer.value(other_memoizer_counter.func()); + } + + // Randomly select copy-assignment or move-assignment. + if (GenerateRandomBool()) { + memoizer = other_memoizer; + } else { + memoizer = std::move(other_memoizer); + } + + // Verify that the given `ThreadSafeMemoizer` behaves correctly after being + // assigned. + if (other_memoizer_has_memoized_value) { + EXPECT_EQ(memoizer.value(other_memoizer_counter.func()), "sx22pz64dn_0"); + EXPECT_EQ(other_memoizer_counter.invocation_count(), 1); + } else { + CountingFunc temp_counter("mx3rfb8qqk"); + EXPECT_EQ(memoizer.value(temp_counter.func()), "mx3rfb8qqk"); + EXPECT_EQ(temp_counter.invocation_count(), 1); + EXPECT_EQ(other_memoizer_counter.invocation_count(), 0); + } +} + +} // namespace diff --git a/Firestore/core/test/unit/util/thread_safe_memoizer_testing.cc b/Firestore/core/test/unit/util/thread_safe_memoizer_testing.cc new file mode 100644 index 00000000000..998bf41bd6f --- /dev/null +++ b/Firestore/core/test/unit/util/thread_safe_memoizer_testing.cc @@ -0,0 +1,144 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Firestore/core/test/unit/util/thread_safe_memoizer_testing.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Firestore/core/src/util/sanitizers.h" + +namespace firebase { +namespace firestore { +namespace testing { +namespace { + +constexpr bool kIsRunningUnderThreadSanitizer = +#if THREAD_SANITIZER + true; +#else + false; +#endif + +std::vector SplitSeparators(const std::string& s) { + std::vector chunks; + + auto found = s.find('%'); + decltype(found) search_start = 0; + decltype(found) substr_start = 0; + while (found != std::string::npos && found < s.size() - 1) { + const auto next_char = s[found + 1]; + if (next_char == 's' || next_char == 'c') { + chunks.push_back(s.substr(substr_start, found - substr_start)); + chunks.push_back(s.substr(found, 2)); + search_start = found + 2; + substr_start = search_start; + } else { + search_start = found + 1; + } + found = s.find('%', search_start); + } + chunks.push_back(s.substr(substr_start)); + + return chunks; +} + +} // namespace + +CountingFunc::CountingFunc(const std::string& format) + : CountingFunc(SplitSeparators(format)) { +} + +CountingFunc::CountingFunc(std::vector chunks) + : chunks_(std::move(chunks)) { + assert(!chunks_.empty()); + // Explicitly store the initial value into count_ because initialization of + // std::atomic is _not_ atomic. + count_.store(0); +} + +std::function()> CountingFunc::func( + std::string cookie) { + return [this, cookie = std::move(cookie)] { + return std::make_shared(Next(cookie)); + }; +} + +int CountingFunc::invocation_count() const { + return count_.load(std::memory_order_acquire); +} + +std::string CountingFunc::Next(const std::string& cookie) { + const int id = count_.fetch_add(1, std::memory_order_acq_rel); + std::ostringstream ss; + for (const std::string& chunk : chunks_) { + if (chunk == "%s") { + ss << id; + } else if (chunk == "%c" && cookie.size() > 0) { + ss << cookie; + } else { + ss << chunk; + } + } + return ss.str(); +} + +CountDownLatch::CountDownLatch(int count) { + // Explicitly store the count into the atomic because initialization is + // NOT atomic. + count_.store(count, std::memory_order_release); +} + +void CountDownLatch::arrive_and_wait() { + count_.fetch_sub(1, std::memory_order_acq_rel); + while (count_.load(std::memory_order_acquire) > 0) { + std::this_thread::yield(); + } +} + +decltype(std::thread::hardware_concurrency()) +max_practical_parallel_threads_for_testing() { + const auto hardware_concurrency = std::thread::hardware_concurrency(); + const auto num_threads = hardware_concurrency != 0 ? hardware_concurrency : 4; + + // Limit the number of threads when running under Thread Sanitizer as the + // boilerplate that it puts around atomics is so much that a large number of + // threads competing for a std::atomic can bring the app to its knees. + if (kIsRunningUnderThreadSanitizer) { + return std::min(static_cast(num_threads), 10); + } + + return num_threads; +} + +bool GenerateRandomBool() { + std::random_device random_device; + std::default_random_engine random_engine(random_device()); + const auto random_value = random_engine.operator()(); + return random_value % 2 == 0; +} + +} // namespace testing +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/unit/util/thread_safe_memoizer_testing.h b/Firestore/core/test/unit/util/thread_safe_memoizer_testing.h new file mode 100644 index 00000000000..31b60f46eff --- /dev/null +++ b/Firestore/core/test/unit/util/thread_safe_memoizer_testing.h @@ -0,0 +1,147 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIRESTORE_CORE_TEST_UNIT_UTIL_THREAD_SAFE_MEMOIZER_TESTING_H_ +#define FIRESTORE_CORE_TEST_UNIT_UTIL_THREAD_SAFE_MEMOIZER_TESTING_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace testing { + +#if defined(GTEST_USES_SIMPLE_RE) || defined(GTEST_USES_RE2) +constexpr const char* FST_RE_DIGIT = "\\d"; +#elif defined(GTEST_USES_POSIX_RE) +constexpr const char* FST_RE_DIGIT = "[[:digit:]]"; +#endif + +/** + * Generates strings that incorporate a count in a thread-safe manner. + * + * The "format" string given to the constructor is literally generated, except + * that all occurrences of "%s" are replaced with the invocation count, and + * all occurrences of "%c" are replaced with the cookie, if a cookie is + * specified. + * + * All functions in this class may be safely called concurrently by multiple + * threads. + */ +class CountingFunc { + public: + /** + * Creates a new `CountingFunc` that generates strings that are equal to + * the base-10 string representation of the invocation count. + */ + CountingFunc() : CountingFunc("%s") { + } + + /** + * Creates a new `CountingFunc` that generates strings that match the given + * format. + * @param format the format to use when generating strings; all occurrences of + * "%s" will be replaced by the count, which starts at 0 (zero). + */ + explicit CountingFunc(const std::string& format); + + /** + * Returns a function that, when invoked, generates a string using the format + * given to the constructor. Every string returned by the function has a + * different count. + * + * Although each invocation of this function _may_ return a distinct function, + * they all use the same counter and may be safely called concurrently from + * multiple threads. + * + * The returned function is valid as long as this `CountingFunc` object is + * valid. + */ + std::function()> func() { + return func(""); + } + + std::function()> func(std::string cookie); + + /** + * Returns the total number of invocations that have occurred on functions + * returned by `func()`. A new instance of this class will return 0 (zero). + */ + int invocation_count() const; + + private: + std::atomic count_; + std::mutex mutex; + std::vector chunks_; + + explicit CountingFunc(std::vector chunks); + std::string Next(const std::string& cookie); +}; + +/** + * A simple implementation of std::latch in C++20. + * + * TODO(c++20) Replace with std::latch. + */ +class CountDownLatch { + public: + explicit CountDownLatch(int count); + void arrive_and_wait(); + + private: + std::atomic count_; +}; + +class SetOnDestructor { + public: + explicit SetOnDestructor(std::atomic& flag) : flag_(flag) { + } + + ~SetOnDestructor() { + flag_.store(true); + } + + private: + std::atomic& flag_; +}; + +/** + * Returns the largest number of threads that can be truly executed in parallel, + * or an arbitrary value greater than one if the number of CPU cores cannot be + * determined. + */ +decltype(std::thread::hardware_concurrency()) +max_practical_parallel_threads_for_testing(); + +/** + * Generates and returns a random boolean value. + */ +bool GenerateRandomBool(); + +} // namespace testing +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_UNIT_UTIL_THREAD_SAFE_MEMOIZER_TESTING_H_ diff --git a/Firestore/core/test/unit/util/thread_safe_memoizer_testing_test.cc b/Firestore/core/test/unit/util/thread_safe_memoizer_testing_test.cc new file mode 100644 index 00000000000..75e99a8f1ef --- /dev/null +++ b/Firestore/core/test/unit/util/thread_safe_memoizer_testing_test.cc @@ -0,0 +1,277 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Firestore/core/test/unit/util/thread_safe_memoizer_testing.h" + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace { + +using firebase::firestore::testing::CountDownLatch; +using firebase::firestore::testing::CountingFunc; +using firebase::firestore::testing::max_practical_parallel_threads_for_testing; + +TEST(ThreadSafeMemoizerTesting, CountingFuncDefaultConstructor) { + CountingFunc counting_func; + auto func = counting_func.func(); + for (int i = 0; i < 100; i++) { + const std::string i_str = std::to_string(i); + SCOPED_TRACE("iteration i=" + i_str); + EXPECT_EQ(*func(), i_str); + } +} + +TEST(ThreadSafeMemoizerTesting, + CountingFuncShouldReturnSameStringIfNoReplacements) { + CountingFunc counting_func("tdjebqrtny"); + auto func = counting_func.func(); + for (int i = 0; i < 100; i++) { + SCOPED_TRACE("iteration i=" + std::to_string(i)); + EXPECT_EQ(*func(), "tdjebqrtny"); + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncHandlesPercentSAtStart) { + CountingFunc counting_func("%scmgb5bsbj2"); + auto func = counting_func.func(); + for (int i = 0; i < 100; i++) { + const std::string i_str = std::to_string(i); + SCOPED_TRACE("iteration i=" + i_str); + EXPECT_EQ(*func(), i_str + "cmgb5bsbj2"); + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncHandlesPercentSAtEnd) { + CountingFunc counting_func("nd3krmj2mn%s"); + auto func = counting_func.func(); + for (int i = 0; i < 100; i++) { + const std::string i_str = std::to_string(i); + SCOPED_TRACE("iteration i=" + i_str); + EXPECT_EQ(*func(), "nd3krmj2mn" + i_str); + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncHandlesPercentSInMiddle) { + CountingFunc counting_func("txxz4%sddrs5"); + auto func = counting_func.func(); + for (int i = 0; i < 100; i++) { + const std::string i_str = std::to_string(i); + SCOPED_TRACE("iteration i=" + i_str); + EXPECT_EQ(*func(), "txxz4" + i_str + "ddrs5"); + } +} + +TEST(ThreadSafeMemoizerTesting, + CountingFuncHandlesMultiplePercentSReplacements) { + CountingFunc counting_func("%scx%s3b%s5jazwf%s"); + auto func = counting_func.func(); + for (int i = 0; i < 100; i++) { + const std::string i_str = std::to_string(i); + SCOPED_TRACE("iteration i=" + i_str); + EXPECT_EQ(*func(), i_str + "cx" + i_str + "3b" + i_str + "5jazwf" + i_str); + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncHandlesPercentCAtStart) { + CountingFunc counting_func("%cwxxsz2qm2e"); + auto func = counting_func.func("7k4bek9pfx"); + for (int i = 0; i < 100; i++) { + SCOPED_TRACE("iteration i=" + std::to_string(i)); + EXPECT_EQ(*func(), "7k4bek9pfxwxxsz2qm2e"); + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncHandlesPercentCAtEnd) { + CountingFunc counting_func("7432wt5hnw%c"); + auto func = counting_func.func("yzcjsrh5tp"); + for (int i = 0; i < 100; i++) { + SCOPED_TRACE("iteration i=" + std::to_string(i)); + EXPECT_EQ(*func(), "7432wt5hnwyzcjsrh5tp"); + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncHandlesPercentCInMiddle) { + CountingFunc counting_func("wxxsz%c2qm2e"); + auto func = counting_func.func("gptebm6kh5"); + for (int i = 0; i < 100; i++) { + SCOPED_TRACE("iteration i=" + std::to_string(i)); + EXPECT_EQ(*func(), "wxxszgptebm6kh52qm2e"); + } +} + +TEST(ThreadSafeMemoizerTesting, + CountingFuncHandlesMultiplePercentCReplacements) { + CountingFunc counting_func("%cw7%c98%c8cg5mz%c"); + auto func = counting_func.func("ww3"); + for (int i = 0; i < 100; i++) { + SCOPED_TRACE("iteration i=" + std::to_string(i)); + EXPECT_EQ(*func(), "ww3w7ww398ww38cg5mzww3"); + } +} + +TEST(ThreadSafeMemoizerTesting, + CountingFuncHandlesDifferingPercentCReplacements) { + CountingFunc counting_func("5c8sc_%c_gr7vf"); + for (int i = 0; i < 100; i++) { + const std::string i_str = std::to_string(i); + auto func = counting_func.func("a" + i_str + "a"); + SCOPED_TRACE("iteration i=" + i_str); + EXPECT_EQ(*func(), "5c8sc_a" + i_str + "a_gr7vf"); + } +} + +TEST(ThreadSafeMemoizerTesting, + CountingFuncHandlesAlternatingPercentReplacements1) { + CountingFunc counting_func("%s_%c_%s_%c_%s"); + auto func = counting_func.func("bbb"); + for (int i = 0; i < 100; i++) { + const std::string i_str = std::to_string(i); + SCOPED_TRACE("iteration i=" + i_str); + EXPECT_EQ(*func(), i_str + "_bbb_" + i_str + "_bbb_" + i_str); + } +} + +TEST(ThreadSafeMemoizerTesting, + CountingFuncHandlesAlternatingPercentReplacements2) { + CountingFunc counting_func("%c_%s_%c_%s_%c"); + auto func = counting_func.func("bbb"); + for (int i = 0; i < 100; i++) { + const std::string i_str = std::to_string(i); + SCOPED_TRACE("iteration i=" + i_str); + EXPECT_EQ(*func(), "bbb_" + i_str + "_bbb_" + i_str + "_bbb"); + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncHandlesInvalidPercents) { + CountingFunc counting_func("%%s %% %x %cs %"); + auto func = counting_func.func("zzz"); + for (int i = 0; i < 100; i++) { + const std::string i_str = std::to_string(i); + SCOPED_TRACE("iteration i=" + i_str); + EXPECT_EQ(*func(), "%" + i_str + " %% %x zzzs %"); + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncFunctionsUseSameCounter) { + CountingFunc counting_func("3gswsz9hyd_%s"); + const std::vector funcs{ + counting_func.func(), counting_func.func(), counting_func.func(), + counting_func.func(), counting_func.func()}; + int next_id = 0; + for (int i = 0; i < 100; i++) { + for (decltype(funcs.size()) j = 0; j < funcs.size(); j++) { + SCOPED_TRACE("iteration i=" + std::to_string(i) + + " j=" + std::to_string(j)); + EXPECT_EQ(*funcs[j](), "3gswsz9hyd_" + std::to_string(next_id++)); + } + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncThreadSafety) { + CountingFunc counting_func("ejrxk3g6tb_%s"); + std::vector threads; + std::array, 20> strings; + CountDownLatch latch(strings.size()); + for (decltype(strings.size()) i = 0; i < strings.size(); i++) { + threads.emplace_back([&, i] { + auto func = counting_func.func(); + auto& results = strings[i]; + latch.arrive_and_wait(); + for (decltype(results.size()) j = 0; j < results.size(); j++) { + results[j] = *func(); + } + }); + } + + for (auto& thread : threads) { + thread.join(); + } + + std::vector actual_strings; + for (const auto& thread_strings : strings) { + actual_strings.insert(actual_strings.end(), thread_strings.begin(), + thread_strings.end()); + } + + std::vector expected_strings; + for (decltype(actual_strings.size()) i = 0; i < actual_strings.size(); i++) { + expected_strings.push_back("ejrxk3g6tb_" + std::to_string(i)); + } + + std::sort(actual_strings.begin(), actual_strings.end()); + std::sort(expected_strings.begin(), expected_strings.end()); + ASSERT_EQ(actual_strings, expected_strings); +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncInvocationCountOnNewInstance) { + CountingFunc counting_func; + EXPECT_EQ(counting_func.invocation_count(), 0); +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncInvocationCountIncrementsBy1) { + CountingFunc counting_func; + auto func = counting_func.func(); + for (int i = 0; i < 100; i++) { + EXPECT_EQ(counting_func.invocation_count(), i); + func(); + EXPECT_EQ(counting_func.invocation_count(), i + 1); + } +} + +TEST(ThreadSafeMemoizerTesting, + CountingFuncInvocationCountIncrementedByEachFunc) { + CountingFunc counting_func; + for (int i = 0; i < 100; i++) { + auto func = counting_func.func(); + EXPECT_EQ(counting_func.invocation_count(), i); + func(); + EXPECT_EQ(counting_func.invocation_count(), i + 1); + } +} + +TEST(ThreadSafeMemoizerTesting, CountingFuncInvocationCountThreadSafe) { + CountingFunc counting_func; + const int num_threads = max_practical_parallel_threads_for_testing(); + std::vector threads; + CountDownLatch latch(num_threads); + for (auto i = num_threads; i > 0; i--) { + threads.emplace_back([&, i] { + auto func = counting_func.func(); + latch.arrive_and_wait(); + auto last_count = counting_func.invocation_count(); + for (int j = 0; j < 100; j++) { + SCOPED_TRACE("Thread i=" + std::to_string(i) + + " j=" + std::to_string(j)); + func(); + auto new_count = counting_func.invocation_count(); + EXPECT_GT(new_count, last_count); + last_count = new_count; + } + }); + } + + for (auto& thread : threads) { + thread.join(); + } + + EXPECT_EQ(counting_func.invocation_count(), num_threads * 100); +} + +} // namespace