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

Demo app: slow renders animation #627

Merged
merged 7 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demo-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ The OpenTelemetry Android Demo App currently supports the following features:
- Automatically detects instances of slow rendering within the app.
- Slow render events are captured as spans, providing information on when and where rendering delays occurred.
- The span includes attributes such as `activity.name`, `screen.name`, `count`, and network details to help diagnose performance issues.
- Note: The app currently does not have any features designed to intentionally trigger slow rendering.
- To trigger a slowly rendering animation in the demo app, add any quantity of The Comet Book (the last product on the product list) to the cart. Note that the number of `slow-render` spans and their respective `count` attributes may vary between runs or across different machines.

* Manual Instrumentation
- Provides access to the OpenTelemetry APIs for manual instrumentation, allowing developers to create custom spans and events as needed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package io.opentelemetry.android.demo.shop.ui.components

import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.zIndex
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.security.SecureRandom

@Composable
fun SlowCometAnimation(modifier: Modifier = Modifier) {
val cometParams = listOf(
CometParams(startX = 0f, startY = 0f, targetX = 1.3f, targetY = 1.4f),
CometParams(startX = 0.2f, startY = 0f, targetX = 1.4f, targetY = 0.9f),
CometParams(startX = 0.2f, startY = -0.1f, targetX = 1.5f, targetY = 0.8f),
CometParams(startX = 0f, startY = 0.6f, targetX = 1.2f, targetY = 1.7f),
CometParams(startX = -0.2f, startY = 0.1f, targetX = 1.4f, targetY = 1.3f),
CometParams(startX = 0.5f, startY = 0f, targetX = 1.7f, targetY = 1.3f),
CometParams(startX = 0f, startY = 0f, targetX = 1.3f, targetY = 1.4f),
CometParams(startX = 0.2f, startY = 0f, targetX = 1.4f, targetY = 1.3f),
CometParams(startX = 0f, startY = 0f, targetX = 1.4f, targetY = 1.3f),
CometParams(startX = 0.2f, startY = -0.1f, targetX = 1.5f, targetY = 0.7f),
CometParams(startX = 0f, startY = 0.6f, targetX = 1.2f, targetY = 1.7f),
CometParams(startX = -0.2f, startY = 0.1f, targetX = 1.3f, targetY = 1.4f),
)

val cometVisibility = remember { mutableStateListOf<Boolean>().apply { addAll(List(cometParams.size) { false }) } }

LaunchedEffect(Unit) {
cometParams.forEachIndexed { index, _ ->
launch {
delay(index * 500L)
cometVisibility[index] = true
}
}
}

cometParams.forEachIndexed { index, params ->
if (cometVisibility[index]) {
SlowSingleCometAnimation(
startX = params.startX,
startY = params.startY,
targetX = params.targetX,
targetY = params.targetY,
modifier = modifier
)
}
}
}

@Composable
fun SlowSingleCometAnimation(
startX: Float,
startY: Float,
targetX: Float,
targetY: Float,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()

val tailPositions = remember { mutableStateListOf<Offset>() }

val animatedX = remember { Animatable(startX) }
val animatedY = remember { Animatable(startY) }

LaunchedEffect(Unit) {
scope.launch {
launch {
delay(500)
animatedX.animateTo(
targetValue = targetX,
animationSpec = tween(durationMillis = 3000, easing = LinearEasing)
)
}
launch {
delay(500)
animatedY.animateTo(
targetValue = targetY,
animationSpec = tween(durationMillis = 3000, easing = LinearEasing)
)
}
}
}

LaunchedEffect(animatedX.value, animatedY.value) {
val currentPosition = Offset(animatedX.value, animatedY.value)
tailPositions.add(currentPosition)

if (tailPositions.size > 30) {
tailPositions.removeAt(0)
}
}

val random = SecureRandom()
repeat(10_000) {
random.nextFloat()
}

ExpensiveCometShape(
modifier = modifier,
headX = animatedX.value,
headY = animatedY.value,
tailPositions = tailPositions
)
}

