Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added FrameMetrics to Android profiling data #2342

Merged
merged 27 commits into from Nov 18, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c9c50a5
added ProfileMeasurements to AndroidTransactionProfiler and Profiling…
stefanosiano Nov 7, 2022
1585c22
added ProfileMeasurements to AndroidTransactionProfiler and Profiling…
stefanosiano Nov 7, 2022
fed5b21
added ProfileMeasurements to AndroidTransactionProfiler and Profiling…
stefanosiano Nov 7, 2022
c5db530
added ProfileMeasurements to AndroidTransactionProfiler and Profiling…
stefanosiano Nov 7, 2022
0da7bc2
updated espresso test dependency to run on Android 13 devices
stefanosiano Nov 8, 2022
d6fe562
frameMetrics collection anticipated to onActivityCreated instead of o…
stefanosiano Nov 10, 2022
1db0628
updated tests
stefanosiano Nov 10, 2022
aad144f
We disable "Don't keep activities" developer option after receiving a…
stefanosiano Nov 10, 2022
f7c07ca
We disable "Don't keep activities" developer option after receiving a…
stefanosiano Nov 10, 2022
a8d1320
disabling "Don't keep activities" developer option programmatically d…
stefanosiano Nov 10, 2022
3d70b80
Format code
getsentry-bot Nov 10, 2022
d3ea8ed
Merge branch 'main' into feat/profiling-frame-metrics
stefanosiano Nov 10, 2022
5b906fa
updated changelog
stefanosiano Nov 10, 2022
1807d69
Merge branch 'main' into feat/profiling-frame-metrics
stefanosiano Nov 11, 2022
5bd9d16
measurements are not sent if empty
stefanosiano Nov 11, 2022
babdaa7
Merge branch 'main' into feat/profiling-frame-metrics
stefanosiano Nov 11, 2022
0fa21e0
updated benchmark device from OnePlus Nord N200 to Google Pixel 3a
stefanosiano Nov 11, 2022
a50903c
Merge remote-tracking branch 'origin/feat/profiling-frame-metrics' in…
stefanosiano Nov 11, 2022
cafe0f3
added finals everywhere
stefanosiano Nov 15, 2022
ace3c67
Remove profiler main thread io (#2348)
stefanosiano Nov 15, 2022
6f80fc1
Merge branch 'main' into feat/profiling-frame-metrics
stefanosiano Nov 16, 2022
80a206d
updated test dependencies to stable versions
stefanosiano Nov 16, 2022
c2599c1
SentryFrameMetricsCollector start collecting frameMetrics in onActivi…
stefanosiano Nov 16, 2022
b771ce1
Merge branch 'main' into feat/profiling-frame-metrics
stefanosiano Nov 16, 2022
e606515
increased benchmark cpu overhead threshold from 5% to 5.5%
stefanosiano Nov 17, 2022
a4e8237
Merge branch 'main' into feat/profiling-frame-metrics
stefanosiano Nov 17, 2022
bf05cd0
updated changelog
stefanosiano Nov 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .sauce/sentry-uitest-android-benchmark-lite.yml
Expand Up @@ -20,7 +20,7 @@ suites:

- name: "Android 11 (api 30)"
devices:
- id: OnePlus_Nord_N200_5G_real_us # OnePlus Nord N200 5G - api 30 (11)
- id: Google_Pixel_3a_real # Google Pixel 3a - api 30 (11)

artifacts:
download:
Expand Down
2 changes: 1 addition & 1 deletion .sauce/sentry-uitest-android-benchmark.yml
Expand Up @@ -28,7 +28,7 @@ suites:
devices:
- id: OnePlus_9_Pro_real_us # OnePlus 9 Pro - api 30 (11) - high end
- id: Google_Pixel_4_real_us # Google Pixel 4 - api 30 (11) - mid end
- id: OnePlus_Nord_N200_5G_real_us # OnePlus Nord N200 5G - api 30 (11) - low end
- id: Google_Pixel_3a_real # Google Pixel 3a - api 30 (11) - low end

- name: "Android 10 (api 29)"
devices:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,10 @@

- Fix `Gpu.vendorId` should be a String ([#2343](https://github.com/getsentry/sentry-java/pull/2343))

### Features

- added FrameMetrics to Android profiling data ([#2342](https://github.com/getsentry/sentry-java/pull/2342))
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved

## 6.7.0

### Fixes
Expand Down
23 changes: 23 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Expand Up @@ -164,6 +164,29 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setProfilingTracesIntervalMillis (I)V
}

public final class io/sentry/android/core/SentryFrameMetricsCollector : android/app/Application$ActivityLifecycleCallbacks {
public fun <init> (Landroid/content/Context;Lio/sentry/SentryOptions;Lio/sentry/android/core/BuildInfoProvider;)V
public fun <init> (Landroid/content/Context;Lio/sentry/SentryOptions;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/SentryFrameMetricsCollector$WindowFrameMetricsManager;)V
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityDestroyed (Landroid/app/Activity;)V
public fun onActivityPaused (Landroid/app/Activity;)V
public fun onActivityResumed (Landroid/app/Activity;)V
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityStarted (Landroid/app/Activity;)V
public fun onActivityStopped (Landroid/app/Activity;)V
public fun startCollection (Lio/sentry/android/core/SentryFrameMetricsCollector$FrameMetricsCollectorListener;)Ljava/lang/String;
public fun stopCollection (Ljava/lang/String;)V
}

public abstract interface class io/sentry/android/core/SentryFrameMetricsCollector$FrameMetricsCollectorListener {
public abstract fun onFrameMetricCollected (Landroid/view/FrameMetrics;F)V
}

public abstract interface class io/sentry/android/core/SentryFrameMetricsCollector$WindowFrameMetricsManager {
public fun addOnFrameMetricsAvailableListener (Landroid/view/Window;Landroid/view/Window$OnFrameMetricsAvailableListener;Landroid/os/Handler;)V
public fun removeOnFrameMetricsAvailableListener (Landroid/view/Window;Landroid/view/Window$OnFrameMetricsAvailableListener;)V
}

public final class io/sentry/android/core/SentryInitProvider : android/content/ContentProvider {
public fun <init> ()V
public fun attachInfo (Landroid/content/Context;Landroid/content/pm/ProviderInfo;)V
Expand Down
Expand Up @@ -154,8 +154,10 @@ static void init(
options.addEventProcessor(new PerformanceAndroidEventProcessor(options, activityFramesTracker));

options.setTransportGate(new AndroidTransportGate(context, options.getLogger()));
SentryFrameMetricsCollector frameMetricsCollector =
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
new SentryFrameMetricsCollector(context, options, buildInfoProvider);
options.setTransactionProfiler(
new AndroidTransactionProfiler(context, options, buildInfoProvider));
new AndroidTransactionProfiler(context, options, buildInfoProvider, frameMetricsCollector));
options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger()));
}

Expand Down
Expand Up @@ -11,6 +11,7 @@
import android.os.Debug;
import android.os.Process;
import android.os.SystemClock;
import android.view.FrameMetrics;
import io.sentry.HubAdapter;
import io.sentry.IHub;
import io.sentry.ITransaction;
Expand All @@ -21,14 +22,18 @@
import io.sentry.SentryLevel;
import io.sentry.android.core.internal.util.CpuInfoUtils;
import io.sentry.exception.SentryEnvelopeException;
import io.sentry.profilemeasurements.ProfileMeasurement;
import io.sentry.profilemeasurements.ProfileMeasurementValue;
import io.sentry.util.Objects;
import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand Down Expand Up @@ -60,23 +65,41 @@ final class AndroidTransactionProfiler implements ITransactionProfiler {
private long profileStartCpuMillis = 0;
private boolean isInitialized = false;
private int transactionsCounter = 0;
private @Nullable String frameMetricsCollectorId;
private final @NotNull SentryFrameMetricsCollector frameMetricsCollector;
private final @NotNull Map<String, ProfilingTransactionData> transactionMap = new HashMap<>();
private final @NotNull ArrayDeque<ProfileMeasurementValue> screenFrameRateMeasurements =
new ArrayDeque<>();
private final @NotNull ArrayDeque<ProfileMeasurementValue> slowFrameRenderMeasurements =
new ArrayDeque<>();
private final @NotNull ArrayDeque<ProfileMeasurementValue> frozenFrameRenderMeasurements =
new ArrayDeque<>();
private final @NotNull Map<String, ProfileMeasurement> measurementsMap = new HashMap<>();

public AndroidTransactionProfiler(
final @NotNull Context context,
final @NotNull SentryAndroidOptions sentryAndroidOptions,
final @NotNull BuildInfoProvider buildInfoProvider) {
this(context, sentryAndroidOptions, buildInfoProvider, HubAdapter.getInstance());
final @NotNull BuildInfoProvider buildInfoProvider,
final @NotNull SentryFrameMetricsCollector frameMetricsCollector) {
this(
context,
sentryAndroidOptions,
buildInfoProvider,
frameMetricsCollector,
HubAdapter.getInstance());
}

public AndroidTransactionProfiler(
final @NotNull Context context,
final @NotNull SentryAndroidOptions sentryAndroidOptions,
final @NotNull BuildInfoProvider buildInfoProvider,
final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
final @NotNull IHub hub) {
this.context = Objects.requireNonNull(context, "The application context is required");
this.options = Objects.requireNonNull(sentryAndroidOptions, "SentryAndroidOptions is required");
this.hub = Objects.requireNonNull(hub, "Hub is required");
this.frameMetricsCollector =
Objects.requireNonNull(frameMetricsCollector, "SentryFrameMetricsCollector is required");
this.buildInfoProvider =
Objects.requireNonNull(buildInfoProvider, "The BuildInfoProvider is required.");
this.packageInfo = ContextUtils.getPackageInfo(context, options.getLogger(), buildInfoProvider);
Expand Down Expand Up @@ -144,21 +167,7 @@ public synchronized void onTransactionStart(@NotNull ITransaction transaction) {
transactionsCounter--;
return;
}

// We stop profiling after a timeout to avoid huge profiles to be sent
scheduledFinish =
options
.getExecutorService()
.schedule(() -> onTransactionFinish(transaction, true), PROFILING_TIMEOUT_MILLIS);

transactionStartNanos = SystemClock.elapsedRealtimeNanos();
profileStartCpuMillis = Process.getElapsedCpuTime();

ProfilingTransactionData transactionData =
new ProfilingTransactionData(transaction, transactionStartNanos, profileStartCpuMillis);
transactionMap.put(transaction.getEventId().toString(), transactionData);

Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs);
onFirstTransactionStarted(transaction, traceFile);
} else {
ProfilingTransactionData transactionData =
new ProfilingTransactionData(
Expand All @@ -175,6 +184,64 @@ public synchronized void onTransactionStart(@NotNull ITransaction transaction) {
transactionsCounter);
}

@SuppressLint("NewApi")
private void onFirstTransactionStarted(
@NotNull ITransaction transaction, @NotNull File traceFile) {

measurementsMap.clear();
screenFrameRateMeasurements.clear();
slowFrameRenderMeasurements.clear();
frozenFrameRenderMeasurements.clear();

frameMetricsCollectorId =
frameMetricsCollector.startCollection(
new SentryFrameMetricsCollector.FrameMetricsCollectorListener() {
final long nanosInSecond = TimeUnit.SECONDS.toNanos(1);
final long frozenFrameThresholdNanos = TimeUnit.MILLISECONDS.toNanos(700);
float lastRefreshRate = 0;

@Override
public void onFrameMetricCollected(
@NotNull FrameMetrics frameMetrics, float refreshRate) {
long frameTimestampRelativeNanos =
SystemClock.elapsedRealtimeNanos() - transactionStartNanos;
long durationNanos = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION);
// Most frames take just a few nanoseconds longer than the optimal calculated
// duration.
// Therefore we subtract one, because otherwise almost all frames would be slow.
boolean isSlow = durationNanos > nanosInSecond / (refreshRate - 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this approach, we should probably also switch over in the ActivityFramesTracker, because for now we're just using hardcoded 16ms for detecting slow frames

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in this PR of course :)

float newRefreshRate = (int) (refreshRate * 100) / 100F;
if (durationNanos > frozenFrameThresholdNanos) {
frozenFrameRenderMeasurements.addLast(
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
} else if (isSlow) {
slowFrameRenderMeasurements.addLast(
new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos));
}
if (newRefreshRate != lastRefreshRate) {
lastRefreshRate = newRefreshRate;
screenFrameRateMeasurements.addLast(
new ProfileMeasurementValue(frameTimestampRelativeNanos, newRefreshRate));
}
}
});

// We stop profiling after a timeout to avoid huge profiles to be sent
scheduledFinish =
options
.getExecutorService()
.schedule(() -> onTransactionFinish(transaction, true), PROFILING_TIMEOUT_MILLIS);

transactionStartNanos = SystemClock.elapsedRealtimeNanos();
profileStartCpuMillis = Process.getElapsedCpuTime();

ProfilingTransactionData transactionData =
new ProfilingTransactionData(transaction, transactionStartNanos, profileStartCpuMillis);
transactionMap.put(transaction.getEventId().toString(), transactionData);

Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs);
}

@Override
public synchronized void onTransactionFinish(@NotNull ITransaction transaction) {
onTransactionFinish(transaction, false);
Expand Down Expand Up @@ -226,8 +293,14 @@ private synchronized void onTransactionFinish(
}
return;
}
onLastTransactionFinished(transaction, isTimeout);
}

