Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ default.profraw
/Backend/BarkFluff.Web/wwwroot/legal/

/Android/.idea/caches
/Android/tools/perf/out/

# Docker local environment: secrets and host-mounted data
docker/**/.env
Expand Down
16 changes: 16 additions & 0 deletions Android/Barkfluff.Client.Android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ android {
"proguard-rules.pro"
)
}
create("benchmark") {
initWith(getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
isDebuggable = false
}
}
compileOptions {
// libsignal-android 0.86+ использует Java records (Java 16+) и требует
Expand Down Expand Up @@ -327,3 +333,13 @@ tasks.withType<org.gradle.api.tasks.testing.Test>().configureEach {
rootProject.file("tools/check_android_ui.py").absolutePath,
)
}

tasks.configureEach {
if ((name.startsWith("install") || name.startsWith("uninstall")) && name.endsWith("Benchmark")) {
doFirst {
check(!System.getenv("ANDROID_SERIAL").isNullOrBlank()) {
"Refusing to run $name without ANDROID_SERIAL. Use a verified benchmark device."
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<profileable android:shell="true" tools:targetApi="29" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.StrictMode
import android.os.Trace
import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
Expand All @@ -28,6 +30,7 @@ import com.barkfluff.client.domain.media.AutoDownloadNetworkState
import com.barkfluff.client.grpc.GrpcClientRegistry
import com.barkfluff.client.grpc.RealtimeService
import com.barkfluff.client.notifications.NotificationHelper
import com.barkfluff.client.perf.PerfTrace
import com.barkfluff.client.repository.ChatRepository
import com.barkfluff.client.send.OutgoingMessageQueue
import com.barkfluff.client.repository.PrivateChatRepository
Expand Down Expand Up @@ -139,7 +142,18 @@ class BarkFluffApplication : Application() {
var pendingDeepLink: android.net.Uri? = null

override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) enableStrictMode()
PerfTrace.beginAppToChats()
Trace.beginSection(PerfTrace.APPLICATION_ON_CREATE)
try {
super.onCreate()
onApplicationCreate()
} finally {
Trace.endSection()
}
}

private fun onApplicationCreate() {
applicationScope.launch(Dispatchers.IO) {
try {
prekeyManager.retryPendingCleanup()
Expand Down Expand Up @@ -209,6 +223,28 @@ class BarkFluffApplication : Application() {
})
}

private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.detectCustomSlowCalls()
.detectResourceMismatches()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects()
.detectLeakedSqlLiteObjects()
.detectActivityLeaks()
.detectLeakedRegistrationObjects()
.penaltyLog()
.build()
)
}

override fun onTerminate() {
audioPlayback.stop()
connectivityManager.unregisterNetworkCallback(networkCallback)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.barkfluff.client.voice.VoiceRecordingController
import com.barkfluff.client.voice.VoiceRecordingMode
import com.barkfluff.client.voice.VoiceRecordingState
import com.barkfluff.client.voice.VoiceDraftPreview
import com.barkfluff.client.perf.PerfTrace
import android.net.Uri
import android.os.Bundle
import android.os.Environment
Expand Down Expand Up @@ -137,6 +138,8 @@ class ChatActivity : AppCompatActivity() {
private val viewModel: ChatViewModel by viewModels()
private var submittedItemsVersion = 0L
private var committedItemsVersion = 0L
private var chatOpenTracePending = false
private var chatOpenTraceEnded = false
private var scrollToBottomJob: Job? = null

private lateinit var globalParam: GlobalParam
Expand Down Expand Up @@ -359,6 +362,7 @@ class ChatActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
chatOpenTracePending = intent.getBooleanExtra(PerfTrace.EXTRA_CHAT_OPEN, false)
binding = ActivityChatBinding.inflate(layoutInflater)
setContentView(binding.root)
messageActionsOverlay = MessageActionsOverlay(binding.chatRootLayout)
Expand Down Expand Up @@ -478,6 +482,10 @@ class ChatActivity : AppCompatActivity() {
val version = ++submittedItemsVersion
messageAdapter.submitList(messageRowProjector.project(state.items)) {
if (version != submittedItemsVersion) return@submitList
if (chatOpenTracePending && !chatOpenTraceEnded && messageAdapter.currentList.isNotEmpty()) {
PerfTrace.endChatOpen()
chatOpenTraceEnded = true
}
committedItemsVersion = version
if (applyMessageNavigation(listIsCommitted = true)) {
// A target has priority while its window is loading or committing.
Expand Down Expand Up @@ -3027,6 +3035,10 @@ class ChatActivity : AppCompatActivity() {
}

override fun onDestroy() {
if (chatOpenTracePending && !chatOpenTraceEnded) {
PerfTrace.endChatOpen()
chatOpenTraceEnded = true
}
binding.messagesRecyclerView.adapter = null
super.onDestroy()
// Сбрасываем открытый чат
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.barkfluff.client.data.OpenChatManager
import com.barkfluff.client.databinding.FragmentChatsBinding
import com.barkfluff.client.domain.model.ChatSummary
import com.barkfluff.client.domain.gateway.FileMediaGateway
import com.barkfluff.client.perf.PerfTrace
import com.barkfluff.client.utils.AvatarLoader
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
Expand Down Expand Up @@ -115,7 +116,13 @@ class ChatsFragment : Fragment() {
if (binding.chatRecyclerView.adapter !== chatAdapter) {
binding.chatRecyclerView.adapter = chatAdapter
}
chatAdapter.submitList(state.displayItems)
val hasRealChat = state.displayItems.any { !it.isFooter }
chatAdapter.submitList(state.displayItems) {
if (hasRealChat && isAdded && PerfTrace.markFirstChatDisplayed()) {
PerfTrace.endAppToChats()
requireActivity().reportFullyDrawn()
}
}
showEmptyState(state.displayItems.isEmpty())
if (initialScrollPending && state.displayItems.isNotEmpty()) {
initialScrollPending = false
Expand Down Expand Up @@ -475,6 +482,7 @@ class ChatsFragment : Fragment() {
}

private fun onChatClicked(chat: ChatSummary) {
PerfTrace.beginChatOpen()
// Находим display item для получения дополнительной информации
val displayItem = chatAdapter.currentList.find { !it.isFooter && it.chatData.id == chat.id }

Expand All @@ -485,7 +493,7 @@ class ChatsFragment : Fragment() {
title = displayItem?.displayTitle ?: chat.title.ifBlank { getString(R.string.create_chat_private) },
inviteState = chat.privateInviteState.number,
inviterUserId = chat.privateInviterUserId
))
).putExtra(PerfTrace.EXTRA_CHAT_OPEN, true))
return
}

Expand All @@ -495,6 +503,7 @@ class ChatsFragment : Fragment() {
putExtra("chat_avatar_file_id", displayItem?.displayAvatarFileId ?: chat.pictureFileId.ifBlank { null })
putExtra("is_group_chat", chat.isGroupChat)
putExtra("other_user_id", displayItem?.otherUserId ?: 0L)
putExtra(PerfTrace.EXTRA_CHAT_OPEN, true)
}

// Устанавливаем чат как открытый перед запуском Activity
Expand Down
56 changes: 56 additions & 0 deletions Android/benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.kotlin.android)
}

android {
namespace = "com.barkfluff.benchmark"
compileSdk = 36

defaultConfig {
minSdk = 31
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy("channel", "nightly")
}

targetProjectPath = ":app-v1"
experimentalProperties["android.experimental.self-instrumenting"] = true

buildTypes {
create("benchmark") {
isDebuggable = true
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}

dependencies {
implementation(libs.androidx.benchmark.macro.junit4)
implementation(libs.androidx.test.uiautomator)
implementation(libs.androidx.junit)
}

tasks.configureEach {
if (name.startsWith("connected") || name.startsWith("install") || name.startsWith("uninstall")) {
doFirst {
check(!System.getenv("ANDROID_SERIAL").isNullOrBlank()) {
"Refusing to run $name without ANDROID_SERIAL. Use a verified benchmark device."
}
}
}
}
6 changes: 6 additions & 0 deletions Android/benchmark/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<package android:name="com.barkfluff.nightly" />
</queries>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.barkfluff.benchmark

import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.test.uiautomator.UiObject2

internal const val TARGET_PACKAGE = "com.barkfluff.nightly"
internal const val UI_WAIT_TIMEOUT_MS = 10_000L
internal const val UI_POLL_INTERVAL_MS = 100L
private const val UI_STATE_CHECK_TIMEOUT_MS = 250L

internal object BenchmarkCompilationModes {
fun parameters(): Collection<Array<Any>> = listOf(
arrayOf<Any>(CompilationMode.None()),
arrayOf<Any>(
CompilationMode.Partial(
baselineProfileMode = BaselineProfileMode.Disable,
warmupIterations = 3,
)
),
)
}

internal fun MacrobenchmarkScope.waitForChatList() {
val chatTitle = onElementOrNull(
timeoutMs = UI_WAIT_TIMEOUT_MS,
pollIntervalMs = UI_POLL_INTERVAL_MS,
) {
viewIdResourceName == "$TARGET_PACKAGE:id/chatTitle"
}
check(chatTitle != null) {
"Не найден список чатов. Залогиньтесь в тестовый аккаунт в benchmark-сборке " +
"и выдайте POST_NOTIFICATIONS."
}
}

internal fun MacrobenchmarkScope.ensureChatList() {
when {
hasVisibleView("$TARGET_PACKAGE:id/chatTitle") -> return
hasVisibleView("$TARGET_PACKAGE:id/messagesRecyclerView") -> pressBack()
else -> {
pressHome()
startActivityAndWait()
}
}
waitForChatList()
}

private fun MacrobenchmarkScope.hasVisibleView(resourceName: String): Boolean = onElementOrNull(
timeoutMs = UI_STATE_CHECK_TIMEOUT_MS,
pollIntervalMs = UI_POLL_INTERVAL_MS,
) {
viewIdResourceName == resourceName
} != null

internal fun MacrobenchmarkScope.firstChatCard(): UiObject2 = onElementOrNull(
timeoutMs = UI_WAIT_TIMEOUT_MS,
pollIntervalMs = UI_POLL_INTERVAL_MS,
) {
viewIdResourceName == "$TARGET_PACKAGE:id/chatCard"
} ?: error("Не найден chatCard в списке чатов тестового аккаунта.")

internal fun MacrobenchmarkScope.waitForMessages(): UiObject2 = onElementOrNull(
timeoutMs = UI_WAIT_TIMEOUT_MS,
pollIntervalMs = UI_POLL_INTERVAL_MS,
) {
viewIdResourceName == "$TARGET_PACKAGE:id/messagesRecyclerView" && childCount > 0
} ?: error("Не появились сообщения в messagesRecyclerView за ${UI_WAIT_TIMEOUT_MS}ms.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.barkfluff.benchmark

import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.ExperimentalMetricApi
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.TraceSectionMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

@RunWith(Parameterized::class)
class ChatOpenBenchmark(private val compilationMode: CompilationMode) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()

@OptIn(ExperimentalMetricApi::class)
@Test
fun openFirstChat() {
benchmarkRule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = listOf(
TraceSectionMetric("BF:chatOpen", TraceSectionMetric.Mode.First),
FrameTimingMetric(),
),
compilationMode = compilationMode,
iterations = 10,
setupBlock = {
ensureChatList()
},
) {
firstChatCard().click()
waitForMessages()
}
}

companion object {
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun compilationModes(): Collection<Array<Any>> = BenchmarkCompilationModes.parameters()
}
}
Loading
Loading