Comparative Analysis of Leak Canary and Matrix Resource Canary in Goose Farm

Keywords: Python Fragment Mobile Android REST

Recommended reading:

Drip Booster Mobile App Quality Optimizing Framework-Learning Tour I

Android module Api walkthrough

Glide Analysis from Different Perspectives (I)

 

Leak Canary is an open source memory leak detection artifact of Square company based on MAT. Leak Canary automatically displays leak information when a memory leak occurs. It has been updated in several versions and re-implemented in kotlin language. The goose farm APM performance monitoring framework also integrates the memory leak module ResourcePlugin. Here is a comparison between the two.

 

1. Component startup

LeakCanary Auto Registration Start

Principle: A ContentProvider is customized specifically to register and start LeakCanary

The realization is as follows:

/**
 * Content providers are loaded before the application class is created. [LeakSentryInstaller] is
 * used to install [leaksentry.LeakSentry] on application start.
 */
internal class LeakSentryInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
    CanaryLog.logger = DefaultCanaryLog()
    val application = context!!.applicationContext as Application
    InternalLeakSentry.install(application)
    return true
  }
  
  ...
}

 

ResourcePlugin needs to be started manually

public class MatrixApplication extends Application {
    ...
    @Override
    public void onCreate() {
        super.onCreate();
        ...
        ResourcePlugin resPlugin = null;
        if (matrixEnable) {
           resPlugin = new ResourcePlugin(new ResourceConfig.Builder()
                    .dynamicConfig(dynamicConfig)
                    .setDumpHprof(false)
                    .setDetectDebuger(true)     //only set true when in sample, not in your app
                    .build())
            //resource
            builder.plugin(resPlugin );
            ResourcePlugin.activityLeakFixer(this);

           ...
        }

        Matrix.init(builder.build());
        if(resPlugin != null){
            resPlugin.start(); 
        }

    }
  
}

 

 

2. watcher Range and Automatic wacherd Objects

 

LeakCanary RefWatcher can watcher any object (including Activity, Fragment, Fragment.View)

class RefWatcher{
    fun watch(watchedInstance: Any) {...}
    fun watch( watchedInstance: Any,name: String) {...}
}

 

Support for automatic watcher Activity, Fragment, Fragment.View objects

1. Automatic watcher Activity

internal class ActivityDestroyWatcher {
private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
            refWatcher.watch(activity)
        }
      }
    }

  companion object {
    fun install(... ) {
      val activityDestroyWatcher =
        ActivityDestroyWatcher(refWatcher, configProvider) 
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}

Activity Destroy Watcher. install is called indirectly in LeakSentry Installer. onCreate to register Activity Lifecycle Callbacks to monitor the life cycle of Activity, thus realizing automatic watcher Activity objects.

 

2. Automatic watcher Fragment, Fragment.View

//Subclasses have
//SupportFragmentDestroyWatcher
//AndroidOFragmentDestroyWatcher
internal interface FragmentDestroyWatcher {

  fun watchFragments(activity: Activity)

  companion object {
    ...
    fun install(... ) {
    
     ...
      application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
        override fun onActivityCreated( activity: Activity,
savedInstanceState: Bundle? ) {
          for (watcher in fragmentDestroyWatchers) {
            watcher.watchFragments(activity)
          }
        }
      })
    }
 
  }
}

 

Fragment Destroy Watcher. install calls indirectly in LeakSentry Installer. onCreate, registers Activity Lifecycle Callbacks to listen to Activity's life cycle function onCreate, and then registers Fragment Lifecycle Callbacks to listen to Fragment's life cycle function on activity. Fragment Manager to achieve automatic watcher Fragment, Fragment.V. The iew is as follows:

internal class XXXFragmentDestroyWatcher(...) : FragmentDestroyWatcher {

  private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

    override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if (view != null && configProvider().watchFragmentViews) {
         //watcher view
         refWatcher.watch(view)
      }
    }

    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      if (configProvider().watchFragments) {
        //watcher fragment
        refWatcher.watch(fragment)
      }
    }
  }
  

  //AndroidOFragmentDestroyWatcher
  override fun watchFragments(activity: Activity) {
    val fragmentManager = activity.fragmentManager
    fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
  }
  
 //SupportFragmentDestroyWatcher
  override fun watchFragments(activity: Activity) {
    if (activity is FragmentActivity) {
      val supportFragmentManager = activity.supportFragmentManager
      supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
    }
  }
}

 

Replugin has only one Activity RefWatcher and only supports watcher Activity. It also monitors the life cycle of activity by registering Activity Lifecycle Callbacks to implement automatic watcher Activity objects.

 

public class ActivityRefWatcher extends FilePublisher implements Watcher {
     @Override
    public void start() {
        stopDetect();
        final Application app = mResourcePlugin.getApplication();
        if (app != null) {
            app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);
            //Polling detection for spillovers
            scheduleDetectProcedure();
           
        }
    }

