Giter VIP home page Giter VIP logo

papa's Introduction

Square Papa

Performance of Android Production Applications

This library is not stable for usage beyond Square, the APIs and internals might change anytime.

Papa... ?

Table of contents

We still need a logo! logo_512.png

Usage

Add the papa dependency to your library or app's build.gradle file:

dependencies {
  implementation 'com.squareup.papa:papa:0.15'
}

Then install a PapaEventListener (see PapaEvent for details):

import android.app.Application
import papa.PapaEvent.AppLaunch
import papa.PapaEventListener
import papa.PapaEventLogger

class ExampleApplication : Application() {
  override fun onCreate() {
    super.onCreate()
    if (BuildConfig.DEBUG) {
      PapaEventListener.install(PapaEventLogger())
    }

    PapaEventListener.install { event ->
      when (event) {
        is AppLaunch -> {
          TODO("Log to analytics")
        }
      }
    }
  }
}

FAQ

It's not really a question, more like a comment..

License

Copyright 2021 Square Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

papa's People

Contributors

fcduarte avatar joshskeen avatar jstaahl-sc avatar nuhkoca avatar pyricau avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

papa's Issues

CurrentFrameMetricsListener is counting UNKNOWN_DELAY_DURATION duration twice?

While browsing some code noticed that

In class CurrentFrameMetricsListener -- line 47, 48 is double counting UNKNOWN_DELAY_DURATION twice?

  val drawEndNanos =
      frameMetrics.getMetric(INTENDED_VSYNC_TIMESTAMP) +
        frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION) +
        frameMetrics.getMetric(FrameMetrics.UNKNOWN_DELAY_DURATION) +

Easy navigation tracking

Figure out some APIs built on top of our interaction tracking to easily integrate with navigation systems.

The litmus test will be that it needs to work with our internal nav system as well as Jetpack's nav.

We also need to figure out inputs to nav and might need to tweak the APIs:

  • Most navs are usually triggers by input
  • Some navs are triggers by async work (e.g. after a loading network call)
  • The first nav is usualled triggers by app launch => this means we can pull up the launch start time. And the end time should be identical. But that'd also mean 2 events (launch + nav), is that right?
  • One simplification we've done internally is that the previous nav is always the previous nav, and that's handled in a centralized way. This might not work if you have several parallel activity tasks and you're switching behind them. Though it might still be interesting to represent it that way.

FrozenFrameOnTouchDetector frame timestamp

Currently FrozenFrameOnTouchDetector listens to ACTION_DOWN events and schedules a frame callback from there, combined with a post at front of queue, and logs a "frozen frame" if the time from down being issued to the end time is greater than 700. This seems wrong as we're scheduling for another frame instead of relying on "end of current frame" as frame end. We probably don't want to rely on frame metrics here, we just want to know how long things were blocked for. A post at front (not async) from event handling should be enough.

Interaction latency API considerations

Currently an interaction result includes the start and end event.

We should consider simply associating a list of events instead. We'll end up with a single event when start & finish are called on the same event, 2 events for normal delayed interactions, 2 events for normal canceled interactions (but 1 for timed out interactions). We could support more than 3 by having a way to pull a running interaction & associate it to the current event, whether that interaction is running or finishing.

There's separately a question of whether even having an interaction type at all is useful. Since the rules are event based and the events carry all the context needed, an interaction could be defined as a list of events with maybe a name.

To do this we'd likely also need to provide access to all past events associated with a given running interactions, so that you could look them up and find the ones with start ids related to the id of the current event.

One consistent feedback is that cancelRunningInteractions() sounds like it would cancel all interactions when in fact it's all interactions local to the rule. Some have expressed the need to actually do that though, i.e. cancel for a bunch of rules, which might be done by handling all sub rules in a single rule.

