background

The project is an internal anomaly monitoring system developed by using vUE framework, which is used to display abnormal information of Java program running, including execution stack, code, variables and other information display.

During the test, colleagues in the department reported that switching between different exception information for several times would cause the web page to crash. When I opened the Task manager of Chrome at the scene of the crime, I saw that the memory usage of this page had reached 9.7G, which initially suspected that there was a leak on the page.

Verify the guess

  1. Open devtool -> Performance to record page performance
  2. Perform operations that switch other abnormal information on the page (the operations on the page that are most likely to cause memory leaks)
  3. View the performance analysis result

You can see the stepwise growth of Nodes, Listeners, and JS Head(memory), where the growth Nodes correspond to each operation. It is obvious that this operation will cause memory to continue to grow, and it is not surprising that memory will eventually overflow.

Problem analysis

Before I did it, I knew:

  1. From the Performace tool you can see the cumulative growth of JS Heap, Nodes, and Listeners
  2. The above three points are actually dependent on each other:
  • Variable reference DOM
  • The child DOM cannot be freed, resulting in the parent DOM not being freed either
  • If the DOM is properly GC, event listeners for the DOM are automatically removed
  1. You can use devtool->memory -> take snapshot to collect memory snapshots and analyze tool documents.

A brief introduction to Snapshot

Well, it’s a bit much, but it’s fairly clear:

  1. By data type, you can see some built-in objects, Vue objects, custom objects (such as Exception, StackFrame, etc.), Detached Element, EventListener, and so on.
  2. The ordinate includes Distance, shallow size, and Retained size, which can be accurately interpreted as:
  • Distance: reference Distance to root
  • Shallow size: The size of the object itself, excluding the size of the data it references
  • Retained size: The size of the object itself and all references, which is the total memory occupied by the objectIf the object it refers to is not referenced by another non-recyclable object. The amount of memory freed by removing the object itself along with related objects that cannot be reached from the GC root, as described on the Google Developer website.
  1. In the Retainers panel below, you can see the specific reference path of the variable, where it was created, and where it was used

Locate the problem: find objects referenced that should have been freed, but didn’t

  1. Perform an operation that causes a memory leak

The core code for this operation looks something like this


Each time setEvent is executed, this. Exception is pointed to a new instance and presented to the page for display, and the object referenced by this. Exception should be released.

  1. Collect new memory snapshot information

  2. Find the differences: Change the view to the differences view

As you can see from the diagram, many new objects appear after Step 1, but the deleted object is 0.

In the case of the Exception object, the new object is created, the old object is released, and the Delta should be zero, following the code logic of Step 1. So it’s clear that this is a problem. There are too many vue components that reference the variable, so it is not easy to locate.

Looking at Listenters, I see something like this:

As expected, this is due to some Nodes not being released. But I really didn’t get much information to analyze.

In addition, view the information about Nodes and search for Detached. You can see objects like Detatched HTMLDivElement that are in memory but not rendered on the page

I found a small detla, node function is also clear (is used for code highlighting elements in the page) Detatched HTMLPreElement for analysis:

Div <= div <= vue component <= var-hover <= events <=… $platform.event…

In this case, $platform.event is the event API provided by platform + module architecture design, which is used for global event communication.

Finally, the above reference relations will be translated: $platform.event (global, bound event functions are not automatically released), Var -hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover () {var-hover ();

Look at the code for var-hover:


Var -hover binds an anonymous function (basically, no untying is written to the event) and uses this, the current vue component, to prevent objects referenced by this component from being GC.

So the culprit is basically found and the next thing to do is fix it -> revalidate it

repair

  1. First simple change: Unbind events in beforeDestroy when verifying that the memory overflow problem is resolved
  2. Manual unbundling is a big hole, and a lot of places people don’t really have the good habit of doing this when they’re writing code. So there you have it: a modification of the platform interface to support automatic component-based unbundling of events. The code is as follows:

This is the actual implementation of $platform.event

The event binding for var-hover is as follows


Removing the beforeDestroy hook also makes the business layer look much better.

validation

  1. If the performance function is used to perform the operations that caused memory overflow, the following results are obtained

Each peak here is the result of memory allocation at the beginning of the operation, and each time after the operation, there is no accumulation of memory and Nodes Listensers

Compare the results of the performance analysis before correcting again


Horrible stairs…

  1. By the way, what has changed in the Memory panel

StackFrameVar and additional EventListeners, observers, etc. to render the StackFrameVar object. This is normal because the data rendered is different between the two StackFrameVar objects

Exception, and many other objects have deltas of 0 (in reverse order of Delta)

Other instructions

The above analysis diagram was analyzed in real time after writing part of the code in the process of writing this article, relatively speaking, it is not as detailed as the actual debugging. Other operations were done during the actual debugging:

  1. Privacy window, disable all extensions (to avoid affecting memory analysis)
  2. Turn off development mode HMR because VUE_HOT_RELOAD also generates a layer of references and I don’t trust it completely
  3. With mock data, every time you perform an operation, you render the same data that can be manually computed (knowing which class will produce how many instances)
  4. Manual GC during Performance

The above approach is to provide a completely clean and controlled analysis environment.