📜 ⬆️ ⬇️

Diagnosing and fixing memory leaks in TypeScript applications

Introduction


Recently, we have completed a large project with a rather complicated advanced UI. Without going into details, let's say that inside the browser something like desktop was implemented with windows, floors and everything that was supposed to. Of course, problems with memory leaks did not bypass us. Frankly, for the time being focused on getting a business result. When the hands reached the memory leaks, it was found that the browser windows occupy gigabytes of RAM. We have classified errors and, in general, have developed an approach to eliminate them. We want to share this approach with you.

Much has already been written on the topic of memory leaks in client applications. Initially, the main problem consisted of browsers IE8 and lower versions (see, for example:
http://habrahabr.ru/post/141451/
http://habrahabr.ru/post/146784/
https://learn.javascript.ru/memory-leaks ).
But now, when it can be said that IE8 is in the past, problems remain. Even the use of such a language as TypeScript does not guarantee their absence. And given the fact that the front-end in web-based applications is becoming increasingly difficult, the urgency of the problem is only increasing.


Causes of errors


The main sources of leaks that we identified for ourselves were:

Looking ahead, we say that the last item caused the most problems. Some of which, unfortunately, cannot be solved at all. However, the negative effects here can be minimized.
')

Cleanup Handler Usage Template


We have identified the cleaning operations in a special module:
import ed = require('disposing'); 

This module contains the ed.Disposables interface, which is essentially a tree of deletion event handlers associated with the displayed view. It is assumed that these handlers will be registered when an object is created, i.e. in the class constructor.
See example:
 class MapControl { constructor( … private beDisposed : ed.Disposables //        … ) { ... beDisposed(beDisposed => { … //  } ); } 


When a part of the view is not needed, for example, when closing a model window, or leaving the page, a part of the tree should be deleted:
 ulo.enableWindowUnloadTracking(window, function () { ed.disposeAll(beDisposed, 'window unloading'); ey.nullify(window, 1, ec.alwaysTrue); }); 

Let's look at eliminating errors with this approach.

1. jQuery widgets


Although in modern life there is a certain tendency to abandon the jQuery library, in many cases one cannot do without it. A huge role is played by the millionth army of widgets, many of which implement very important and useful features.
A typical method of working with a widget is to “wrap” it into a wrapper object, for example:
 export function toCheckbox( $element: JQuery, options: CheckboxOptions ) : JQuery { return $element.jqxCheckBox(options); } 

The problem is that after the widget has been constructed, a reference to the jQuery selector ($ element) is saved in the wrapper object. Such a link can generate a circular dependency between the object and the corresponding DOM element. Therefore, such a link should be “cleaned up” when a memory cleaning mechanism is triggered. In our case, a special function nullify is used for this, which is called for the jQuery selector and the “nullification” of the reference to the jQuery selector:
  //  -c jQuery- export function toCheckbox( beDisposed: ed.Disposables, $element: JQuery, options: CheckboxOptions ) : JQuery { ed.append(beDisposed, function disposeJqxCheckbox() { if ($element != null) { let instance = $element.jqxCheckBox('getInstance'); //   jQuery- $element.jqxCheckBox('destroy'); // ''  jQuery- ed.nullify(instance, 1); // ''  $element = null; options = null; beDisposed = null; } }); return $element.jqxCheckBox(options); } 

Thus, we eliminate all opportunities for the widget, and its DOM representations to remain "not cleansed".

2. Custom Knockout-bindings


In general, wherever DOM elements appear, it is necessary to closely monitor leaks. Since the main purpose of Konckout is to interconnect the model and the view, it is important to carefully manage the DOM elements.

In the following example, the object will “hang” in memory, because binds its event handling to the DOM element:
 init: function (element: HTMLElement, …) { let beDisposed = xko.toBeDisposed(element); $(document).on('keypress', 'input,textarea,select', function (e) { $(element).on('keypress', 'input, textarea, select', function (e) { ... }); }); } 

Therefore, you need to untie them accordingly:
 init: function (element: HTMLElement, …) { let beDisposed = xko.toBeDisposed(element); $(document).on('keypress', 'input,textarea,select', function (e) { $(element).on('keypress', 'input, textarea, select', function (e) { ... }); }); ed.appendUntied(beDisposed, () => { $(element).off('keypress'); $(document).off('keypress'); }); } 


3. Implementation of publish / subscribe architecture


The publish / subscribe architecture is a typical technique for reducing connectivity. In our project, such a technique was implemented in the form of signals (Signals). Problems here can be caused by the absence of a reply from existing signal handlers after the object has already been deleted. If the code contains a subscription to an event, then there must be an unsubscribe from it. The lack of a formal reply is fraught with large leaks, especially if the callback function contains references to objects with a short lifespan. We illustrate this with an example of using a signal followed by a reply to the click event of the specified html element:
 class Button implements ucb.Component { //  ,             public justClicked = es.toActSignal(); private noMoreOpt : () => void = null; constructor( //        private beDisposed: ed.Disposables, private stopPropogation: boolean ) { … } attach(element: HTMLButtonElement): void { //         click html-,        this.noMoreOpt = ue.listenToUntil( this.beDisposed, element, 'click', (event: MouseEvent) => { if (this.stopPropogation) { ue.stopEventPropagation(event, 'Event blocked by a button component.'); } //   this.justClicked(); } ); } detach(): void { //         if (this.noMoreOpt != null) { this.noMoreOpt(); this.noMoreOpt = null; } } } 

In addition, note that when subscribing to an event, you also need to pass a link to the handler tree:
 submit.justClicked.watchUntil(beDisposed, () => { //   }); 


4. Promise and race conditions (race condition in client code)


It is not uncommon for javascript applications to make it necessary to use asynchronous operations, for example, accessing a server. To solve such problems, as a rule, they use so-called Promises. These can be either jQuery-promises or promises from the popular Q library. Regardless of which library is used, the ways of working with such objects are similar. Using promises in itself is not difficult, but, depending on the scenario being implemented, side effects may occur, which can only be noticed with the help of debugging and analysis tools.
Consider the case of so-called races or race conditions, using the following code as an example:
 //   then some.willGetLegendImage(this.beDisposed).then((image) => { model.setLegendImage(image); }); 

The problem here is that the calling code ignored the promise object returned by then (...) and, as a result, has no idea when the transferred function will end. Execution of the function as if drops out of the main thread, and the objects used remain locked in memory until the callback. In addition, it is possible that the function will work after deleting the original object, and we will get an exception of the form undefined.

To get a more secure code, you need to return promise for later use:
 //     promise,     then.  promise   promise,   willGetLegendImage. var promise = some.willGetLegendImage(this.beDisposed).then((image) => { //   model  callback-     model.setLegendImage(image); }); 

Thus, you should always track the status of the original promises and always return the result of the then () call inherited from the original promises. These objects can also be deleted using the delete event handler mechanism.

5. D3 library


To implement the requirements in our project, it became necessary to display graphs and charts that are difficult to implement with only html elements at our disposal. Inspired by the capabilities of the D3 library, it was decided to use it for use in conjunction with SVG elements. Despite the fact that the library's API is relatively simple, accompanied by well-developed documentation, and there are a lot of workable examples, in the context of our application, a set of nuances has arisen that I would like to mention.

5.1. D3 Updated Selection


Using D3 to display such things as graphics, charts, and other related elements (for example, the chart axis) turned out to be quite easy. And due to the presence of its own data binding mechanism, the code for updating the views looked readable and concise. However, typical techniques for using D3 had to be diluted with functionality for cleaning the removed elements.

It is logical to assume that when updating data you can get a subset of svg-elements that can not be compared with the updated data, in other words, they become unnecessary. Often, when constructing a representation, the elements of which it consists, can be used by other components and such elements definitely need to be somehow cleaned before being removed. After binding to the data, we get the object D3 UpdatedSelection. Such an object has an exit () method that allows access to a subset of the elements to be deleted. The method returns an array of elements, iterating over which in the loop, you can execute the cleanup code. If there is no such functionality and somewhere there are links to a previously generated element, then later you can find DOM elements (so called detached DOM elements) that are hanging in memory.
For example, we have an idea from markers - points on the map, where each point is defined by its coordinates:
 //     var markers = this.layer.selectAll('svg.marker') .data(dataItems, d => d.itemId); //       markers.enter() .append('svg') .classed('marker', true) .each(function(data) { transform.call(this, data, paddingLeft, paddingTop); }); .each(function(data: DataItem) { //   renderMarker.call(this, data, that.renderOptions); }) //        SelectManager,        markers.each(function(data) { var element = d3.select(this).node(); var markerElement = $(element).find('circle, polygon').get(0); //      select.attach(markerElement, dragObjectFrom); }) //       DOM-      SelectManager markers.exit() .each(function() { var element = d3.select(this).node(); var markerElement = $(element).find('circle, polygon').get(0); //   select.detach(markerElement); }) .remove(); // , ,   

Note also that such dangling elements, it makes sense to delete immediately in the process of redrawing, and not when deleting the subtree of representations.

5.2. D3 Events


D3 provides its own abstraction for working with events. The need to use events via D3 may arise, for example, when data is bound, or when using d3.behaviors (drag, move, zoom, and the like).
If it is assumed that the view should handle events, then code should be present that executes the existing event handlers from the events. Removal of the handler is done simply: we specify the string constant as the name of the event and pass the null handler function instead:
 //     d3 UpdatedSelection selection.datum(null) //     click, dblclick  mousedown: .on('click', null) .on('dbclick', null) .on('mousedown', null) //   ,   d3 drag behavior: .on('drag', null) .on('dragstart', null) .on('dragend', null) .on('zoom', null) 

This code, of course, can and should be placed inside an object deletion handler.

6. Google Map


6.1. Specificity of the life cycle of the Map object


With all the advantages of this component, it is necessary to take into account the peculiarities of the life cycle of the map object: once you create an instance of the Map object, you cannot delete it. The Google Map API does not provide a destructor function for this object. This aspect is discussed in more detail here: https://code.google.com/p/gmaps-api-issues/issues/detail?id=3803 .
It is necessary from the very beginning to take into account this nuance in the architecture of the application, in order to avoid unwanted memory leaks.

Therefore, in order to minimize memory leaks, the developer is simply obliged to take care of the careful use of such a specific object.

Depending on the specifics of the application, it may be recommended to reuse the once created instance of the map, if it is one. Or to have a pool of map objects, if you need to simultaneously work with several maps.

6.2. Work with map events and map objects



 import ed = require('disposing'); //       disposing,     ed … class MapControl { private dispatch: { … }; //   (d3) constructor( //        private beDisposed : ed.Disposables, … ) { ... //    ed.append(beDisposed, () => { //      this.mapEventsListeners.forEach(listener => google.maps.event.removeListener(listener)); //     () google.maps.event.clearInstanceListeners(this.map); this.map.unbindAll(); ea.use(this.markers, (marker: google.maps.Marker) => { marker.setMap(null); marker = null; }); this.markers = null; //   DOM-,   () var element = this.map.getDiv(); ea.use( ud.toNodeArray(element.getElementsByTagName('img')), (image: HTMLImageElement) => { image.src = ''; } ); ko.cleanNode(element, 'map-control'); // ''     this.map = null; }); } } 

As can be seen from the above example, we first unsubscribe from the events, and then clear the map objects.

Instead of conclusion


In conclusion, we want to say a few words about the toolkit that was used to diagnose leaks.


PS We showed what we faced and how we solved these problems. If you have a similar experience, we will be happy to find out about it, since a general approach to the problem, in our opinion, is still being developed.

Source: https://habr.com/ru/post/274185/


All Articles