guardian / toolargetool

A tool to help you debug TransactionTooLargeExceptions on Android 7+
MIT License
1.11k stars 104 forks source link

Drill into viewHierarchyState #14

Open edenman opened 5 years ago

edenman commented 5 years ago

This library is great!

10-11 11:53:13.179 D/TooLargeTool(21791): MainActivity.onSaveInstanceState wrote: Bundle@161550193 contains 7 keys and measures 821.2 KB when serialized as a Parcel
10-11 11:53:13.179 D/TooLargeTool(21791): * android:sessionId = 0.1 KB
10-11 11:53:13.179 D/TooLargeTool(21791): * com.google.app_measurement.screen_service = 0.2 KB
10-11 11:53:13.179 D/TooLargeTool(21791): * map_state = 0.2 KB
10-11 11:53:13.179 D/TooLargeTool(21791): * android:fragments = 0.8 KB
10-11 11:53:13.179 D/TooLargeTool(21791): * @android:autofillResetNeeded = 0.1 KB
10-11 11:53:13.179 D/TooLargeTool(21791): * android:lastAutofillId = 0.1 KB
10-11 11:53:13.179 D/TooLargeTool(21791): * android:viewHierarchyState = 819.7 KB

I'd like to figure out what part of the viewHierarchyState is taking up so much room. Any thoughts on how to best allow drilling-down in a situation like this? I could specify keys that I want to be drilled-down into when I call TooLargeTool.startLogging(this)? Or we could automatically drill down into the keys above a certain size? Happy to put together a PR if you give me some guidance on what approach(es) you'd be ok with.

edenman commented 5 years ago

(i ended up just doing it manually but still think it'd be cool to make this generic)

maxspencer commented 5 years ago

This sounds like a nice idea for an improvement, thank you.

How did you tackle this manually? and if you had to make it generic how would you go about it?

The first idea springs to my mind is to make the tool print a kind of tree, that shows each "sub-Bundle" of the saved instance state (I'm assuming android:viewHierarchyState is indeed a Bundle here?) indented underneath their top-level key and total size. Simple values would look like they do now.

You know, now I come to think of it (this is going back a while so I could be wrong) I think the big bit of state I was looking for that prompted me to make this tool was actually nested inside a fragment's arguments and I did some manual extra work to identify the root cause then as well. I guess I did something like:

// Manually added in onCreate of offending fragment:
TooLargeTool.logBundleBreakdown(TAG, getArguments());

Obviously I was sick of the whole thing by then and didn't follow up with a proper generic solution! But maybe we can now?

edenman commented 5 years ago

Here's what I ended up doing:

  private var savedState: Bundle? = null

  override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    map.onSaveInstanceState(outState)
    savedState = outState
  }

  override fun onStop() {
    super.onStop()
    val foo = savedState
    if (foo != null) {
      for (key in foo.keySet()) {
        if (key == "android:viewHierarchyState") {
          val vhBundle = foo.getBundle("android:viewHierarchyState").require("No bundle")
          val viewsSparseArray = vhBundle.getSparseParcelableArray<Parcelable>("android:views")
              .require("No bundle for android:views")
          viewsSparseArray.forEach { viewKey, viewState ->
            val idName = safeNameForID(resources, viewKey)
            Timber.d("view name($idName) took size ${sizeOfParcel(viewState)}")
          }
        }
      }
      savedState = null
    }
  }

  fun sizeOfParcel(foo: Parcelable): Int {
    val parcel = Parcel.obtain()
    parcel.writeParcelable(foo, 0)
    val dataSize = parcel.dataSize()
    parcel.recycle()
    return dataSize
  }

I think it would make sense to just auto-expand keys above a certain size, but I'd have to do some tinkering to figure out how to detect the type of thing (android:viewHierarchyState is a Bundle, but then android:views is a SparseArray).

maxspencer commented 5 years ago

You can use BaseBundle.get(key: String): Object and then use is/instanceof to find out what the value is.

We have TooLargeTool.valueSizes(bundle: Bundle): Map<String, Int> which breaks down a Bundle and reports the size of each key, I guess we'll need a similar function to operate on a SparseArray too?

As a preliminary step I might introduce a type to better represent these tree structures because Map<String, Int> ain't gonna cut it.