Skip to content

Commit bbc346b

Browse files
greyson-signalalan-signal
authored andcommitted
Create a system for scheduling work post-initial-render.
1 parent cf32b93 commit bbc346b

6 files changed

Lines changed: 191 additions & 47 deletions

File tree

app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public static ApplicationContext getInstance(Context context) {
114114
@Override
115115
public void onCreate() {
116116
Tracer.getInstance().start("Application#onCreate()");
117+
AppStartup.getInstance().onApplicationCreate();
117118

118119
long startTime = System.currentTimeMillis();
119120

@@ -123,42 +124,42 @@ public void onCreate() {
123124

124125
super.onCreate();
125126

126-
new AppStartup().addBlocking("security-provider", this::initializeSecurityProvider)
127-
.addBlocking("logging", () -> {
128-
initializeLogging();
129-
Log.i(TAG, "onCreate()");
130-
})
131-
.addBlocking("crash-handling", this::initializeCrashHandling)
132-
.addBlocking("eat-db", () -> DatabaseFactory.getInstance(this))
133-
.addBlocking("app-dependencies", this::initializeAppDependencies)
134-
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
135-
.addBlocking("app-migrations", this::initializeApplicationMigrations)
136-
.addBlocking("ring-rtc", this::initializeRingRtc)
137-
.addBlocking("mark-registration", () -> RegistrationUtil.maybeMarkRegistrationComplete(this))
138-
.addBlocking("lifecycle-observer", () -> ProcessLifecycleOwner.get().getLifecycle().addObserver(this))
139-
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
140-
.addBlocking("vector-compat", () -> {
141-
if (Build.VERSION.SDK_INT < 21) {
142-
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
143-
}
144-
})
145-
.addDeferred(this::initializeMessageRetrieval)
146-
.addDeferred(this::initializeExpiringMessageManager)
147-
.addDeferred(this::initializeRevealableMessageManager)
148-
.addDeferred(this::initializeGcmCheck)
149-
.addDeferred(this::initializeSignedPreKeyCheck)
150-
.addDeferred(this::initializePeriodicTasks)
151-
.addDeferred(this::initializeCircumvention)
152-
.addDeferred(this::initializePendingMessages)
153-
.addDeferred(this::initializeBlobProvider)
154-
.addDeferred(this::initializeCleanup)
155-
.addDeferred(this::initializeGlideCodecs)
156-
.addDeferred(FeatureFlags::init)
157-
.addDeferred(() -> NotificationChannels.create(this))
158-
.addDeferred(RefreshPreKeysJob::scheduleIfNecessary)
159-
.addDeferred(StorageSyncHelper::scheduleRoutineSync)
160-
.addDeferred(() -> ApplicationDependencies.getJobManager().beginJobLoop())
161-
.execute();
127+
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
128+
.addBlocking("logging", () -> {
129+
initializeLogging();
130+
Log.i(TAG, "onCreate()");
131+
})
132+
.addBlocking("crash-handling", this::initializeCrashHandling)
133+
.addBlocking("eat-db", () -> DatabaseFactory.getInstance(this))
134+
.addBlocking("app-dependencies", this::initializeAppDependencies)
135+
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
136+
.addBlocking("app-migrations", this::initializeApplicationMigrations)
137+
.addBlocking("ring-rtc", this::initializeRingRtc)
138+
.addBlocking("mark-registration", () -> RegistrationUtil.maybeMarkRegistrationComplete(this))
139+
.addBlocking("lifecycle-observer", () -> ProcessLifecycleOwner.get().getLifecycle().addObserver(this))
140+
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
141+
.addBlocking("vector-compat", () -> {
142+
if (Build.VERSION.SDK_INT < 21) {
143+
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
144+
}
145+
})
146+
.addNonBlocking(this::initializeMessageRetrieval)
147+
.addNonBlocking(this::initializeRevealableMessageManager)
148+
.addNonBlocking(this::initializeGcmCheck)
149+
.addNonBlocking(this::initializeSignedPreKeyCheck)
150+
.addNonBlocking(this::initializePeriodicTasks)
151+
.addNonBlocking(this::initializeCircumvention)
152+
.addNonBlocking(this::initializePendingMessages)
153+
.addNonBlocking(this::initializeCleanup)
154+
.addNonBlocking(this::initializeGlideCodecs)
155+
.addNonBlocking(FeatureFlags::init)
156+
.addNonBlocking(RefreshPreKeysJob::scheduleIfNecessary)
157+
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
158+
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
159+
.addPostRender(this::initializeExpiringMessageManager)
160+
.addPostRender(this::initializeBlobProvider)
161+
.addPostRender(() -> NotificationChannels.create(this))
162+
.execute();
162163

163164
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
164165
Tracer.getInstance().end("Application#onCreate()");

app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import org.signal.core.util.logging.Log;
1818
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
19+
import org.thoughtcrime.securesms.util.AppStartup;
1920
import org.thoughtcrime.securesms.util.ConfigurationUtil;
2021
import org.thoughtcrime.securesms.util.TextSecurePreferences;
2122
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
@@ -32,8 +33,10 @@ public abstract class BaseActivity extends AppCompatActivity {
3233

3334
@Override
3435
protected void onCreate(Bundle savedInstanceState) {
36+
AppStartup.getInstance().onCriticalRenderEventStart();
3537
logEvent("onCreate()");
3638
super.onCreate(savedInstanceState);
39+
AppStartup.getInstance().onCriticalRenderEventEnd();
3740
}
3841

3942
@Override

app/src/main/java/org/thoughtcrime/securesms/MainActivity.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import androidx.annotation.NonNull;
1010
import androidx.annotation.Nullable;
1111

12+
import org.thoughtcrime.securesms.util.AppStartup;
1213
import org.thoughtcrime.securesms.util.CachedInflater;
1314
import org.thoughtcrime.securesms.util.CommunicationActions;
1415
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
@@ -33,6 +34,7 @@ public class MainActivity extends PassphraseRequiredActivity {
3334

3435
@Override
3536
protected void onCreate(Bundle savedInstanceState, boolean ready) {
37+
AppStartup.getInstance().onCriticalRenderEventStart();
3638
super.onCreate(savedInstanceState, ready);
3739
setContentView(R.layout.main_activity);
3840

app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.thoughtcrime.securesms.recipients.Recipient;
2727
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
2828
import org.thoughtcrime.securesms.service.KeyCachingService;
29+
import org.thoughtcrime.securesms.util.AppStartup;
2930
import org.thoughtcrime.securesms.util.TextSecurePreferences;
3031

3132
import java.util.Locale;
@@ -51,6 +52,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
5152
@Override
5253
protected final void onCreate(Bundle savedInstanceState) {
5354
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
55+
AppStartup.getInstance().onCriticalRenderEventStart();
5456
this.networkAccess = new SignalServiceNetworkAccess(this);
5557
onPreCreate();
5658

@@ -64,6 +66,7 @@ protected final void onCreate(Bundle savedInstanceState) {
6466
onCreate(savedInstanceState, true);
6567
}
6668

69+
AppStartup.getInstance().onCriticalRenderEventEnd();
6770
Tracer.getInstance().end(Log.tag(getClass()) + "#onCreate()");
6871
}
6972

app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,13 @@
116116
import org.thoughtcrime.securesms.service.KeyCachingService;
117117
import org.thoughtcrime.securesms.sms.MessageSender;
118118
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
119+
import org.thoughtcrime.securesms.util.AppStartup;
119120
import org.thoughtcrime.securesms.util.AvatarUtil;
120121
import org.thoughtcrime.securesms.util.PlayStoreUtil;
121122
import org.thoughtcrime.securesms.util.ServiceUtil;
122123
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
123124
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
125+
import org.thoughtcrime.securesms.util.Stopwatch;
124126
import org.thoughtcrime.securesms.util.TextSecurePreferences;
125127
import org.thoughtcrime.securesms.util.Util;
126128
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -180,6 +182,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
180182
private Drawable archiveDrawable;
181183
private LifecycleObserver visibilityLifecycleObserver;
182184

185+
private Stopwatch startupStopwatch;
186+
183187
public static ConversationListFragment newInstance() {
184188
return new ConversationListFragment();
185189
}
@@ -188,6 +192,7 @@ public static ConversationListFragment newInstance() {
188192
public void onCreate(Bundle icicle) {
189193
super.onCreate(icicle);
190194
setHasOptionsMenu(true);
195+
startupStopwatch = new Stopwatch("startup");
191196
}
192197

193198
@Override
@@ -488,6 +493,19 @@ private void initializeListAdapters() {
488493
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
489494

490495
setAdapter(defaultAdapter);
496+
497+
defaultAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
498+
@Override
499+
public void onItemRangeInserted(int positionStart, int itemCount) {
500+
startupStopwatch.split("data-set");
501+
defaultAdapter.unregisterAdapterDataObserver(this);
502+
list.post(() -> {
503+
AppStartup.getInstance().onCriticalRenderEventEnd();
504+
startupStopwatch.split("first-render");
505+
startupStopwatch.stop(TAG);
506+
});
507+
}
508+
});
491509
}
492510

493511
@SuppressWarnings("rawtypes")

app/src/main/java/org/thoughtcrime/securesms/util/AppStartup.java

Lines changed: 128 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package org.thoughtcrime.securesms.util;
22

3+
import android.app.Application;
4+
import android.os.Handler;
5+
import android.os.Looper;
6+
7+
import androidx.annotation.MainThread;
38
import androidx.annotation.NonNull;
49

510
import org.signal.core.util.concurrent.SignalExecutors;
@@ -8,44 +13,156 @@
813
import java.util.LinkedList;
914
import java.util.List;
1015

16+
/**
17+
* Manages our app startup flow.
18+
*/
1119
public final class AppStartup {
1220

21+
/** The time to wait after Application#onCreate() to see if any UI rendering starts */
22+
private final long UI_WAIT_TIME = 500;
23+
24+
/** The maximum amount of time we'll wait for critical rendering events to finish. */
25+
private final long FAILSAFE_RENDER_TIME = 2500;
26+
1327
private static final String TAG = Log.tag(AppStartup.class);
1428

29+
private static final AppStartup INSTANCE = new AppStartup();
30+
1531
private final List<Task> blocking;
16-
private final List<Task> deferred;
32+
private final List<Task> nonBlocking;
33+
private final List<Task> postRender;
34+
private final Handler postRenderHandler;
35+
36+
private int outstandingCriticalRenderEvents;
37+
38+
private long applicationStartTime;
39+
private long renderStartTime;
40+
private long renderEndTime;
41+
42+
public static @NonNull AppStartup getInstance() {
43+
return INSTANCE;
44+
}
1745

18-
public AppStartup() {
19-
this.blocking = new LinkedList<>();
20-
this.deferred = new LinkedList<>();
46+
private AppStartup() {
47+
this.blocking = new LinkedList<>();
48+
this.nonBlocking = new LinkedList<>();
49+
this.postRender = new LinkedList<>();
50+
this.postRenderHandler = new Handler(Looper.getMainLooper());
2151
}
2252

23-
public @NonNull
24-
AppStartup addBlocking(@NonNull String name, @NonNull Runnable task) {
53+
public void onApplicationCreate() {
54+
this.applicationStartTime = System.currentTimeMillis();
55+
}
56+
57+
/**
58+
* Schedules a task that must happen during app startup in a blocking fashion.
59+
*/
60+
@MainThread
61+
public @NonNull AppStartup addBlocking(@NonNull String name, @NonNull Runnable task) {
2562
blocking.add(new Task(name, task));
2663
return this;
2764
}
2865

29-
public @NonNull
30-
AppStartup addDeferred(@NonNull Runnable task) {
31-
deferred.add(new Task("", task));
66+
/**
67+
* Schedules a task that should not block app startup, but should still happen as quickly as
68+
* possible.
69+
*/
70+
@MainThread
71+
public @NonNull AppStartup addNonBlocking(@NonNull Runnable task) {
72+
nonBlocking.add(new Task("", task));
3273
return this;
3374
}
3475

76+
/**
77+
* Schedules a task that should only be executed after all critical UI has been rendered. If no
78+
* UI will be shown (i.e. the Application was created in the background), this will simply happen
79+
* a short delay after {@link Application#onCreate()}.
80+
* @param task
81+
* @return
82+
*/
83+
@MainThread
84+
public @NonNull AppStartup addPostRender(@NonNull Runnable task) {
85+
postRender.add(new Task("", task));
86+
return this;
87+
}
88+
89+
/**
90+
* Indicates a UI event critical to initial rendering has started. This will delay tasks that were
91+
* scheduled via {@link #addPostRender(Runnable)}. You MUST call
92+
* {@link #onCriticalRenderEventEnd()} for each invocation of this method.
93+
*/
94+
@MainThread
95+
public void onCriticalRenderEventStart() {
96+
if (outstandingCriticalRenderEvents == 0 && postRender.size() > 0) {
97+
Log.i(TAG, "Received first critical render event.");
98+
renderStartTime = System.currentTimeMillis();
99+
100+
postRenderHandler.removeCallbacksAndMessages(null);
101+
postRenderHandler.postDelayed(() -> {
102+
Log.w(TAG, "Reached the failsafe event for post-render! Either someone forgot to call #onRenderEnd(), the activity was started while the phone was locked, or app start is taking a very long time.");
103+
executePostRender();
104+
}, FAILSAFE_RENDER_TIME);
105+
}
106+
107+
outstandingCriticalRenderEvents++;
108+
}
109+
110+
/**
111+
* Indicates a UI event critical to initial rendering has ended. Should only be paired with
112+
* {@link #onCriticalRenderEventStart()}.
113+
*/
114+
@MainThread
115+
public void onCriticalRenderEventEnd() {
116+
if (outstandingCriticalRenderEvents <= 0) {
117+
Log.w(TAG, "Too many end events! onCriticalRenderEventStart/End was mismanaged.");
118+
}
119+
120+
outstandingCriticalRenderEvents = Math.max(outstandingCriticalRenderEvents - 1, 0);
121+
122+
if (outstandingCriticalRenderEvents == 0 && postRender.size() > 0) {
123+
renderEndTime = System.currentTimeMillis();
124+
125+
Log.i(TAG, "First render has finished. " +
126+
"Cold Start: " + (renderEndTime - applicationStartTime) + " ms, " +
127+
"Render Time: " + (renderEndTime - renderStartTime) + " ms");
128+
129+
postRenderHandler.removeCallbacksAndMessages(null);
130+
executePostRender();
131+
}
132+
}
133+
134+
/**
135+
* Begins all pending task execution.
136+
*/
137+
@MainThread
35138
public void execute() {
36139
Stopwatch stopwatch = new Stopwatch("init");
37140

38141
for (Task task : blocking) {
39142
task.getRunnable().run();
40143
stopwatch.split(task.getName());
41144
}
145+
blocking.clear();
42146

43-
for (Task task : deferred) {
147+
for (Task task : nonBlocking) {
44148
SignalExecutors.BOUNDED.execute(task.getRunnable());
45149
}
150+
nonBlocking.clear();
46151

47-
stopwatch.split("schedule-deferred");
152+
stopwatch.split("schedule-non-blocking");
48153
stopwatch.stop(TAG);
154+
155+
postRenderHandler.postDelayed(() -> {
156+
Log.i(TAG, "Assuming the application has started in the background. Running post-render tasks.");
157+
executePostRender();
158+
}, UI_WAIT_TIME);
159+
}
160+
161+
private void executePostRender() {
162+
for (Task task : postRender) {
163+
SignalExecutors.BOUNDED.execute(task.getRunnable());
164+
}
165+
postRender.clear();
49166
}
50167

51168
private class Task {

0 commit comments

Comments
 (0)