Leakcanary: Improve text rendering of leaks

Created on 18 Oct 2018  ยท  9Comments  ยท  Source: square/leakcanary

After testing this it's different people, it's clear that we could improve text LeakTraces.

Note: the comments reflects different things we've tried, but this issue description has been updated to reflect the latest.

Current state

In com.example.leakcanary:1.0:1.
* com.example.leakcanary.MainActivity has leaked:
* Toast$TN.mNextView
* โ†ณ LinearLayout.mContext
* โ†ณ MainActivity.!(httpRequestHelper)!
* โ†ณ HttpRequestHelper.!(button)!
* โ†ณ Button.mContext
* โ†ณ MainActivity

Goals

  • Enable Android developers to fix leaks quickly by pointing them to the potential leak causes.
  • Help developers understand how Leakcanary is identifying the potential leak causes.

Insights

  • Developers tend to focus on instances in the leak chain, when they should instead focus on the references between instances. The cause of a leak is an incorrect reference.
  • Fully qualified class names add noise that impacts legibility
  • The first thing developers care about is which instead is leaking. Then which instance is causing that instance to leak. Etc. So we should put the gc roots at the bottom. While trees are traditionally represented with root at the top, this is a special case (linear, no sibling). Worth noting Java stacktraces are also represented with the root of the call stack at the bottom.
  • It should be clear that the leak trace is a directed chain.
  • The leak trace should be as self served as possible:

    • Use traditional graphical representations instead of words to improve legibility, if some representations can be interpreted without subtext.

    • Use minimum words necessary to provide context

  • To help understand how LeakCanary is identifying the potential leak causes, we need to surface which instances it believes are leaking and which it knows are not.

    • We need to do this in a way that feels like metadata rather than the most important information. Developers tend to jump to "this instance is leaking, therefore the problem is there". The problem isn't there, the problem is in between the group of non leaking instances and the group of leaking instances.

  • We looked at many ways to represent the known state of instances (when it is known).

    • The most correct would be: "This instance is reachable as we expected" and "This instance is reachable but should be unreachable"

    • We tried โœ“, โ€ , โœ— . However, the meaning isn't clear. Also, the โœ— tends to draw more attention (when in fact โœ— and โœ“ are equally important, and they're just metadata)

    • "reachable" / "unreachable" is a little bit too technical. Especially since it's about expectations (they're actually all reachable and that's the issue)

    • "alive" isn't much better, not very clear what that would mean.

  • When two instances of the same class exist in a chain, we need to disambiguate
  • Some descriptions of "why we know this is leaking / non leaking" are actually just references to other elements in the chain. So we need to be able to point to other elements.

Latest proposed solution