Figure out pattern for configuration / setup

  • Should support init via ContentProvider, direct call, or jetpack startup
  • On init, there's a bunch of work which we want to make it possible to skip / disable as a configuration option. One approach is using XML bool resources. Another approach is to have a "Configurator" class that's defined as an XML string resource and is loaded dynamically. Another option is to make configuration something possible only with manual install, where one passes the config in.

Cancellation via timeout in LaunchTracker not needed to support trampolining

Currently, a launch is considered in-progress until there are 500+ ms between an executed runnable posted from a lifecycle callback and the next lifecycle callback (or we receive our first frame). It seems this timeout-based approach for what is considered a launch still-in-progress is used to support the case of trampoline activities. Outside of the context of trampoline activities, this timeout doesn't really come into play. That's because if you post a runnable during an activity lifecycle event, it will not get executed until after onResume. In the context of trampolining activities, I think a good way to distill this down to what this actually accomplishes is to consider the path to the first frame after onResume as unbroken as long as the distance between the last lifecycle callback of one activity to the first lifecycle callback of the next is less than 500ms. However, I don't believe we need a timeout-based approach like this to support trampoline activities.

I believe that a more deterministic approach is to allow a launch measurement to continue indefinitely (until the first frame is rendered following onResume), unless the app ever goes from 1+ started activities to 0 started activities. In my testing, under all trampoline configurations, I am unable to get a transition from 1+ started activities to 0 started activities through the trampoline. Through the trampoline, we only ever go from 0 to 1+ started activities, and always remain at 1+ started activities.

API 34 calls first post before activity is created

Running Papa sample app I have noticed that on API 34 preLaunchState was always warm (preLaunchState=NO_ACTIVITY_NO_SAVED_STATE(WARM)). Adding some debugging log I notice that the recording of the first post on main thread is called before the recordActivityCreated is called.

This makes the calculation in computePreLaunchState never detect a cold state.
Here are the logs I captured force-closing the application.

API 31

papa: recordActivityCreated com.example.papa.MainActivity
Papa First post
AppLaunch(preLaunchState=NO_PROCESS(COLD), duration=269 ms, isSlowLaunch=false, trampolined=false, backgroundDuration=5496 ms, startUptimeMillis=7484542)
AppStartData(processStartRealtimeMillis=7484340, processStartUptimeMillis=7484340, handleBindApplicationElapsedUptimeMillis=202, firstAppClassLoadElapsedUptimeMillis=280, perfsInitElapsedUptimeMillis=292, importance=100, importanceAfterFirstPost=100, importanceReasonCode=0, importanceReasonPid=0, startImportanceReasonComponent=null, lastAppVisibilityState=INVISIBLE, lastVisibilityChangeElapsedTimeMillis=5585, lastAppAliveElapsedTimeMillis=3293, appTasks=[AppTask(topActivity={com.example.papa/com.example.papa.MainActivity}, elapsedSinceLastActiveRealtimeMillis=308, numActivities=1, baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 pkg=com.example.papa cmp=com.example.papa/.MainActivity })], classLoaderInstantiatedElapsedUptimeMillis=281, applicationInstantiatedElapsedUptimeMillis=291, firstIdleElapsedUptimeMillis=476, appUpdateData=RealAppUpdateData(status=NORMAL_START, firstInstallTimeMillis=1697521926122, lastUpdateTimeMillis=1697521926122, allInstalledVersionNames=[null], allInstalledVersionCodes=[0], updatedOsSinceLastStart=false, rebootedSinceLastStart=false, crashedInLastProcess=false, elapsedRealtimeSinceCrash=null), firstPostElapsedUptimeMillis=405, firstPostAtFrontElapsedUptimeMillis=320, firstComponentInstantiated=papa.AndroidComponentEvent@f3400f, firstActivityOnCreate=papa.ActivityOnCreateEvent@fb1089c, firstActivityOnStart=papa.AndroidComponentEvent@3a862a5, firstActivityOnResume=papa.AndroidComponentEvent@d5e767a, firstGlobalLayout=papa.AndroidComponentEvent@5adce2b, firstPreDraw=papa.AndroidComponentEvent@141288, firstDraw=papa.AndroidComponentEvent@3618421, firstIdleAfterFirstDraw=papa.AndroidComponentEvent@94d8046, firstPostAfterFirstDraw=papa.AndroidComponentEvent@f392207, firstTouchEvent=null, firstFrameAfterFullyDrawnElapsedUptimeMillis=null, customFirstEvents={})

