Skip to content

Commit b1b2eb0

Browse files
authored
Merge 0da7bc2 into 7597ded
2 parents 7597ded + 0da7bc2 commit b1b2eb0

File tree

18 files changed

+1162
-51
lines changed

18 files changed

+1162
-51
lines changed

.sauce/sentry-uitest-android-benchmark.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ suites:
3232

3333
- name: "Android 10 (api 29)"
3434
devices:
35-
- id: OnePlus_7_Pro_real # OnePlus 7 Pro - api 29 (10)
35+
- id: OnePlus_7T_real_us # OnePlus 7T - api 29 (10)
3636
- id: Nokia_7_1_real_us # Nokia 7.1 - api 29 (10)
3737

3838
# At the time of writing (July, 4, 2022), the market share per android version is:

.sauce/sentry-uitest-android-ui.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ suites:
3232

3333
- name: "Android 10 Ui test (api 29)"
3434
devices:
35-
- id: OnePlus_7_Pro_real # OnePlus 7 Pro - api 29 (10)
35+
- id: OnePlus_7T_real_us # OnePlus 7T - api 29 (10)
3636

3737
# Controls what artifacts to fetch when the suite on Sauce Cloud has finished.
3838
artifacts:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Provide hook for Jetpack Compose navigation instrumentation ([#2320](https://github.com/getsentry/sentry-java/pull/2320))
1414
- Populate `event.modules` with dependencies metadata ([#2324](https://github.com/getsentry/sentry-java/pull/2324))
1515
- Support Spring 6 and Spring Boot 3 ([#2289](https://github.com/getsentry/sentry-java/pull/2289))
16+
- added FrameMetrics to Android profiling data ([#2342](https://github.com/getsentry/sentry-java/pull/2342))
1617

1718
### Dependencies
1819

buildSrc/src/main/java/Config.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,18 +139,16 @@ object Config {
139139
}
140140

141141
object TestLibs {
142-
private val androidxTestVersion = "1.4.0"
143-
144-
// todo This beta version is needed to run ui tests on Android 13.
145-
// It will be replaced by androidxTestVersion when 1.5.0 will be out.
146-
private val androidxTestVersionBeta = "1.5.0-beta01"
147-
private val espressoVersion = "3.4.0"
142+
// todo These rc versions are needed to run ui tests on Android 13.
143+
// They will be replaced by stable version when 1.5.0/3.5.0 will be out.
144+
private val androidxTestVersion = "1.5.0-rc01"
145+
private val espressoVersion = "3.5.0-rc01"
148146

149147
val androidJUnitRunner = "androidx.test.runner.AndroidJUnitRunner"
150148
val kotlinTestJunit = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion"
151-
val androidxCore = "androidx.test:core:$androidxTestVersionBeta"
149+
val androidxCore = "androidx.test:core:$androidxTestVersion"
152150
val androidxRunner = "androidx.test:runner:$androidxTestVersion"
153-
val androidxTestCoreKtx = "androidx.test:core-ktx:$androidxTestVersionBeta"
151+
val androidxTestCoreKtx = "androidx.test:core-ktx:$androidxTestVersion"
154152
val androidxTestRules = "androidx.test:rules:$androidxTestVersion"
155153
val espressoCore = "androidx.test.espresso:espresso-core:$espressoVersion"
156154
val espressoIdlingResource = "androidx.test.espresso:espresso-idling-resource:$espressoVersion"

sentry-android-core/api/sentry-android-core.api

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,29 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
164164
public fun setProfilingTracesIntervalMillis (I)V
165165
}
166166

167+
public final class io/sentry/android/core/SentryFrameMetricsCollector : android/app/Application$ActivityLifecycleCallbacks {
168+
public fun <init> (Landroid/content/Context;Lio/sentry/SentryOptions;Lio/sentry/android/core/BuildInfoProvider;)V
169+
public fun <init> (Landroid/content/Context;Lio/sentry/SentryOptions;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/SentryFrameMetricsCollector$WindowFrameMetricsManager;)V
170+
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
171+
public fun onActivityDestroyed (Landroid/app/Activity;)V
172+
public fun onActivityPaused (Landroid/app/Activity;)V
173+
public fun onActivityResumed (Landroid/app/Activity;)V
174+
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
175+
public fun onActivityStarted (Landroid/app/Activity;)V
176+
public fun onActivityStopped (Landroid/app/Activity;)V
177+
public fun startCollection (Lio/sentry/android/core/SentryFrameMetricsCollector$FrameMetricsCollectorListener;)Ljava/lang/String;
178+
public fun stopCollection (Ljava/lang/String;)V
179+
}
180+
181+
public abstract interface class io/sentry/android/core/SentryFrameMetricsCollector$FrameMetricsCollectorListener {
182+
public abstract fun onFrameMetricCollected (Landroid/view/FrameMetrics;F)V
183+
}
184+
185+
public abstract interface class io/sentry/android/core/SentryFrameMetricsCollector$WindowFrameMetricsManager {
186+
public fun addOnFrameMetricsAvailableListener (Landroid/view/Window;Landroid/view/Window$OnFrameMetricsAvailableListener;Landroid/os/Handler;)V
187+
public fun removeOnFrameMetricsAvailableListener (Landroid/view/Window;Landroid/view/Window$OnFrameMetricsAvailableListener;)V
188+
}
189+
167190
public final class io/sentry/android/core/SentryInitProvider : android/content/ContentProvider {
168191
public fun <init> ()V
169192
public fun attachInfo (Landroid/content/Context;Landroid/content/pm/ProviderInfo;)V

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@ static void init(
154154
options.addEventProcessor(new PerformanceAndroidEventProcessor(options, activityFramesTracker));
155155

156156
options.setTransportGate(new AndroidTransportGate(context, options.getLogger()));
157+
SentryFrameMetricsCollector frameMetricsCollector =
158+
new SentryFrameMetricsCollector(context, options, buildInfoProvider);
157159
options.setTransactionProfiler(
158-
new AndroidTransactionProfiler(context, options, buildInfoProvider));
160+
new AndroidTransactionProfiler(context, options, buildInfoProvider, frameMetricsCollector));
159161
options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger()));
160162
}
161163

sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.os.Debug;
1212
import android.os.Process;
1313
import android.os.SystemClock;
14+
import android.view.FrameMetrics;
1415
import io.sentry.HubAdapter;
1516
import io.sentry.IHub;
1617
import io.sentry.ITransaction;
@@ -21,14 +22,18 @@
2122
import io.sentry.SentryLevel;
2223
import io.sentry.android.core.internal.util.CpuInfoUtils;
2324
import io.sentry.exception.SentryEnvelopeException;
25+
import io.sentry.profilemeasurements.ProfileMeasurement;
26+
import io.sentry.profilemeasurements.ProfileMeasurementValue;
2427
import io.sentry.util.Objects;
2528
import java.io.File;
29+
import java.util.ArrayDeque;
2630
import java.util.ArrayList;
2731
import java.util.HashMap;
2832
import java.util.List;
2933
import java.util.Map;
3034
import java.util.UUID;
3135
import java.util.concurrent.Future;
36+
import java.util.concurrent.TimeUnit;
3237
import org.jetbrains.annotations.NotNull;
3338
import org.jetbrains.annotations.Nullable;
3439

@@ -60,23 +65,41 @@ final class AndroidTransactionProfiler implements ITransactionProfiler {
6065
private long profileStartCpuMillis = 0;
6166
private boolean isInitialized = false;
6267
private int transactionsCounter = 0;
68+
private @Nullable String frameMetricsCollectorId;
69+
private final @NotNull SentryFrameMetricsCollector frameMetricsCollector;
6370
private final @NotNull Map<String, ProfilingTransactionData> transactionMap = new HashMap<>();
71+
private final @NotNull ArrayDeque<ProfileMeasurementValue> screenFrameRateMeasurements =
72+
new ArrayDeque<>();
73+
private final @NotNull ArrayDeque<ProfileMeasurementValue>
74+
adverseFrameRenderTimestampMeasurements = new ArrayDeque<>();
75+
private final @NotNull ArrayDeque<ProfileMeasurementValue>
76+
adverseFrozenFrameTimestampMeasurements = new ArrayDeque<>();
77+
private final @NotNull Map<String, ProfileMeasurement> measurementsMap = new HashMap<>();
6478

6579
public AndroidTransactionProfiler(
6680
final @NotNull Context context,
6781
final @NotNull SentryAndroidOptions sentryAndroidOptions,
68-
final @NotNull BuildInfoProvider buildInfoProvider) {
69-
this(context, sentryAndroidOptions, buildInfoProvider, HubAdapter.getInstance());
82+
final @NotNull BuildInfoProvider buildInfoProvider,
83+
final @NotNull SentryFrameMetricsCollector frameMetricsCollector) {
84+
this(
85+
context,
86+
sentryAndroidOptions,
87+
buildInfoProvider,
88+
frameMetricsCollector,
89+
HubAdapter.getInstance());
7090
}
7191

7292
public AndroidTransactionProfiler(
7393
final @NotNull Context context,
7494
final @NotNull SentryAndroidOptions sentryAndroidOptions,
7595
final @NotNull BuildInfoProvider buildInfoProvider,
96+
final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
7697
final @NotNull IHub hub) {
7798
this.context = Objects.requireNonNull(context, "The application context is required");
7899
this.options = Objects.requireNonNull(sentryAndroidOptions, "SentryAndroidOptions is required");
79100
this.hub = Objects.requireNonNull(hub, "Hub is required");
101+
this.frameMetricsCollector =
102+
Objects.requireNonNull(frameMetricsCollector, "SentryFrameMetricsCollector is required");
80103
this.buildInfoProvider =
81104
Objects.requireNonNull(buildInfoProvider, "The BuildInfoProvider is required.");
82105
this.packageInfo = ContextUtils.getPackageInfo(context, options.getLogger(), buildInfoProvider);
@@ -144,21 +167,7 @@ public synchronized void onTransactionStart(@NotNull ITransaction transaction) {
144167
transactionsCounter--;
145168
return;
146169
}
147-
148-
// We stop profiling after a timeout to avoid huge profiles to be sent
149-
scheduledFinish =
150-
options
151-
.getExecutorService()
152-
.schedule(() -> onTransactionFinish(transaction, true), PROFILING_TIMEOUT_MILLIS);
153-
154-
transactionStartNanos = SystemClock.elapsedRealtimeNanos();
155-
profileStartCpuMillis = Process.getElapsedCpuTime();
156-
157-
ProfilingTransactionData transactionData =
158-
new ProfilingTransactionData(transaction, transactionStartNanos, profileStartCpuMillis);
159-
transactionMap.put(transaction.getEventId().toString(), transactionData);
160-
161-
Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs);
170+
onFirstTransactionStarted(transaction, traceFile);
162171
} else {
163172
ProfilingTransactionData transactionData =
164173
new ProfilingTransactionData(
@@ -175,6 +184,64 @@ public synchronized void onTransactionStart(@NotNull ITransaction transaction) {
175184
transactionsCounter);
176185
}
177186

187+
@SuppressLint("NewApi")
188+
private void onFirstTransactionStarted(
189+
@NotNull ITransaction transaction, @NotNull File traceFile) {
190+
191+
measurementsMap.clear();
192+
screenFrameRateMeasurements.clear();
193+
adverseFrameRenderTimestampMeasurements.clear();
194+
adverseFrozenFrameTimestampMeasurements.clear();
195+
196+
frameMetricsCollectorId =
197+
frameMetricsCollector.startCollection(
198+
new SentryFrameMetricsCollector.FrameMetricsCollectorListener() {
199+
final long nanosInSecond = TimeUnit.SECONDS.toNanos(1);
200+
final long frozenFrameThresholdNanos = TimeUnit.MILLISECONDS.toNanos(700);
201+
float lastRefreshRate = 0;
202+
203+
@Override
204+
public void onFrameMetricCollected(
205+
@NotNull FrameMetrics frameMetrics, float refreshRate) {
206+
long frameTimestampRelativeNanos =
207+
SystemClock.elapsedRealtimeNanos() - transactionStartNanos;
208+
long durationNanos = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION);
209+
// Most frames take just a few nanoseconds longer than the optimal calculated
210+
// duration.
211+
// Therefore we subtract one, because otherwise almost all frames would be slow.
212+
boolean isSlow = durationNanos > nanosInSecond / (refreshRate - 1);
213+
float newRefreshRate = (int) (refreshRate * 100) / 100F;
214+
if (durationNanos > frozenFrameThresholdNanos) {
215+
adverseFrozenFrameTimestampMeasurements.addLast(
216+
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
217+
} else if (isSlow) {
218+
adverseFrameRenderTimestampMeasurements.addLast(
219+
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
220+
}
221+
if (newRefreshRate != lastRefreshRate) {
222+
lastRefreshRate = newRefreshRate;
223+
screenFrameRateMeasurements.addLast(
224+
new ProfileMeasurementValue(frameTimestampRelativeNanos, newRefreshRate));
225+
}
226+
}
227+
});
228+
229+
// We stop profiling after a timeout to avoid huge profiles to be sent
230+
scheduledFinish =
231+
options
232+
.getExecutorService()
233+
.schedule(() -> onTransactionFinish(transaction, true), PROFILING_TIMEOUT_MILLIS);
234+
235+
transactionStartNanos = SystemClock.elapsedRealtimeNanos();
236+
profileStartCpuMillis = Process.getElapsedCpuTime();
237+
238+
ProfilingTransactionData transactionData =
239+
new ProfilingTransactionData(transaction, transactionStartNanos, profileStartCpuMillis);
240+
transactionMap.put(transaction.getEventId().toString(), transactionData);
241+
242+
Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs);
243+
}
244+
178245
@Override
179246
public synchronized void onTransactionFinish(@NotNull ITransaction transaction) {
180247
onTransactionFinish(transaction, false);
@@ -226,8 +293,14 @@ private synchronized void onTransactionFinish(
226293
}
227294
return;
228295
}
296+
onLastTransactionFinished(transaction, isTimeout);
297+
}
229298

299+
@SuppressLint("NewApi")
300+
private void onLastTransactionFinished(ITransaction transaction, boolean isTimeout) {
230301
Debug.stopMethodTracing();
302+
frameMetricsCollector.stopCollection(frameMetricsCollectorId);
303+
231304
long transactionEndNanos = SystemClock.elapsedRealtimeNanos();
232305
long transactionEndCpuMillis = Process.getElapsedCpuTime();
233306
long transactionDurationNanos = transactionEndNanos - transactionStartNanos;
@@ -270,6 +343,18 @@ private synchronized void onTransactionFinish(
270343
profileStartCpuMillis);
271344
}
272345

346+
measurementsMap.put(
347+
ProfileMeasurement.ID_SLOW_FRAME_RENDERS,
348+
new ProfileMeasurement(
349+
ProfileMeasurement.UNIT_NANOSECONDS, adverseFrameRenderTimestampMeasurements));
350+
measurementsMap.put(
351+
ProfileMeasurement.ID_FROZEN_FRAME_RENDERS,
352+
new ProfileMeasurement(
353+
ProfileMeasurement.UNIT_NANOSECONDS, adverseFrozenFrameTimestampMeasurements));
354+
measurementsMap.put(
355+
ProfileMeasurement.ID_SCREEN_FRAME_RATES,
356+
new ProfileMeasurement(ProfileMeasurement.UNIT_HZ, screenFrameRateMeasurements));
357+
273358
// cpu max frequencies are read with a lambda because reading files is involved, so it will be
274359
// done in the background when the trace file is read
275360
ProfilingTraceData profilingTraceData =
@@ -292,7 +377,8 @@ private synchronized void onTransactionFinish(
292377
options.getEnvironment(),
293378
isTimeout
294379
? ProfilingTraceData.TRUNCATION_REASON_TIMEOUT
295-
: ProfilingTraceData.TRUNCATION_REASON_NORMAL);
380+
: ProfilingTraceData.TRUNCATION_REASON_NORMAL,
381+
measurementsMap);
296382

297383
SentryEnvelope envelope;
298384
try {

0 commit comments

Comments
 (0)