/
PerformanceAndroidEventProcessor.java
341 lines (295 loc) · 12.6 KB
/
PerformanceAndroidEventProcessor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
package io.sentry.android.core;
import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_COLD;
import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM;
import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP;
import android.os.Looper;
import io.sentry.EventProcessor;
import io.sentry.Hint;
import io.sentry.MeasurementUnit;
import io.sentry.SentryEvent;
import io.sentry.SpanContext;
import io.sentry.SpanDataConvention;
import io.sentry.SpanId;
import io.sentry.SpanStatus;
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.protocol.App;
import io.sentry.protocol.MeasurementValue;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentrySpan;
import io.sentry.protocol.SentryTransaction;
import io.sentry.util.Objects;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** Event Processor responsible for adding Android metrics to transactions */
final class PerformanceAndroidEventProcessor implements EventProcessor {
private static final String APP_METRICS_ORIGIN = "auto.ui";
private static final String APP_METRICS_CONTENT_PROVIDER_OP = "contentprovider.load";
private static final String APP_METRICS_ACTIVITIES_OP = "activity.load";
private static final String APP_METRICS_APPLICATION_OP = "application.load";
private static final String APP_METRICS_PROCESS_INIT_OP = "process.load";
private static final long MAX_PROCESS_INIT_APP_START_DIFF_MS = 10000;
private boolean sentStartMeasurement = false;
private final @NotNull ActivityFramesTracker activityFramesTracker;
private final @NotNull SentryAndroidOptions options;
PerformanceAndroidEventProcessor(
final @NotNull SentryAndroidOptions options,
final @NotNull ActivityFramesTracker activityFramesTracker) {
this.options = Objects.requireNonNull(options, "SentryAndroidOptions is required");
this.activityFramesTracker =
Objects.requireNonNull(activityFramesTracker, "ActivityFramesTracker is required");
}
/**
* Returns the event itself
*
* @param event the SentryEvent the SentryEvent
* @param hint the Hint the Hint
* @return returns the event itself
*/
@Override
@Nullable
public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
// that's only necessary because on newer versions of Unity, if not overriding this method, it's
// throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably
// compilation mismatch.
return event;
}
@SuppressWarnings("NullAway")
@Override
public synchronized @NotNull SentryTransaction process(
@NotNull SentryTransaction transaction, @NotNull Hint hint) {
if (!options.isTracingEnabled()) {
return transaction;
}
// the app start measurement is only sent once and only if the transaction has
// the app.start span, which is automatically created by the SDK.
if (hasAppStartSpan(transaction)) {
if (!sentStartMeasurement) {
final @NotNull TimeSpan appStartTimeSpan =
AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options);
final long appStartUpDurationMs = appStartTimeSpan.getDurationMs();
// if appStartUpDurationMs is 0, metrics are not ready to be sent
if (appStartUpDurationMs != 0) {
final MeasurementValue value =
new MeasurementValue(
(float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName());
final String appStartKey =
AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD
? MeasurementValue.KEY_APP_START_COLD
: MeasurementValue.KEY_APP_START_WARM;
transaction.getMeasurements().put(appStartKey, value);
attachColdAppStartSpans(AppStartMetrics.getInstance(), transaction);
sentStartMeasurement = true;
}
}
@Nullable App appContext = transaction.getContexts().getApp();
if (appContext == null) {
appContext = new App();
transaction.getContexts().setApp(appContext);
}
final String appStartType =
AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD
? "cold"
: "warm";
appContext.setStartType(appStartType);
}
setContributingFlags(transaction);
final SentryId eventId = transaction.getEventId();
final SpanContext spanContext = transaction.getContexts().getTrace();
// only add slow/frozen frames to transactions created by ActivityLifecycleIntegration
// which have the operation UI_LOAD_OP. If a user-defined (or hybrid SDK) transaction
// users it, we'll also add the metrics if available
if (eventId != null
&& spanContext != null
&& spanContext.getOperation().contentEquals(UI_LOAD_OP)) {
final Map<String, @NotNull MeasurementValue> framesMetrics =
activityFramesTracker.takeMetrics(eventId);
if (framesMetrics != null) {
transaction.getMeasurements().putAll(framesMetrics);
}
}
return transaction;
}
private void setContributingFlags(SentryTransaction transaction) {
@Nullable SentrySpan ttidSpan = null;
@Nullable SentrySpan ttfdSpan = null;
for (final @NotNull SentrySpan span : transaction.getSpans()) {
if (ActivityLifecycleIntegration.TTID_OP.equals(span.getOp())) {
ttidSpan = span;
} else if (ActivityLifecycleIntegration.TTFD_OP.equals(span.getOp())) {
ttfdSpan = span;
}
// once both are found we can early exit
if (ttidSpan != null && ttfdSpan != null) {
break;
}
}
if (ttidSpan == null && ttfdSpan == null) {
return;
}
for (final @NotNull SentrySpan span : transaction.getSpans()) {
// as ttid and ttfd spans are artificially created, we don't want to set the flags on them
if (span == ttidSpan || span == ttfdSpan) {
continue;
}
// let's assume main thread, unless it's set differently
boolean spanOnMainThread = true;
final @Nullable Map<String, Object> spanData = span.getData();
if (spanData != null) {
final @Nullable Object threadName = spanData.get(SpanDataConvention.THREAD_NAME);
spanOnMainThread = threadName == null || "main".equals(threadName);
}
// for ttid, only main thread spans are relevant
final boolean withinTtid =
(ttidSpan != null)
&& isTimestampWithinSpan(span.getStartTimestamp(), ttidSpan)
&& spanOnMainThread;
final boolean withinTtfd =
(ttfdSpan != null) && isTimestampWithinSpan(span.getStartTimestamp(), ttfdSpan);
if (withinTtid || withinTtfd) {
@Nullable Map<String, Object> data = span.getData();
if (data == null) {
data = new ConcurrentHashMap<>();
span.setData(data);
}
if (withinTtid) {
data.put(SpanDataConvention.CONTRIBUTES_TTID, true);
}
if (withinTtfd) {
data.put(SpanDataConvention.CONTRIBUTES_TTFD, true);
}
}
}
}
private static boolean isTimestampWithinSpan(
final double timestamp, final @NotNull SentrySpan target) {
return timestamp >= target.getStartTimestamp()
&& (target.getTimestamp() == null || timestamp <= target.getTimestamp());
}
private boolean hasAppStartSpan(final @NotNull SentryTransaction txn) {
final @NotNull List<SentrySpan> spans = txn.getSpans();
for (final @NotNull SentrySpan span : spans) {
if (span.getOp().contentEquals(APP_START_COLD)
|| span.getOp().contentEquals(APP_START_WARM)) {
return true;
}
}
final @Nullable SpanContext context = txn.getContexts().getTrace();
return context != null
&& (context.getOperation().equals(APP_START_COLD)
|| context.getOperation().equals(APP_START_WARM));
}
private void attachColdAppStartSpans(
final @NotNull AppStartMetrics appStartMetrics, final @NotNull SentryTransaction txn) {
// data will be filled only for cold app starts
if (appStartMetrics.getAppStartType() != AppStartMetrics.AppStartType.COLD) {
return;
}
final @Nullable SpanContext traceContext = txn.getContexts().getTrace();
if (traceContext == null) {
return;
}
final @NotNull SentryId traceId = traceContext.getTraceId();
// determine the app.start.cold span, where all other spans will be attached to
@Nullable SpanId parentSpanId = null;
final @NotNull List<SentrySpan> spans = txn.getSpans();
for (final @NotNull SentrySpan span : spans) {
if (span.getOp().contentEquals(APP_START_COLD)) {
parentSpanId = span.getSpanId();
break;
}
}
// Process init
final long classInitUptimeMs = appStartMetrics.getClassLoadedUptimeMs();
final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan();
if (appStartTimeSpan.hasStarted()
&& Math.abs(classInitUptimeMs - appStartTimeSpan.getStartUptimeMs())
<= MAX_PROCESS_INIT_APP_START_DIFF_MS) {
final @NotNull TimeSpan processInitTimeSpan = new TimeSpan();
processInitTimeSpan.setStartedAt(appStartTimeSpan.getStartUptimeMs());
processInitTimeSpan.setStartUnixTimeMs(appStartTimeSpan.getStartTimestampMs());
processInitTimeSpan.setStoppedAt(classInitUptimeMs);
processInitTimeSpan.setDescription("Process Initialization");
txn.getSpans()
.add(
timeSpanToSentrySpan(
processInitTimeSpan, parentSpanId, traceId, APP_METRICS_PROCESS_INIT_OP));
}
// Content Providers
final @NotNull List<TimeSpan> contentProviderOnCreates =
appStartMetrics.getContentProviderOnCreateTimeSpans();
if (!contentProviderOnCreates.isEmpty()) {
for (final @NotNull TimeSpan contentProvider : contentProviderOnCreates) {
txn.getSpans()
.add(
timeSpanToSentrySpan(
contentProvider, parentSpanId, traceId, APP_METRICS_CONTENT_PROVIDER_OP));
}
}
// Application.onCreate
final @NotNull TimeSpan appOnCreate = appStartMetrics.getApplicationOnCreateTimeSpan();
if (appOnCreate.hasStopped()) {
txn.getSpans()
.add(
timeSpanToSentrySpan(appOnCreate, parentSpanId, traceId, APP_METRICS_APPLICATION_OP));
}
// Activities
final @NotNull List<ActivityLifecycleTimeSpan> activityLifecycleTimeSpans =
appStartMetrics.getActivityLifecycleTimeSpans();
if (!activityLifecycleTimeSpans.isEmpty()) {
for (ActivityLifecycleTimeSpan activityTimeSpan : activityLifecycleTimeSpans) {
if (activityTimeSpan.getOnCreate().hasStarted()
&& activityTimeSpan.getOnCreate().hasStopped()) {
txn.getSpans()
.add(
timeSpanToSentrySpan(
activityTimeSpan.getOnCreate(),
parentSpanId,
traceId,
APP_METRICS_ACTIVITIES_OP));
}
if (activityTimeSpan.getOnStart().hasStarted()
&& activityTimeSpan.getOnStart().hasStopped()) {
txn.getSpans()
.add(
timeSpanToSentrySpan(
activityTimeSpan.getOnStart(),
parentSpanId,
traceId,
APP_METRICS_ACTIVITIES_OP));
}
}
}
}
@NotNull
private static SentrySpan timeSpanToSentrySpan(
final @NotNull TimeSpan span,
final @Nullable SpanId parentSpanId,
final @NotNull SentryId traceId,
final @NotNull String operation) {
final Map<String, Object> defaultSpanData = new HashMap<>(2);
defaultSpanData.put(SpanDataConvention.THREAD_ID, Looper.getMainLooper().getThread().getId());
defaultSpanData.put(SpanDataConvention.THREAD_NAME, "main");
defaultSpanData.put(SpanDataConvention.CONTRIBUTES_TTID, true);
defaultSpanData.put(SpanDataConvention.CONTRIBUTES_TTFD, true);
return new SentrySpan(
span.getStartTimestampSecs(),
span.getProjectedStopTimestampSecs(),
traceId,
new SpanId(),
parentSpanId,
operation,
span.getDescription(),
SpanStatus.OK,
APP_METRICS_ORIGIN,
new ConcurrentHashMap<>(),
new ConcurrentHashMap<>(),
null,
defaultSpanData);
}
}