API 34

Papa First post
papa: recordActivityCreated com.example.papa.MainActivity
AppLaunch(preLaunchState=NO_ACTIVITY_NO_SAVED_STATE(WARM), duration=62 ms, isSlowLaunch=false, trampolined=false, backgroundDuration=39160 ms, startUptimeMillis=101300601)
AppStartData(processStartRealtimeMillis=154339820, processStartUptimeMillis=101300428, handleBindApplicationElapsedUptimeMillis=49, firstAppClassLoadElapsedUptimeMillis=110, perfsInitElapsedUptimeMillis=122, importance=100, importanceAfterFirstPost=100, importanceReasonCode=0, importanceReasonPid=0, startImportanceReasonComponent=null, lastAppVisibilityState=INVISIBLE, lastVisibilityChangeElapsedTimeMillis=39109, lastAppAliveElapsedTimeMillis=37840, appTasks=[AppTask(topActivity={com.example.papa/com.example.papa.MainActivity}, elapsedSinceLastActiveRealtimeMillis=140, numActivities=1, baseIntent=Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 pkg=com.example.papa cmp=com.example.papa/.MainActivity })], classLoaderInstantiatedElapsedUptimeMillis=111, applicationInstantiatedElapsedUptimeMillis=121, firstIdleElapsedUptimeMillis=238, appUpdateData=RealAppUpdateData(status=NORMAL_START, firstInstallTimeMillis=1697027361683, lastUpdateTimeMillis=1697529874253, allInstalledVersionNames=[null], allInstalledVersionCodes=[0], updatedOsSinceLastStart=false, rebootedSinceLastStart=false, crashedInLastProcess=false, elapsedRealtimeSinceCrash=null), firstPostElapsedUptimeMillis=154, firstPostAtFrontElapsedUptimeMillis=151, firstComponentInstantiated=papa.AndroidComponentEvent@d43ab9d, firstActivityOnCreate=papa.ActivityOnCreateEvent@c881812, firstActivityOnStart=papa.AndroidComponentEvent@d33ebe3, firstActivityOnResume=papa.AndroidComponentEvent@58cc9e0, firstGlobalLayout=papa.AndroidComponentEvent@5b46499, firstPreDraw=papa.AndroidComponentEvent@bcba35e, firstDraw=papa.AndroidComponentEvent@a2f633f, firstIdleAfterFirstDraw=papa.AndroidComponentEvent@824a40c, firstPostAfterFirstDraw=papa.AndroidComponentEvent@eddb155, firstTouchEvent=null, firstFrameAfterFullyDrawnElapsedUptimeMillis=null, customFirstEvents={})

is this a known issue or I am reading this wrong?

Multi warm start logs

Quotes from someone reaching out:

So, I tried to use tart library 0.4.1 in our code, it seems that when i install it and launch Coldstart, it marks it as "warm_start".
It considers the app in PreLaunchState.NO_ACTIVITY_NO_SAVED_STATE -> "warm start"
Also, when the app starts up in Coldstart, there are multiple calls to the listeners and it prints multiple warm start values. I am not sure but our app actually calls multiple activities during login.

bash-3.2$ cat temp.log | grep "launch:"
11-14 01:31:08.776 16717 16717 I System.out: cold start launch: 26161 ms
11-14 01:31:08.873 16717 16717 I System.out: warm start launch: 402 ms
11-14 01:31:17.739 16717 16717 I System.out: warm start launch: 4835 ms
11-14 01:31:17.981 16717 16717 I System.out: warm start launch: 3930 ms