MainActivity leaked due to one of the fields highlighted with ~~~
โ”ฌ
โ”œโ”€ android.widget.Toast$TN
โ”‚  Leaking: NO (LinearLayoutโ†“ is not leaking)
โ”‚  Leaking: NO (it's a GC root)
โ”‚  โ†“ Toast$TN.mNextView 
โ”‚
โ”œโ”€ android.widget.LinearLayout
โ”‚  Leaking: NO (View#mAttachInfo is not null)
โ”‚  Leaking: NO (MainActivityโ†“ is leaking)
โ”‚  View is attached (View#mAttachInfo is not null)
โ”‚  View id is R.id.container_layout
โ”‚  โ†“ LinearLayout.mContext
โ”‚
โ”œโ”€ com.example.MainActivity
โ”‚  Leaking: NO (Activity#mDestroyed is false)
โ”‚  โ†“ MainActivity.httpRequestHelper
โ”‚                 ~~~~~~~~~~~~~~~~~
โ”‚
โ”‚
โ”œโ”€ com.example.HttpRequestHelper
โ”‚  Leaking: UNKNOWN
โ”‚  โ†“ HttpRequestHelper.button
โ”‚                      ~~~~~~
โ”‚
โ”œโ”€ android.widget.Button
โ”‚  Leaking: YES (View#mAttachInfo is null)
โ”‚  View id is R.id.charge_button
โ”‚  โ†“ Button.mContext
โ”‚
โ•ฐโ†’ com.example.MainActivity
   Leaking: YES (Activity#mDestroyed is true)
   Leaking: YES (RefWatcher was watching this)
help wanted

Most helpful comment

Yes. The plan is that I'll work on this when I have time, and I'll make a release when I'm done working on it :) .

All 9 comments

In com.example.leakcanary:1.0:1.
MainActivity@718013552 is leaking, see โ•ญ?โ”€โ•ฏ for the potential causes:
โ”ฌ
โ•ฐโ†’ Toast$TN (Alive: it's a GC root)
   Toast$TN.mNextView โ”€โ•ฎ
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
โ•ฐโ†’ LinearLayout@1935364696 (Alive: View#mAttachInfo is not null)
   LinearLayout.mContextโ”€โ•ฎ
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
โ•ฐโ†’ MainActivity@718262384 (Alive: Activity#mDestroyed is not true)
   MainActivity.httpRequestHelper โ”€โ•ฎ
โ•ญ?โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
โ•ฐโ†’ HttpRequestHelper@1867963480
   HttpRequestHelper.button โ”€โ•ฎ
โ•ญ?โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
โ•ฐโ†’ Button@718299264 (Not alive: View#mAttachInfo is null)
   Button.mContext โ”€โ•ฎ
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
โ•ฐโ†’ MainActivity@718013552 (Not alive: Activity#mDestroyed is true)

screen shot 2018-10-17 at 6 29 49 pm

Some more iteration: backward and inlining:

In com.example.leakcanary:1.0:1.
MainActivity@718013552 is leaking, see โ•ฐ?โ”€โ•ฎ for the potential causes:
โ•ญโ†’ MainActivity@718013552 not alive: Activity#mDestroyed is true
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ•ญโ†’ Button.mContext โ”€โ•ฏ Button@718299264 not alive: View#mAttachInfo is null
โ•ฐ?โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ•ญโ†’ HttpRequestHelper.button โ”€โ•ฏ HttpRequestHelper@1867963480
โ•ฐ?โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ•ญโ†’ MainActivity.httpRequestHelper โ”€โ•ฏ MainActivity@718262384 alive: Activity#mDestroyed is not true
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ•ญโ†’ LinearLayout.mContextโ”€โ•ฏ LinearLayout@1935364696 alive: View#mAttachInfo is not null
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ•ญโ†’Toast$TN.mNextView โ”€โ•ฏ Toast$TN alive: it's a GC root
โ”ด

screen shot 2018-10-17 at 7 14 56 pm

In com.example.leakcanary:1.0:1.
MainActivity@718013552 is leaking, see โ•ฐ?โ”€โ•ฏ for the potential causes:
                                  โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ•ญโ†’ MainActivity                   โ”‚ MainActivity@718013552, not alive: Activity#mDestroyed is true โ”‚
โ”‚                                 โ”‚                                                                โ”‚
โ”œโ”€ Button.mContext                โ”‚ Button@718013552, not alive: View#mAttachInfo is null          โ”‚
โ”Š                                 โ”‚                                                                โ”‚
โ”œโ”€ HttpRequestHelper.button       โ”‚ HttpRequestHelper@1867963480                                   โ”‚
โ”Š                    โ•ฐ?โ”€โ”€โ”€โ•ฏ       โ”‚                                                                โ”‚
โ”œโ”€ MainActivity.httpRequestHelper โ”‚ MainActivity@718262384, alive: Activity#mDestroyed is not true โ”‚
โ”‚               โ•ฐ?โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚                                                                โ”‚
โ”œโ”€ LinearLayout.mContext          โ”‚ LinearLayout@1935364696, alive: View#mAttachInfo is not null   โ”‚
โ”‚                                 โ”‚                                                                โ”‚
โ”œโ”€ Toast$TN.mNextView             โ”‚ Toast$TN, alive: LinearLayout@1935364696 is alive              โ”‚
โ”ด                                 โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

screen shot 2018-10-18 at 6 36 16 am


In com.example.leakcanary:1.0:1.
@6 MainActivity is leaking, see โ•ญ?โ”€โ•ฎ for the potential causes:

โ•ญโ†’ @6 MainActivity                              
โ”‚                                 
โ”œโ”€ @5 Button.mContext                
โ”Š                       โ•ญ?โ”€โ”€โ”€โ•ฎ    
โ”œโ”€ @4 HttpRequestHelper.button                                              
โ”Š                  โ•ญ?โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ                                       
โ”œโ”€ @3 MainActivity.httpRequestHelper
โ”‚
โ”œโ”€ @2 LinearLayout.mContext    
โ”‚                                           
โ”œโ”€ @1 Toast$TN.mNextView          
โ”ด                                 

โ•ญโ†’ @6 MainActivity leaking: Activity#mDestroyed is true
โ”œโ”€ @5 Button leaking: View#mAttachInfo is null
โ”œโ”€ @4 HttpRequestHelper
โ”œโ”€ @3 MainActivity alive: Activity#mDestroyed is not true
โ”œโ”€ @2 LinearLayout alive: View#mAttachInfo is not null
โ”œโ”€ @1 Toast$TN alive: @2 LinearLayout is alive
โ”ด  

screen shot 2018-10-18 at 7 13 23 am

In com.example.leakcanary:1.0:1.
MainActivityยน is leaking, see โ•ญ?โ”€โ•ฎ for the potential causes:
โ•ญโ†’ MainActivityยน
โ”‚    MainActivity leaking: Activity#mDestroyed is true
โ”œโ”€ Button.mContext
โ”Š    Button leaking: View#mAttachInfo is null
โ”Š                    โ•ญ?โ”€โ”€โ”€โ•ฎ
โ”œโ”€ HttpRequestHelper.button
โ”Š               โ•ญ?โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”œโ”€ MainActivityยฒ.httpRequestHelper
โ”‚    MainActivity not leaking: Activity#mDestroyed is not true
โ”œโ”€ LinearLayoutยน.mContext
โ”‚    LinearLayout not leaking: View#mAttachInfo is not null
โ”œโ”€ Toast$TN.mNextView
โ”‚    Toast$TN not leaking: LinearLayoutยน is not leaking
โ”ด

screen shot 2018-10-18 at 9 10 53 am

In com.example.leakcanary:1.0:1.
MainActivityยน is leaking, see ? for the potential causes:
โ•ญโ†’ MainActivityยน
โ”‚    MainActivity leaking: Activity#mDestroyed is true
โ”œโ”€ Button.mContext
โ”Š    Button leaking: View#mAttachInfo is null
โ”Š?                   โ•ญโ”€โ”€โ”€โ”€โ•ฎ
โ”œโ”€ HttpRequestHelper.button
โ”Š?               โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”œโ”€ MainActivityยฒ.httpRequestHelper
โ”‚    MainActivity not leaking: Activity#mDestroyed is not true
โ”œโ”€ LinearLayoutยน.mContext
โ”‚    LinearLayout not leaking: View#mAttachInfo is not null
โ”œโ”€ Toast$TN.mNextView
โ”‚    Toast$TN not leaking: LinearLayoutยน is not leaking
โ”ด

screen shot 2018-10-18 at 9 13 46 am

@pyricau thanks for the change! This is super helpful for troubleshooting memory leaks, do you have a plan for a new release with this feature?

Yes. The plan is that I'll work on this when I have time, and I'll make a release when I'm done working on it :) .

This shipped in 2.0 alpha 1

Was this page helpful?
0 / 5 - 0 ratings

Related issues

devism picture devism  ยท  6Comments

SUPERCILEX picture SUPERCILEX  ยท  4Comments

matejdro picture matejdro  ยท  6Comments

msfjarvis picture msfjarvis  ยท  6Comments

edgelv34 picture edgelv34  ยท  7Comments