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

Add profilesSampleRate and profileSampler for Android sdk #2184

Merged
merged 8 commits into from
Aug 5, 2022
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

### Features

- Add profilesSampleRate and profileSampler options for Android sdk ([#2184](https://github.com/getsentry/sentry-java/pull/2184))
- SentryOptions.setProfilingEnabled has been deprecated in favor of setProfilesSampleRate
- Bump Native SDK to v0.4.18 ([#2154](https://github.com/getsentry/sentry-java/pull/2154))
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0418)
- [diff](https://github.com/getsentry/sentry-native/compare/0.4.17...0.4.18)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ final class ManifestMetadataReader {
static final String TRACES_UI_ENABLE = "io.sentry.traces.user-interaction.enable";

static final String TRACES_PROFILING_ENABLE = "io.sentry.traces.profiling.enable";
static final String PROFILES_SAMPLE_RATE = "io.sentry.traces.profiling.sample-rate";

@ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling";

Expand All @@ -82,6 +83,7 @@ private ManifestMetadataReader() {}
* @param context the application context
* @param options the SentryAndroidOptions
*/
@SuppressWarnings("deprecation")
static void applyMetadata(
final @NotNull Context context, final @NotNull SentryAndroidOptions options) {
Objects.requireNonNull(context, "The application context is required.");
Expand Down Expand Up @@ -245,6 +247,13 @@ static void applyMetadata(
options.setProfilingEnabled(
readBool(metadata, logger, TRACES_PROFILING_ENABLE, options.isProfilingEnabled()));

if (options.getProfilesSampleRate() == null) {
final Double profilesSampleRate = readDouble(metadata, logger, PROFILES_SAMPLE_RATE);
if (profilesSampleRate != -1) {
options.setProfilesSampleRate(profilesSampleRate);
}
}

options.setEnableUserInteractionTracing(
readBool(metadata, logger, TRACES_UI_ENABLE, options.isEnableUserInteractionTracing()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class AndroidTransactionProfilerTest {
val mockLogger = mock<ILogger>()
val options = SentryAndroidOptions().apply {
dsn = mockDsn
isProfilingEnabled = true
profilesSampleRate = 1.0
isDebug = true
setLogger(mockLogger)
}
Expand Down Expand Up @@ -99,9 +99,9 @@ class AndroidTransactionProfilerTest {
}

@Test
fun `profiler on isProfilingEnabled false`() {
fun `profiler on profilesSampleRate=0 false`() {
fixture.options.apply {
isProfilingEnabled = false
profilesSampleRate = 0.0
}
val profiler = fixture.getSut(context)
profiler.onTransactionStart(fixture.transaction1)
Expand All @@ -110,9 +110,9 @@ class AndroidTransactionProfilerTest {
}

@Test
fun `profiler evaluates isProfiling options only on first transaction profiling`() {
fun `profiler evaluates if profiling is enabled in options only on first transaction profiling`() {
fixture.options.apply {
isProfilingEnabled = false
profilesSampleRate = 0.0
}

// We create the profiler, and nothing goes wrong
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,47 @@ class ManifestMetadataReaderTest {
assertFalse(fixture.options.isProfilingEnabled)
}

@Test
fun `applyMetadata reads profilesSampleRate from metadata`() {
// Arrange
val expectedSampleRate = 0.99f
val bundle = bundleOf(ManifestMetadataReader.PROFILES_SAMPLE_RATE to expectedSampleRate)
val context = fixture.getContext(metaData = bundle)

// Act
ManifestMetadataReader.applyMetadata(context, fixture.options)

// Assert
assertEquals(expectedSampleRate.toDouble(), fixture.options.profilesSampleRate)
}

@Test
fun `applyMetadata does not override profilesSampleRate from options`() {
// Arrange
val expectedSampleRate = 0.99f
fixture.options.profilesSampleRate = expectedSampleRate.toDouble()
val bundle = bundleOf(ManifestMetadataReader.PROFILES_SAMPLE_RATE to 0.1f)
val context = fixture.getContext(metaData = bundle)

// Act
ManifestMetadataReader.applyMetadata(context, fixture.options)

// Assert
assertEquals(expectedSampleRate.toDouble(), fixture.options.profilesSampleRate)
}

@Test
fun `applyMetadata without specifying profilesSampleRate, stays null`() {
// Arrange
val context = fixture.getContext()

// Act
ManifestMetadataReader.applyMetadata(context, fixture.options)

// Assert
assertNull(fixture.options.profilesSampleRate)
}

@Test
fun `applyMetadata reads tracingOrigins to options`() {
// Arrange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class SdkBenchmarkTest : BaseBenchmarkTest() {
val opPerfProfilingSdk = getOperation {
SentryAndroid.init(context) {
it.dsn = "https://key@host/proj"
it.isProfilingEnabled = true
it.profilesSampleRate = 1.0
it.tracesSampleRate = 1.0
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class SentryBenchmarkTest : BaseBenchmarkTest() {
SentryAndroid.init(context) { options: SentryOptions ->
options.dsn = "https://key@uri/1234567"
options.tracesSampleRate = 1.0
options.isProfilingEnabled = true
options.profilesSampleRate = 1.0
options.isEnableAutoSessionTracking = false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class EnvelopeTests : BaseUiTest() {

initSentry(true) { options: SentryOptions ->
options.tracesSampleRate = 1.0
options.isProfilingEnabled = true
options.profilesSampleRate = 1.0
}
relayIdlingResource.increment()
val transaction = Sentry.startTransaction("e2etests", "test1")
Expand All @@ -70,7 +70,7 @@ class EnvelopeTests : BaseUiTest() {
initSentry(false) { options: SentryOptions ->
options.dsn = "https://640fae2f19ac4ba78ad740175f50195f@o1137848.ingest.sentry.io/6191083"
options.tracesSampleRate = 1.0
options.isProfilingEnabled = true
options.profilesSampleRate = 1.0
}

val transaction = Sentry.startTransaction("e2etests", "testProfile")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
android:name=".PermissionsActivity"
android:exported="false" />

<activity
android:name=".ProfilingActivity"
android:exported="false" />

<activity
android:name=".compose.ComposeActivity"
android:exported="false" />
Expand Down Expand Up @@ -100,7 +104,7 @@
<meta-data android:name="io.sentry.traces.sample-rate" android:value="1.0" />

<!-- how to enable profiling when starting transactions -->
<!-- <meta-data android:name="io.sentry.traces.profiling.enable" android:value="true" />-->
<meta-data android:name="io.sentry.traces.profiling.sample-rate" android:value="1.0" />

<!-- how to disable the Activity auto instrumentation for tracing-->
<!-- <meta-data android:name="io.sentry.traces.activity.enable" android:value="false" />-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ protected void onCreate(Bundle savedInstanceState) {
startActivity(new Intent(this, ComposeActivity.class));
});

binding.openProfilingActivity.setOnClickListener(
view -> {
startActivity(new Intent(this, ProfilingActivity.class));
});

setContentView(binding.getRoot());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package io.sentry.samples.android

import android.os.Bundle
import android.view.View
import android.widget.SeekBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import io.sentry.ITransaction
import io.sentry.ProfilingTraceData
import io.sentry.Sentry
import io.sentry.SentryEnvelopeItem
import io.sentry.samples.android.databinding.ActivityProfilingBinding
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.UUID
import java.util.concurrent.Executors
import java.util.zip.GZIPOutputStream

class ProfilingActivity : AppCompatActivity() {

private lateinit var binding: ActivityProfilingBinding
private val executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
private var profileFinished = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityProfilingBinding.inflate(layoutInflater)

binding.profilingDurationSeekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(p0: SeekBar, p1: Int, p2: Boolean) {
val seconds = getProfileDuration(p0)
binding.profilingDurationText.text = getString(R.string.profiling_duration, seconds)
}
override fun onStartTrackingTouch(p0: SeekBar) {}
override fun onStopTrackingTouch(p0: SeekBar) {}
})
val initialDurationSeconds = getProfileDuration(binding.profilingDurationSeekbar)
binding.profilingDurationText.text = getString(R.string.profiling_duration, initialDurationSeconds)

binding.profilingThreadsSeekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(p0: SeekBar, p1: Int, p2: Boolean) {
val backgroundThreads = getBackgroundThreads(p0)
binding.profilingThreadsText.text = getString(R.string.profiling_threads, backgroundThreads)
}
override fun onStartTrackingTouch(p0: SeekBar) {}
override fun onStopTrackingTouch(p0: SeekBar) {}
})
val initialBackgroundThreads = getBackgroundThreads(binding.profilingThreadsSeekbar)
binding.profilingThreadsSeekbar.max = Runtime.getRuntime().availableProcessors() - 1
binding.profilingThreadsText.text = getString(R.string.profiling_threads, initialBackgroundThreads)

binding.profilingList.adapter = ProfilingListAdapter()
binding.profilingList.layoutManager = LinearLayoutManager(this)

binding.profilingStart.setOnClickListener {
binding.profilingProgressBar.visibility = View.VISIBLE
profileFinished = false
val seconds = getProfileDuration(binding.profilingDurationSeekbar)
val threads = getBackgroundThreads(binding.profilingThreadsSeekbar)
val t = Sentry.startTransaction("Profiling Test", "$seconds s - $threads threads")
repeat(threads) {
executors.submit { runMathOperations() }
}
executors.submit { swipeList() }
binding.profilingStart.postDelayed({ finishTransactionAndPrintResults(t) }, (seconds * 1000).toLong())
}
setContentView(binding.root)
}

private fun finishTransactionAndPrintResults(t: ITransaction) {
t.finish()
binding.profilingProgressBar.visibility = View.GONE
profileFinished = true
val profilesDirPath = Sentry.getCurrentHub().options.profilingTracesDirPath
if (profilesDirPath == null) {
Toast.makeText(this, R.string.profiling_running, Toast.LENGTH_SHORT).show()
return
}
// Get the last trace file, which is the current profile
val origProfileFile = File(profilesDirPath).listFiles()?.maxByOrNull { f -> f.lastModified() }
// Create a new profile file and copy the content of the original file into it
val profile = File(cacheDir, UUID.randomUUID().toString())
origProfileFile?.copyTo(profile)
val profileLength = profile.length()
val traceData = ProfilingTraceData(profile, t)
// Create envelope item from copied profile
val item =
SentryEnvelopeItem.fromProfilingTrace(traceData, Long.MAX_VALUE, Sentry.getCurrentHub().options.serializer)
val itemData = item.data

// Compress the envelope item using Gzip
val bos = ByteArrayOutputStream()
GZIPOutputStream(bos).bufferedWriter().use { it.write(String(itemData)) }

binding.profilingResult.text =
getString(R.string.profiling_result, profileLength, itemData.size, bos.toByteArray().size)
}

private fun swipeList() {
while (!profileFinished) {
if ((binding.profilingList.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition() == 0) {
binding.profilingList.smoothScrollToPosition(100)
} else {
binding.profilingList.smoothScrollToPosition(0)
}
Thread.sleep(3000)
}
}

private fun runMathOperations() {
while (!profileFinished) {
fibonacci(25)
}
}

private fun fibonacci(n: Int): Int {
return when {
profileFinished -> n // If we destroy the activity we stop this function
n <= 1 -> 1
else -> fibonacci(n - 1) + fibonacci(n - 2)
}
}

override fun onResume() {
super.onResume()
Sentry.getSpan()?.finish()
}

override fun onBackPressed() {
if (profileFinished) {
super.onBackPressed()
} else {
Toast.makeText(this, R.string.profiling_running, Toast.LENGTH_SHORT).show()
}
}

private fun getProfileDuration(s: SeekBar): Float {
// Minimum duration of the profile is 100 milliseconds
return s.progress / 10.0F + 0.1F
}

private fun getBackgroundThreads(s: SeekBar): Int {
// Minimum duration of the profile is 100 milliseconds
return s.progress.coerceIn(0, Runtime.getRuntime().availableProcessors() - 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.sentry.samples.android

import android.graphics.Bitmap
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import io.sentry.samples.android.databinding.ProfilingItemListBinding
import kotlin.random.Random

class ProfilingListAdapter : RecyclerView.Adapter<ViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ProfilingItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.imageView.setImageBitmap(generateBitmap())
}

@Suppress("MagicNumber")
private fun generateBitmap(): Bitmap {
val bitmapSize = 128
val colors = (0 until (bitmapSize * bitmapSize)).map {
Color.rgb(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
}.toIntArray()
return Bitmap.createBitmap(colors, bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888)
}

// Disables view recycling.
override fun getItemViewType(position: Int): Int = position

override fun getItemCount(): Int = 200
}

class ViewHolder(binding: ProfilingItemListBinding) : RecyclerView.ViewHolder(binding.root) {
val imageView: ImageView = binding.benchmarkItemListImage
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/open_compose_activity"/>

<Button
android:id="@+id/open_profiling_activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/open_profiling_activity"/>
</LinearLayout>

</ScrollView>