@Composable
fun ExpensiveCometShape(
modifier: Modifier = Modifier,
headX: Float = 0.8f,
headY: Float = 0.3f,
tailPositions: List<Offset> = emptyList()
) {
Canvas(modifier = modifier) {
val headRadius = size.minDimension * 0.05f
val animatedHeadX = size.width * headX
val animatedHeadY = size.height * headY


for (i in tailPositions.indices) {
val position = tailPositions[i]
val posX = size.width * position.x
val posY = size.height * position.y

val progress = i / tailPositions.size.toFloat()
val tailRadius = headRadius * (1 - progress)
val tailAlpha = 0.3f * (1 - progress)

repeat(100) {
drawCircle(
color = Color(0xFFFFD700).copy(alpha = tailAlpha),
radius = tailRadius,
center = Offset(posX, posY)
)
}
}

repeat(100) {
drawCircle(
color = Color(0x80FFFF00),
radius = headRadius * 2,
center = Offset(animatedHeadX, animatedHeadY)
)
drawCircle(
color = Color(0xAAFFA500),
radius = headRadius * 1.5f,
center = Offset(animatedHeadX, animatedHeadY)
)
drawCircle(
color = Color(0xFFFFD700),
radius = headRadius,
center = Offset(animatedHeadX, animatedHeadY)
)
drawCircle(
color = Color.White,
radius = headRadius * 0.6f,
center = Offset(animatedHeadX, animatedHeadY)
)
}
}
}


@Preview(showBackground = true)
@Composable
fun PreviewCometAnimation() {
SlowCometAnimation(
Modifier
.fillMaxSize()
.zIndex(1f)
)
}

data class CometParams(
val startX: Float,
val startY: Float,
val targetX: Float,
val targetY: Float
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import io.opentelemetry.android.demo.shop.ui.cart.CartViewModel
import io.opentelemetry.android.demo.shop.ui.components.UpPressButton
import androidx.compose.ui.Alignment
import androidx.compose.ui.zIndex
import io.opentelemetry.android.demo.shop.clients.ProductCatalogClient
import io.opentelemetry.android.demo.shop.clients.RecommendationService
import io.opentelemetry.android.demo.shop.ui.components.SlowCometAnimation
import io.opentelemetry.android.demo.shop.ui.components.ConfirmCrashPopup
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
Expand All @@ -39,6 +41,8 @@ fun ProductDetails(
val sourceProductImage = imageLoader.load(product.picture)
var quantity by remember { mutableIntStateOf(1) }

var slowRender by remember { mutableStateOf(false) }

val productsClient = ProductCatalogClient(context)
val recommendationService = remember { RecommendationService(productsClient, cartViewModel) }
val recommendedProducts = remember { recommendationService.getRecommendedProducts(product) }
Expand Down Expand Up @@ -86,7 +90,11 @@ fun ProductDetails(
Spacer(modifier = Modifier.height(32.dp))
QuantityChooser(quantity = quantity, onQuantityChange = { quantity = it })
Spacer(modifier = Modifier.height(16.dp))
AddToCartButton(cartViewModel = cartViewModel, product = product, quantity = quantity)
AddToCartButton(
cartViewModel = cartViewModel,
product = product,
quantity = quantity,
onSlowRenderChange = { slowRender = it })
Spacer(modifier = Modifier.height(32.dp))
RecommendedSection(recommendedProducts = recommendedProducts, onProductClick = onProductClick)
}
Expand All @@ -97,23 +105,33 @@ fun ProductDetails(
.align(Alignment.TopStart)
.padding(8.dp)
)
if (slowRender) {
SlowCometAnimation(
modifier = Modifier
.fillMaxSize()
.zIndex(1f)
)
}
}
}

@Composable
fun AddToCartButton(
cartViewModel: CartViewModel,
product: Product,
quantity: Int
quantity: Int,
onSlowRenderChange: (Boolean) -> Unit
) {

var showPopup by remember { mutableStateOf(false) }

Button(
onClick = {
if (product.id == "OLJCESPC7Z" && quantity == 10) {
showPopup = true
} else {
if (product.id == "HQTGWGPNH4") {
onSlowRenderChange(true)
}
cartViewModel.addProduct(product, quantity)
}
},
Expand Down Expand Up @@ -161,7 +179,3 @@ fun multiThreadCrashing(numThreads : Int = 4) {
}
latch.countDown()
}