So, when I do an app coldstart, it does properly do the "cold start launch: 26161 ms" logging. However, during startup for login screen, we create multiple activites and the screen does go back and forth, I am assuming that is the reason, we are see multiple warm starts. However, this happens only for login. I will need to test it for our regular flow.

bash-3.2$ cat temp4.log | grep "launch:"
11-14 15:09:25.133 28592 28592 I System.out: cold start launch: 25984 ms AppLaunch(preLaunchState=NO_PROCESS_FIRST_LAUNCH_AFTER_INSTALL, start=CpuDuration(unit=MILLISECONDS, uptime=51343916, realtime=51343916), end=CpuDuration(unit=NANOSECONDS, uptime=51369900643380, realtime=51369900636193), duration=CpuDuration(unit=NANOSECONDS, uptime=25984643380, realtime=25984636193), isSlowLaunch=true)
11-14 15:09:25.176 28592 28592 I System.out: warm start launch: 451 ms AppLaunch(preLaunchState=NO_ACTIVITY_NO_SAVED_STATE, start=CpuDuration(unit=NANOSECONDS, uptime=51370052020166, realtime=51370052022718), end=CpuDuration(unit=NANOSECONDS, uptime=51370503960940, realtime=51370503959638), duration=CpuDuration(unit=NANOSECONDS, uptime=451940774, realtime=451936920), isSlowLaunch=false)
11-14 15:09:34.478 28592 28592 I System.out: warm start launch: 4987 ms AppLaunch(preLaunchState=NO_ACTIVITY_NO_SAVED_STATE, start=CpuDuration(unit=NANOSECONDS, uptime=51373486448008, realtime=51373486450925), end=CpuDuration(unit=NANOSECONDS, uptime=51378473692463, realtime=51378473690119), duration=CpuDuration(unit=NANOSECONDS, uptime=4987244455, realtime=4987239194), isSlowLaunch=true)
11-14 15:09:34.700 28592 28592 I System.out: warm start launch: 3900 ms AppLaunch(preLaunchState=NO_ACTIVITY_NO_SAVED_STATE, start=CpuDuration(unit=NANOSECONDS, uptime=51375758783495, realtime=51375758786776), end=CpuDuration(unit=NANOSECONDS, uptime=51379659350550, realtime=51379659348675), duration=CpuDuration(unit=NANOSECONDS, uptime=3900567055, realtime=3900561899), isSlowLaunch=true)

when you tap the app icon, that launches an activity, which then .. finishes itself and launches another activity?

yes, but the activity is then paused and it launches another activity

Make the interaction a val

If mutability is desired it's easy enough to make the interaction class itself hold onto mutable fields.

Windowless onCurrentFrame / onNextFrame?

Currently these APIs are extension functions on Window because frame metrics are per window.

Need to experiment with multi window (activity + dialog) to see if the metrics actually differ. All windows are rendered in one go. A possible difference might be if each traversal is measured separately? tbd.

If there's no difference, then we maybe could come up with a way to grab any or "the best" window and just run with that, removing the need for a window, which is nice when you're in the middle of your navigation code.

onCurrentFrameRendered should join multiple calls into a single post

Current impl:

internal fun onCurrentFrameRendered(callback: (Long) -> Unit) {
  mainHandler.postAtFrontOfQueueAsync {
    val frameRenderedUptimeMillis = SystemClock.uptimeMillis()
    callback(frameRenderedUptimeMillis)
  }
}

This should be stateful and aware of any pending calls so that we only register a single call and a single uptime and dispatch to all callbacks

Add debug drawing of interactions in flight

We should use an overlay window that adds text that changes for each frame. One row per interaction in flight. Likely we should use the event toString() as the name. We should show counters for how many frames an interaction has been running, and also add information about the event that started the interaction (tap, key, etc)

use ApplicationExitInfo to retrieve lastAppAliveElapsedTimeMillis

Using shared preferences with a 1 second interval causes a lot of reads/writes to disk, retrieve elapsed time millis from ApplicationExitInfo.

