Skip to content

Commit 589de6a

Browse files
authored
Crashlytics efficient keys (#3254)
Refactors UserMetadata and related classes to gain some significant efficiency improvements when calling `setCustomKey`: * Checks if the map contents have actually changed before writing the key/value store to disk. * UserMetadata now maintains a "dirty" marker bit for the keys, so calls to serialize the key will no-op when the bit is false. * Allow at most one queued serialization task at a time, and the queued task will always grab the latest key data when it executes. This prevents a large amount of stale Task objects being created and queued when `setCustomKeys` is invoked repeatedly. (Which has been known to cause OOMs in some extreme cases.) Also finishes cleanup of extra files from PR #3160: That PR was a quick fix for misreading the user id, which was stored in two different places on disk. This is a proper fix that uses a single source of truth for the serialized user id.
1 parent 221f616 commit 589de6a

23 files changed

+392
-263
lines changed

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import com.google.firebase.crashlytics.internal.DevelopmentPlatformProvider;
3838
import com.google.firebase.crashlytics.internal.NativeSessionFileProvider;
3939
import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger;
40-
import com.google.firebase.crashlytics.internal.log.LogFileManager;
40+
import com.google.firebase.crashlytics.internal.metadata.LogFileManager;
4141
import com.google.firebase.crashlytics.internal.persistence.FileStore;
4242
import com.google.firebase.crashlytics.internal.settings.SettingsDataProvider;
4343
import com.google.firebase.crashlytics.internal.settings.TestSettingsData;
@@ -54,6 +54,7 @@
5454

5555
public class CrashlyticsControllerTest extends CrashlyticsTestCase {
5656
private static final String GOOGLE_APP_ID = "google:app:id";
57+
private static final String SESSION_ID = "session_id";
5758

5859
private Context testContext;
5960
private IdManager idManager;
@@ -170,7 +171,7 @@ private ControllerBuilder builder() {
170171
/** Creates a new CrashlyticsController with default options and opens a session. */
171172
private CrashlyticsController createController() {
172173
final CrashlyticsController controller = builder().build();
173-
controller.openSession();
174+
controller.openSession(SESSION_ID);
174175
return controller;
175176
}
176177

@@ -488,7 +489,7 @@ public void testFatalEvent_sendsAppExceptionEvent() {
488489
when(mockSessionReportingCoordinator.listSortedOpenSessionIds())
489490
.thenReturn(new TreeSet<>(Collections.singleton(sessionId)));
490491

491-
controller.openSession();
492+
controller.openSession(SESSION_ID);
492493
controller.handleUncaughtException(
493494
testSettingsDataProvider, Thread.currentThread(), new RuntimeException("Fatal"));
494495
controller.finalizeSessions(testSettingsDataProvider);

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.google.firebase.crashlytics.internal.breadcrumbs.BreadcrumbHandler;
3838
import com.google.firebase.crashlytics.internal.breadcrumbs.BreadcrumbSource;
3939
import com.google.firebase.crashlytics.internal.breadcrumbs.DisabledBreadcrumbSource;
40+
import com.google.firebase.crashlytics.internal.metadata.UserMetadata;
4041
import com.google.firebase.crashlytics.internal.persistence.FileStore;
4142
import com.google.firebase.crashlytics.internal.settings.SettingsController;
4243
import com.google.firebase.crashlytics.internal.settings.TestSettingsData;
@@ -105,6 +106,12 @@ public void testCustomAttributes() throws Exception {
105106
crashlyticsCore.setCustomKey(key1, value1);
106107
assertEquals(value1, metadata.getCustomKeys().get(key1));
107108

109+
// Adding an existing key with the same value should return false
110+
assertFalse(metadata.setCustomKey(key1, value1));
111+
assertTrue(metadata.setCustomKey(key1, "someOtherValue"));
112+
assertTrue(metadata.setCustomKey(key1, value1));
113+
assertFalse(metadata.setCustomKey(key1, value1));
114+
108115
final String longValue = longId.replaceAll("0", "x");
109116
final String superLongValue = longValue + "some more chars";
110117

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131

3232
import com.google.android.gms.tasks.Task;
3333
import com.google.android.gms.tasks.Tasks;
34-
import com.google.firebase.crashlytics.internal.log.LogFileManager;
34+
import com.google.firebase.crashlytics.internal.metadata.LogFileManager;
35+
import com.google.firebase.crashlytics.internal.metadata.UserMetadata;
3536
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
3637
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.CustomAttribute;
3738
import com.google.firebase.crashlytics.internal.model.ImmutableList;
@@ -411,20 +412,6 @@ public void onReportSend_successfulReportsAreDeleted() {
411412
verify(reportSender).sendReport(mockReport2);
412413
}
413414

414-
@Test
415-
public void testPersistUserIdForCurrentSession_persistsCurrentUserIdForCurrentSessionId() {
416-
final String currentSessionId = "currentSessionId";
417-
final String userId = "testUserId";
418-
final long timestamp = System.currentTimeMillis();
419-
when(dataCapture.captureReportData(anyString(), anyLong())).thenReturn(mockReport);
420-
when(reportMetadata.getUserId()).thenReturn(userId);
421-
422-
reportingCoordinator.onBeginSession(currentSessionId, timestamp);
423-
reportingCoordinator.persistUserId(currentSessionId);
424-
425-
verify(reportPersistence).persistUserIdForSession(userId, currentSessionId);
426-
}
427-
428415
@Test
429416
public void testHasReportsToSend() {
430417
when(reportPersistence.hasFinalizedReports()).thenReturn(true);
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package com.google.firebase.crashlytics.internal.log;
15+
package com.google.firebase.crashlytics.internal.metadata;
1616

17-
import static com.google.firebase.crashlytics.internal.log.LogFileManager.MAX_LOG_SIZE;
17+
import static com.google.firebase.crashlytics.internal.metadata.LogFileManager.MAX_LOG_SIZE;
1818
import static org.mockito.ArgumentMatchers.any;
1919
import static org.mockito.ArgumentMatchers.eq;
2020
import static org.mockito.Mockito.mock;
Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package com.google.firebase.crashlytics.internal.common;
15+
package com.google.firebase.crashlytics.internal.metadata;
1616

1717
import com.google.firebase.crashlytics.internal.CrashlyticsTestCase;
18+
import com.google.firebase.crashlytics.internal.common.CrashlyticsBackgroundWorker;
1819
import com.google.firebase.crashlytics.internal.persistence.FileStore;
1920
import java.util.Collections;
2021
import java.util.HashMap;
@@ -39,6 +40,7 @@ public class MetaDataStoreTest extends CrashlyticsTestCase {
3940
private static final String ESCAPED = "\ttest\nvalue";
4041

4142
private FileStore fileStore;
43+
private CrashlyticsBackgroundWorker worker = new CrashlyticsBackgroundWorker(Runnable::run);
4244

4345
private MetaDataStore storeUnderTest;
4446

@@ -49,66 +51,69 @@ public void setUp() throws Exception {
4951
storeUnderTest = new MetaDataStore(fileStore);
5052
}
5153

52-
private static UserMetadata metadataWithUserId() {
53-
return metadataWithUserId(USER_ID);
54+
private UserMetadata metadataWithUserId(String sessionId) {
55+
return metadataWithUserId(sessionId, USER_ID);
5456
}
5557

56-
private static UserMetadata metadataWithUserId(String id) {
57-
UserMetadata metadata = new UserMetadata();
58-
metadata.setUserId(id);
58+
private UserMetadata metadataWithUserId(String sessionId, String userId) {
59+
UserMetadata metadata = new UserMetadata(sessionId, fileStore, worker);
60+
metadata.setUserId(userId);
5961
return metadata;
6062
}
6163

6264
public void testWriteUserData_allFields() {
63-
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId());
64-
final UserMetadata userData = storeUnderTest.readUserData(SESSION_ID_1);
65+
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId(SESSION_ID_1).getUserId());
66+
UserMetadata userData = UserMetadata.loadFromExistingSession(SESSION_ID_1, fileStore, worker);
6567
assertEquals(USER_ID, userData.getUserId());
6668
}
6769

6870
public void testWriteUserData_noFields() {
69-
storeUnderTest.writeUserData(SESSION_ID_1, new UserMetadata());
70-
final UserMetadata userData = storeUnderTest.readUserData(SESSION_ID_1);
71+
storeUnderTest.writeUserData(
72+
SESSION_ID_1, new UserMetadata(SESSION_ID_1, fileStore, null).getUserId());
73+
UserMetadata userData = UserMetadata.loadFromExistingSession(SESSION_ID_1, fileStore, worker);
7174
assertNull(userData.getUserId());
7275
}
7376

7477
public void testWriteUserData_singleField() {
75-
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId());
76-
final UserMetadata userData = storeUnderTest.readUserData(SESSION_ID_1);
78+
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId(SESSION_ID_1).getUserId());
79+
UserMetadata userData = UserMetadata.loadFromExistingSession(SESSION_ID_1, fileStore, worker);
7780
assertEquals(USER_ID, userData.getUserId());
7881
}
7982

8083
public void testWriteUserData_null() {
81-
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId(null));
82-
final UserMetadata userData = storeUnderTest.readUserData(SESSION_ID_1);
84+
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId(SESSION_ID_1, null).getUserId());
85+
UserMetadata userData = UserMetadata.loadFromExistingSession(SESSION_ID_1, fileStore, worker);
8386
assertEquals(null, userData.getUserId());
8487
}
8588

8689
public void testWriteUserData_emptyString() {
87-
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId(""));
88-
final UserMetadata userData = storeUnderTest.readUserData(SESSION_ID_1);
90+
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId(SESSION_ID_1, "").getUserId());
91+
UserMetadata userData = UserMetadata.loadFromExistingSession(SESSION_ID_1, fileStore, worker);
8992
assertEquals("", userData.getUserId());
9093
}
9194

9295
public void testWriteUserData_unicode() {
93-
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId(UNICODE));
94-
final UserMetadata userData = storeUnderTest.readUserData(SESSION_ID_1);
96+
storeUnderTest.writeUserData(
97+
SESSION_ID_1, metadataWithUserId(SESSION_ID_1, UNICODE).getUserId());
98+
UserMetadata userData = UserMetadata.loadFromExistingSession(SESSION_ID_1, fileStore, worker);
9599
assertEquals(UNICODE, userData.getUserId());
96100
}
97101