private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {

@Override
public void onActivityDestroyed(Activity activity) {
//Processing mDestroyed Activity Infos through polling detection in push mDestroyed Activity Infos collection
pushDestroyedActivityInfo(activity);
synchronized (mDestroyedActivityInfos) {
mDestroyedActivityInfos.notifyAll();
}
}
};
 

 

3. Realization of Detecting Leakage

1. Detecting threads

LeakCanay detection implementation, the old version is a handler Thread polling detection, now changes, first triggered in the main thread, by RefWatcher.watch actively triggered, activity, Fragment, Fragment.view detection, that is, triggered by the life cycle, and then

Real check in non-main threads.

 

Now the passive trigger detection in the main line is based on the following:

class RefWatcher{
   
 fun watch( watchedInstance: Any,name: String) {
    ...
    watchedInstances[key] = reference
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
   }
}


internal object InternalLeakSentry {

  ...
  private val checkRetainedExecutor = Executor {
    mainHandler.postDelayed(it, LeakSentry.config.watchDurationMillis)
  }
  val refWatcher = RefWatcher(
      clock = clock,
      checkRetainedExecutor = checkRetainedExecutor,
      onInstanceRetained = { listener.onReferenceRetained() },
      isEnabled = { LeakSentry.config.enabled }
  )
...
}

 

Called from moveToRetained, and eventually rolled over to HeapDumpTrigger's method scheduleRetained InstanceCheck, then made a real check on the non-mainline, the code is as follows:

 

internal class HeapDumpTrigger() {
 private fun scheduleRetainedInstanceCheck(reason: String) {
    if (checkScheduled) {
      CanaryLog.d("Already scheduled retained check, ignoring ($reason)")
      return
    }
    checkScheduled = true
    //Non-main thread hanlder
    backgroundHandler.post {
      checkScheduled = false
      checkRetainedInstances(reason)
    }
  }
...
}

 

 

ResourcePlugin refers to the old version of LeakCanary and uses thread polling detection based on the following:

 


//ActivityRefWatcher.start
private void scheduleDetectProcedure() {

    //Detecting the polling mScan Destroyed Activities Task execute function always returns RetryableTask.Status.RETRY
  mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
}


class
RetryableTaskExecutor{ private void postToBackgroundWithDelay(final RetryableTask task, final int failedAttempts) { //Non-main thread handler mBackgroundHandler.postDelayed(new Runnable() { @Override public void run() { RetryableTask.Status status = task.execute(); if (status == RetryableTask.Status.RETRY) { postToBackgroundWithDelay(task, failedAttempts + 1); } } }, mDelayMillis); } }

 

2. Realization of leak detection logic

LeakCanay Check Detection

Principle: VM adds recyclable objects to the ReferenceQueue associated with WeakReference

1) According to retained Reference Count > 0, trigger a gc request and retrieve retained Reference Count again

 var retainedReferenceCount = refWatcher.retainedInstanceCount

    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = refWatcher.retainedInstanceCount
    }

 

2) Judge whether the retained Reference Count is larger than the retained Visible Threshold (default is 5), and if it is smaller, skip the next detection.

if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

 

3) Depending on the dumpHeapWhenDebugging switch and whether or not Debugging is debugged, if the configuration switch is on and debugging is ongoing, polling is delayed and debugging is completed.

if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
      showRetainedCountWithDebuggerAttached(retainedReferenceCount)
      scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
      return
    }

 

4) dump Hprof file

 val heapDumpFile = heapDumper.dumpHeap()
 if (heapDumpFile == null) {
    showRetainedCountWithHeapDumpFailed(retainedReferenceCount)
    return
}

 

5) Open Heap Analyzer Service for Hprof analysis

 

In the old version, there may be false positives on individual systems for the following reasons:

  • VM does not provide an API for forcibly triggering GC. GC can only be performed by "recommendation" system through System.gc() or Runtime.getRuntime().gc(), and if the system ignores our GC request, the recoverable object will not be added to Reference Queue.

  • It takes a while to add recyclable objects to Reference Queue. Leak Canary evades this by delaying 100 ms, but it doesn't seem to work absolutely.

  • Monitoring logic is asynchronous, and if an Activity is held by a local variable of a method when judging whether it is recoverable, it will cause misjudgment.

  • Leak Canary repeatedly prompts the activity to leak if it repeatedly enters the leaked Activity.

Now this 2.0-alpha-2 version has not been weighted, of course, this is not good to say, if an Activity has multiple leaks, and the reasons for leaks are different, weighting will lead to missed reports.

 

Resource Plugin Check Detection

Principle: Use WeakReference.get() directly to determine whether the object has been recycled, so as to avoid misjudgement due to delay.

1) Judging whether the current mDestroyed Activity Infos is empty, if it is empty, there is no need to leak, because it is polling, so we should prevent CPU idling and waste electricity.

