Skip to content
Open
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
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ dependencies {
androidTestImplementation(composeBom)
debugImplementation(libs.compose.debug.test)
debugImplementation(composeBom)

implementation libs.androidx.metrics.performance

}

private setSigningConfigKey(config, Properties props) {
Expand Down
87 changes: 87 additions & 0 deletions app/src/main/java/org/wikipedia/JankStatsAggregator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.wikipedia

import android.view.Window
import androidx.metrics.performance.FrameData
import androidx.metrics.performance.JankStats

/**
* This utility class can be used to provide a simple data aggregation mechanism for JankStats.
* Instead of receiving a callback on every frame and caching that data, JankStats users can
* create JankStats indirectly through this Aggregator class, which will compile the data
* and issue it upon request.
*
* @param window The Window for which stats will be tracked. A JankStatsAggregator
* instance is specific to each window in an application, since the timing metrics are
* tracked on a per-window basis internally.
* @param onJankReportListener This listener will be called whenever there is a call to
* [issueJankReport].
* @throws IllegalStateException This function will throw an exception if `window` has
* a null DecorView.
*/
class JankStatsAggregator(
window: Window,
private val onJankReportListener: OnJankReportListener
) {

private val listener = JankStats.OnFrameListener { frameData ->
++numFrames
if (frameData.isJank) {
jankReport.add(frameData.copy())
if (jankReport.size >= REPORT_BUFFER_LIMIT) {
issueJankReport("Max buffer size reached")
}
}
}

val jankStats = JankStats.createAndTrack(window, listener)

private var jankReport = ArrayList<FrameData>()

private var numFrames: Int = 0

/**
* Issue a report on current jank data. The data includes FrameData for every frame
* experiencing jank since the listener was set, or since the last time a report
* was issued for this JankStats object. Calling this function will cause the jankData
* to be reset and cleared. Note that this function may be called externally, from application
* code, but it may also be called internally for various reasons (to reduce memory size
* by clearing the buffer, or because there was an important lifecycle event). The
* [reason] parameter explains why the report was issued if it was not called externally.
*
* @param reason An optional parameter specifying the reason that the report was issued.
* This parameter may be used if JankStats issues a report for some internal reason.
*/
fun issueJankReport(reason: String = "") {
val jankReportCopy = jankReport
val numFramesCopy = numFrames
onJankReportListener.onJankReport(reason, numFramesCopy, jankReportCopy)
jankReport = ArrayList()
numFrames = 0
}

/**
* This listener is called whenever there is a call to [issueJankReport].
*/
fun interface OnJankReportListener {
/**
* The implementation of this method will be called whenever there is a call
* to [issueJankReport].
*
* @param reason Optional reason that this report was issued
* @param totalFrames The total number of frames (jank and not) since collection
* began (or since the last time the report was issued and reset)
* @param jankFrameData The FrameData for every frame experiencing jank during
* the collection period
*/
fun onJankReport(reason: String, totalFrames: Int, jankFrameData: List<FrameData>)
}

companion object {
/**
* The number of frames for which data can be accumulated is limited to avoid
* memory problems. When the limit is reached, a report is automatically issued
* and the buffer is cleared.
*/
private const val REPORT_BUFFER_LIMIT = 1000
}
}
7 changes: 6 additions & 1 deletion app/src/main/java/org/wikipedia/feed/FeedFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.metrics.performance.PerformanceMetricsState
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.wikipedia.BackPressedHandler
Expand Down Expand Up @@ -132,11 +133,15 @@ class FeedFragment : Fragment(), BackPressedHandler {
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(requireActivity().window.decorView)
metricsStateHolder.state?.putState("screen", this.javaClass.simpleName)
}
override fun onResume() {
super.onResume()
maybeShowRegionalLanguageVariantDialog()
OnThisDayGameOnboardingFragment.maybeShowOnThisDayGameDialog(requireActivity(), InvokeSource.FEED)

// Explicitly invalidate the feed adapter, since it occasionally crashes the StaggeredGridLayout
// on certain devices.
// https://issuetracker.google.com/issues/188096921
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/java/org/wikipedia/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.metrics.performance.PerformanceMetricsState
import org.wikipedia.Constants
import org.wikipedia.JankStatsAggregator
import org.wikipedia.R
import org.wikipedia.activity.SingleFragmentActivity
import org.wikipedia.analytics.eventplatform.ImageRecommendationsEvent
Expand Down Expand Up @@ -38,6 +40,15 @@ class MainActivity : SingleFragmentActivity<MainFragment>(), MainFragment.Callba
}
}

private lateinit var jankStatsAggregator: JankStatsAggregator
private val jankReportListener = JankStatsAggregator.OnJankReportListener { reason, totalFrames, jankFrameData ->
println("JankStats: " +
"reason $reason" +
"totalFrames = $totalFrames" +
"jankFrames = ${jankFrameData.size}")
println("JankStats --> $jankFrameData")
}

override fun inflateAndSetContentView() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
Expand All @@ -48,6 +59,10 @@ class MainActivity : SingleFragmentActivity<MainFragment>(), MainFragment.Callba
if (!DeviceUtil.assertAppContext(this)) {
return
}
val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)
jankStatsAggregator = JankStatsAggregator(window, jankReportListener)
metricsStateHolder.state?.putState("screen", javaClass.simpleName)
metricsStateHolder.state?.putState("device", android.os.Build.MODEL)

setImageZoomHelper()
if (Prefs.isInitialOnboardingEnabled && savedInstanceState == null &&
Expand All @@ -67,9 +82,16 @@ class MainActivity : SingleFragmentActivity<MainFragment>(), MainFragment.Callba

override fun onResume() {
super.onResume()
jankStatsAggregator.jankStats.isTrackingEnabled = true
invalidateOptionsMenu()
}

override fun onPause() {
super.onPause()
jankStatsAggregator.issueJankReport("Activity paused")
jankStatsAggregator.jankStats.isTrackingEnabled = false
}

override fun createFragment(): MainFragment {
return MainFragment.newInstance()
}
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ kotlinxSerializationJson = "1.8.1"
kspPlugin = "2.1.21-2.0.1"
leakCanaryVersion = "2.14"
material = "1.12.0"
metricsPerformance = "1.0.0-beta02"
metricsVersion = "2.9"
mlKitVersion = "17.0.6"
mockitoVersion = "5.2.0"
Expand Down Expand Up @@ -55,6 +56,7 @@ android-sdk = { module = "org.maplibre.gl:android-sdk", version.ref = "androidSd
android-plugin-annotation-v9 = { module = "org.maplibre.gl:android-plugin-annotation-v9", version.ref = "androidPluginAnnotationV9" }
androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoVersion" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
androidx-metrics-performance = { module = "androidx.metrics:metrics-performance", version.ref = "metricsPerformance" }
androidx-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestrator" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
Expand Down
Loading