val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val applicationExitInfos: List<ApplicationExitInfo> = am.getHistoricalProcessExitReasons(null, 0, 0)
applicationExitInfos.first().timestamp

Switch to window based commits

https://developer.android.com/reference/android/view/ViewTreeObserver#registerFrameCommitCallback(java.lang.Runnable)

We can reimplement this pre API 29 by using a draw (or pre draw) listener then posting at front. JankStats likely does this.

The sticking point is, you need a window. But maybe we should just grab all windows and go with the timestamp for the first one.

We can also get smarter when we have a touch event, as we'll know which window the touch event is associated with and could check the UI for that touch event. Of course that might prove tricky if the touch is to close a window and we send the update event from the below window long after it's gone.

Naming nits

So like the events in TrackedInteraction isn’t super straightforward - events isn’t a good name for that I think sentEvents would make more sense. I had to explain this which is why I’m saying this.

SafeTraceComposable

Currently you can't wrap composables with safe trace, I was able to get this working with this approach:

@Composable
inline fun  safeTraceComposable(
  crossinline label: () -> String,
  crossinline block: @Composable () -> Unit
) {
  if (!SafeTrace.isCurrentlyTracing) {
    block()
  } else {
    SafeTrace.beginSection(label())
    block()
    SafeTrace.endSection()
  }
}

I can open a PR for this if you think it would be a good addition, but didn't know how you wanted to handle depending on Compose.

AppComponentFactory and compatibility with other factories

Unfortunately each app can only have a single AppComponentFactory. Since Papa uses a non-public AppComponentFactory applications that have their own custom factory cannot effectively use Papa.

One solution would be to make Papa's factory public so consumers could pass their own factory as Papa's delegate instead of only supporting AndroidX's.

More straightforward way to provide end metadata for interactions?

Currently there's an interaction object that can be updated, that works but not convenient as it has to be created early, so the end metadata is typically a lateinit var, which you might forget. We want to make sure we don't forget to provide the data as we call finish.

Method 'int android.view.View.getWindowAttachCount()' is inaccessible to class 'android.view.ViewSpyInternalKt' (declaration of 'android.view.ViewSpyInternalKt' appears in base.apk)

Trace:

java.lang.IllegalAccessError: Method 'int android.view.View.getWindowAttachCount()' is inaccessible to class 'android.view.ViewSpyInternalKt' (declaration of 'android.view.ViewSpyInternalKt' appears in base.apk)
        at android.view.JavaViewSpy.windowAttachCount(JavaViewSpy:23)
        at android.view.ViewSpyInternalKt.windowAttachCount(ViewSpyInternalKt:8)
        at curtains.WindowsKt.getWindowAttachCount(WindowsKt:219)
        at papa.internal.RealInputTracker$listener$1.onRootViewAdded(RealInputTracker:78)
        at curtains.OnRootViewAddedListener$DefaultImpls.onRootViewsChanged(OnRootViewAddedListener:38)
        at papa.internal.RealInputTracker$listener$1.onRootViewsChanged(RealInputTracker:76)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.add(RootViewsSpy:25)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.add(RootViewsSpy:23)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:399)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:148)
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4859)
        at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:57)
        at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:179)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2306)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7918)
        at java.lang.reflect.Method.invoke(Method.java:-2)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

Issue looks similar to square/curtains#37

Leverage onPreDraw callback

Currently we leverage onDraw instead of onPreDraw, which is better as it's guaranteed to be called even if it turns out there's nothing to draw.

Add event system

Need to create an event recording system that we can hook into to set listeners (otherwise they're lost) and then each app launch sets a listener and provides the list of events once done. The EventRecorder need to be thread safe. We also likely need 2 recorders or a way to duplicate because process start is one and cold app launch is another. Also need to be able to insert events at a specific time. Need to be able to add custom events.

Once we have that we can migrate a lot of the custom measurements to these events.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.