@SuppressLint("NewApi")
private void onLastTransactionFinished(ITransaction transaction, boolean isTimeout) {
Debug.stopMethodTracing();
frameMetricsCollector.stopCollection(frameMetricsCollectorId);

long transactionEndNanos = SystemClock.elapsedRealtimeNanos();
long transactionEndCpuMillis = Process.getElapsedCpuTime();
long transactionDurationNanos = transactionEndNanos - transactionStartNanos;
Expand Down Expand Up @@ -270,6 +343,23 @@ private synchronized void onTransactionFinish(
profileStartCpuMillis);
}

if (!slowFrameRenderMeasurements.isEmpty()) {
measurementsMap.put(
ProfileMeasurement.ID_SLOW_FRAME_RENDERS,
new ProfileMeasurement(ProfileMeasurement.UNIT_NANOSECONDS, slowFrameRenderMeasurements));
}
if (!frozenFrameRenderMeasurements.isEmpty()) {
measurementsMap.put(
ProfileMeasurement.ID_FROZEN_FRAME_RENDERS,
new ProfileMeasurement(
ProfileMeasurement.UNIT_NANOSECONDS, frozenFrameRenderMeasurements));
}
if (!screenFrameRateMeasurements.isEmpty()) {
measurementsMap.put(
ProfileMeasurement.ID_SCREEN_FRAME_RATES,
new ProfileMeasurement(ProfileMeasurement.UNIT_HZ, screenFrameRateMeasurements));
}

// cpu max frequencies are read with a lambda because reading files is involved, so it will be
// done in the background when the trace file is read
ProfilingTraceData profilingTraceData =
Expand All @@ -292,7 +382,8 @@ private synchronized void onTransactionFinish(
options.getEnvironment(),
isTimeout
? ProfilingTraceData.TRUNCATION_REASON_TIMEOUT
: ProfilingTraceData.TRUNCATION_REASON_NORMAL);
: ProfilingTraceData.TRUNCATION_REASON_NORMAL,
measurementsMap);

SentryEnvelope envelope;
try {
Expand Down