// If destroyed activity list is empty, just wait to save power.
while (mDestroyedActivityInfos.isEmpty()) {
    synchronized (mDestroyedActivityInfos) {
        try {
               mDestroyedActivityInfos.wait();
        } catch (Throwable ignored) {
           // Ignored.
        }
    }
}

 

2) Depending on the configuration switch and whether it is debugged in Debug, if the configuration switch is on and in debugging, skip this check, wait for the next poll, and the debugging is over.

// Fake leaks will be generated when debugger is attached.
if (Debug.isDebuggerConnected() && !mResourcePlugin.getConfig().getDetectDebugger()) {
        MatrixLog.w(TAG, "debugger is connected, to avoid fake result, detection was delayed.");
        return Status.RETRY;
}

 

3) Add a "Sentinel" object that must be recycled to confirm that the system did GC, and skip this check until the next poll if it did not.

final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
triggerGc();
if (sentinelRef.get() != null) {
   // System ignored our gc request, we will retry later.
   MatrixLog.d(TAG, "system ignore our gc request, wait for next detection.");
   return Status.RETRY;
}

 

4) Record the class name of Activeness that has been judged to be leaking, and avoid repeating the notice that the Activity has been leaked and is valid for one day.

final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
if (isPublished(destroyedActivityInfo.mActivityName)) {
    MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName);
    infoIt.remove();
    continue;
}

As mentioned earlier, weight loss is still flawed. For example, there are many leaks in an Activity, and the reasons for leaks are different. Weight loss can lead to missed reports.

 

5) If it is found that an Activity cannot be reclaimed, the judgement is repeated three times, and it requires that more than two activities be created from the record of the Activity to be considered leaking, in order to prevent the Activity from being held by local variables leading to misjudgement.

++destroyedActivityInfo.mDetectedCount;
long createdActivityCountFromDestroy = mCurrentCreatedActivityCount.get() - destroyedActivityInfo.mLastCreatedActivityCount;
if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
                    || (createdActivityCountFromDestroy < CREATED_ACTIVITY_COUNT_THRESHOLD && !mResourcePlugin.getConfig().getDetectDebugger())) {
    // Although the sentinel tell us the activity should have been recycled,
    // system may still ignore it, so try again until we reach max retry times.
   continue;
}

 

6. Depending on whether mHeap Dumper is set, if it is set, dumpHeap is performed, then Canary Worker Service is opened, shrinkHprof AndReport is performed, otherwise simple onDetectIssue is performed.

if (mHeapDumper != null) {
    final File hprofFile = mHeapDumper.dumpHeap();
    if (hprofFile != null) {
        markPublished(destroyedActivityInfo.mActivityName);
        final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName);
        mHeapDumpHandler.process(heapDump);
        infoIt.remove();
    } else {
         infoIt.remove();
     }
} else {
                   
       markPublished(destroyedActivityInfo.mActivityName);
       if (mResourcePlugin != null) {
             ...           
             mResourcePlugin.onDetectIssue(new Issue(resultJson));
                  
       }
}

 

 

4. Hprof tailoring and analysis (not detailed for the time being)

LeakCanary does not shrink the Hprof file, parses it with haha, analyses the GC Root reference chain of the leaked object, and puts the detection and Analysis on the client side.

 

ResourcePlugin only detects and shrink s Hprof files. It does not support Hprof files on the client side. It needs to use the jar provided by ResourcePlugin to analyze Hprof separately. In the process of analysis, it can also find out the GC ROOT chain of redundant Bitmap.

Clipping Hprof file source code see: HprofBufferShrinker().shrink

Redundant Bitmap analyzer: DuplicatedBitmapAnalyzer

Activity leak analyzer: ActivityLeakAnalyzer

 

The size of Hprof file is generally about the size of memory occupied by Dump, and the size of Hprof from Dump is more than 100 M. If you do not do any processing to upload the Hprof file directly to the server, on the one hand, it will consume a lot of bandwidth resources, on the other hand, the server will occupy the storage of the server when it files the Hprof file for a long time. Space.

By analyzing the format of Hprof file, it can be seen that the buffer area of Hprof file stores all the data of objects, including string data, all arrays, etc. But in our analysis process, only part of the string data and the buffer array of Bitmap are needed, and the rest of the buffer data can be eliminated directly. Later Hprof files can usually be more than one-tenth smaller than the original file.

 

Reference chain lookup algorithm in LeakCanary is designed for a single target. When searching for redundant Bitmap in ResourceCanary, multiple results may be found. If the algorithm is called on the Bitmap object in each result, there will be a lot of duplicate visited nodes in the reference graph, which will reduce the number of duplicate visited nodes. The search efficiency is improved. For this reason, we modified LeakCanary's reference chain lookup algorithm to find the shortest reference chain from multiple targets to GC Root at the same time in a single call.

 

 

If you are interested in the blogger's updates, please pay attention to the public number!

 


Posted by glitch003 on Fri, 05 Jul 2019 11:00:36 -0700