98102
public void testWriteUserData_escaped() {
99-
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId(ESCAPED));
100-
final UserMetadata userData = storeUnderTest.readUserData(SESSION_ID_1);
103+
storeUnderTest.writeUserData(
104+
SESSION_ID_1, metadataWithUserId(SESSION_ID_1, ESCAPED).getUserId());
105+
UserMetadata userData = UserMetadata.loadFromExistingSession(SESSION_ID_1, fileStore, worker);
101106
assertEquals(ESCAPED.trim(), userData.getUserId());
102107
}
103108

104109
public void testWriteUserData_readDifferentSession() {
105-
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId());
106-
final UserMetadata userData = storeUnderTest.readUserData(SESSION_ID_2);
110+
storeUnderTest.writeUserData(SESSION_ID_1, metadataWithUserId(SESSION_ID_1).getUserId());
111+
UserMetadata userData = UserMetadata.loadFromExistingSession(SESSION_ID_2, fileStore, worker);
107112
assertNull(userData.getUserId());
108113
}
109114

110115
public void testReadUserData_noStoredData() {
111-
final UserMetadata userData = storeUnderTest.readUserData(SESSION_ID_1);
116+
UserMetadata userData = UserMetadata.loadFromExistingSession(SESSION_ID_1, fileStore, worker);
112117
assertNull(userData.getUserId());
113118
}
114119

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/persistence/CrashlyticsReportPersistenceTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -390,13 +390,11 @@ public void testFinalizeReports_removesOldestReportsFirst() throws IOException {
390390
public void testLoadFinalizedReports_reportWithUserId_returnsReportWithProperUserId() {
391391
final String sessionId = "testSession";
392392
final String userId = "testUser";
393-
final CrashlyticsReport testReport = makeTestReport(sessionId);
393+
final CrashlyticsReport testReport = makeTestReport(sessionId, userId);
394394
final CrashlyticsReport.Session.Event testEvent = makeTestEvent();
395395

396396
reportPersistence.persistReport(testReport);
397397
reportPersistence.persistEvent(testEvent, sessionId);
398-
reportPersistence.persistUserIdForSession(userId, sessionId);
399-
400398
reportPersistence.finalizeReports("skippedSession", 0L);
401399

402400
final List<CrashlyticsReportWithSessionId> finalizedReports =
@@ -409,21 +407,19 @@ public void testLoadFinalizedReports_reportWithUserId_returnsReportWithProperUse
409407

410408
public void
411409
testLoadFinalizedReports_reportsWithUserIdInMultipleSessions_returnsReportsWithProperUserIds() {
410+
final String userId1 = "testUser1";
411+
final String userId2 = "testUser2";
412412
final String sessionId1 = "testSession1";
413-
final CrashlyticsReport testReport1 = makeTestReport(sessionId1);
414413
final String sessionId2 = "testSession2";
415-
final CrashlyticsReport testReport2 = makeTestReport(sessionId2);
414+
final CrashlyticsReport testReport1 = makeTestReport(sessionId1, userId1);
415+
final CrashlyticsReport testReport2 = makeTestReport(sessionId2, userId2);
416416
final CrashlyticsReport.Session.Event testEvent1 = makeTestEvent();
417417
final CrashlyticsReport.Session.Event testEvent2 = makeTestEvent();
418-
final String userId1 = "testUser1";
419-
final String userId2 = "testUser2";
420418

421419
reportPersistence.persistReport(testReport1);
422420
reportPersistence.persistReport(testReport2);
423421
reportPersistence.persistEvent(testEvent1, sessionId1);
424422
reportPersistence.persistEvent(testEvent2, sessionId2);
425-
reportPersistence.persistUserIdForSession(userId1, sessionId1);
426-
reportPersistence.persistUserIdForSession(userId2, sessionId2);
427423

428424
reportPersistence.finalizeReports("skippedSession", 0L);
429425

@@ -675,6 +671,10 @@ private static CrashlyticsReport makeTestReport(String sessionId) {
675671
return makeIncompleteReport().setSession(makeTestSession(sessionId)).build();
676672
}
677673

674+
private static CrashlyticsReport makeTestReport(String sessionId, String userId) {
675+
return makeTestReport(sessionId).withSessionEndFields(0L, false, userId);
676+
}
677+
678678
private static CrashlyticsReport.FilesPayload makeFilePayload() {
679679
byte[] testContents = {0, 2, 20, 10};
680680
return CrashlyticsReport.FilesPayload.builder()

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import android.os.StatFs;
3333
import android.provider.Settings.Secure;
3434
import android.text.TextUtils;
35+
import androidx.annotation.Nullable;
3536
import com.google.firebase.crashlytics.internal.Logger;
3637
import java.io.BufferedReader;
3738
import java.io.Closeable;
@@ -632,4 +633,13 @@ public static boolean canTryConnection(Context context) {
632633
return true;
633634
}
634635
}
636+
637+
/** @return true if s1.equals(s2), or if both are null. */
638+
public static boolean nullSafeEquals(@Nullable String s1, @Nullable String s2) {
639+
// :TODO: replace calls to this method with Objects.equals(...) when minSdkVersion is 19+
640+
if (s1 == null) {
641+
return s2 == null;
642+
}
643+
return s1.equals(s2);
644+
}
635645
}

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsBackgroundWorker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
* future work, and run the future work on the executor's thread, but not put it in the queue as its
3434
* own worker.
3535
*/
36-
class CrashlyticsBackgroundWorker {
36+
public class CrashlyticsBackgroundWorker {
3737
private final Executor executor;
3838

3939
private Task<Void> tail = Tasks.forResult(null);

0 commit comments

Comments
 (0)