📜 ⬆️ ⬇️

Trying to manage freeing memory in javascript



There are thousands of ways to allocate memory in JavaScript, but the developers of the language have deprived us of the right to free it. This is done by the garbage collector (GC), which also has no control functions. In most cases, it does its job well, but when large amounts of data are continuously released in the program, on the order of a megabyte per second, the garbage collector can be blunt, which causes the browser process to grow to insane sizes. In this article, I will show a couple of dirty tricks with which you can speed up the release of memory.

READ MORE ABOUT THE PROBLEM


As an example, there will be an extension for Chrome and Firefox , which shows video — live broadcasts — by continuously downloading from the network, processing and freeing up arrays of binary data of several megabytes in size. Take a look at the memory consumption (working set) of the browser process in which the extension is running. Green color - Chrome 57, red - Firefox 52. The graphic itself was kindly provided by the perfmon2.msc perfmon2.msc .



')
If in Firefox the garbage collector does a pretty good job, then in Chrome it obviously takes time off from work and begs for dismissal. It's funny that a year ago the picture was the opposite! Browsers, and the algorithms of the garbage collector in particular, are constantly changing, and not always for the better. And what do we need to rewrite the code after the release of the new version of the browser?

It may be objected that nowadays half a gigabyte is a seed, even in smartphones there is more memory. First, I prefer that the free memory (if any) is used to store not obviously unnecessary garbage, but useful things, such as the cache of the operating system. Secondly, most browsers are still 32-bit, which means their address space is noticeably less than 4 gigabytes. A few running copies of the extension pretty quickly exhaust it and lead either to a “crash” of the process, or to problems with video playback.

SEARCH DECISION


Data is stored in an ArrayBuffer . This object was specially created for storing and working with large amounts of binary data. However, it does not have a function that frees the memory allocated for the buffer, or at least changing the size of the buffer. In 2014, Mozilla offered to add the ArrayBuffer.transfer() method, which, among other things, made it possible to free up memory, leaving the object in a detached-state. Despite the simple implementation of the function, the developers of other browsers refused to add it. Happiness was so close ...

ArrayBuffer.transfer() was proposed primarily for use in conjunction with asm.js. I checked how things are going with memory management in the current version of the child asm.js, WebAssembly . No way, memory management only in the plans.

As I said above, after freeing the memory allocated for the object, this object is transferred to the detached state. How does it look in practice? Cishnyk probably immediately thought that it is replaced by null. No, the replacement is a “dummy object”, whose byteLength property is 0, and an attempt to access the contents of the buffer throws (in Firefox) a TypeError: attempting to access detached ArrayBuffer exception TypeError: attempting to access detached ArrayBuffer . Such pacifiers take up little memory, so the garbage collector does a good job with their disposal.

All modern browsers have a postMessage() function that is able to translate an ArrayBuffer into a detached state. True, it does not release the buffer, but passes it to another context (for example, iframe or workflow), so additional actions are needed to free the memory. Next, I will show two tricks that call postMessage() in different ways.

TRICK WITH MESSAGECHANNEL


MessageChannel designed to transfer data between contexts. It has two ports: we send data to one, we receive from the other. An interesting feature is the ability to close the receiving port. What happens in this case with the data sent? There are two options:


The first option definitely suits us. True, the word “transfer” can mean not “release immediately,” but only “mark as useless.” In the latter case, the release will occur during the next cycle of work of the garbage collector, which is not known when it will begin.

In practice, we have confusion and vacillation. Chrome 55 and Firefox 50 free memory acceleration. In Firefox 51+, the memory is immediately released. In Chrome 56, this trick cannot be applied because the data is stuck in the channel.

Here is the trick source code:

 // HACK Firefox 49:   ,    asm.js //   ,     out of memory. const _ = (function() { let _ = null; function () { if (typeof  !== 'object' ||  === null) { return; } if (.buffer) {  = .buffer; } if (.byteLength) { console.log(`[]  ${.byteLength} `); if (!_) { _ = new MessageChannel(); _.port2.close(); } //  transferable   disentangled . _.port1.postMessage(, []); } } return {}; })(); 

And its use:

 //  . //   ,   XMLHttpRequest, fetch  .. var  = new ArrayBuffer(1e6); //       ... //    . //          . _.();  = null; 

We look, as the trick affects work of expansion. Compare with the red graph at the beginning of the article.



Maximum memory consumption dropped by 100 MB. A good addition to the pension. Plus we have a guarantee that the memory consumption will not be uncontrolledly growing, for example, due to an increase in the video bitrate or the frequency of downloading files.

I don’t like this trick due to the compatibility issues described above. However, for some time it was used in the extension.

TRUNK WITH OPERATIONAL FLOW


Worker is a JavaScript code that runs in parallel with the page's JavaScript code (main thread). Buffers in the workflow move the Worker.postMessage() method. However, moving alone is not enough. Buffers will roll in the workflow and wait for the garbage collector to reach their hands. Most likely it will only get worse, because according to my observations, the garbage collector in the workflow is more lazy than on the page.

To get profit, you need to complete the execution of the workflow . During this procedure, the browser will quickly free up all the memory allocated to the stream. I do not know if it is spelled out in the standard. I did not test the performance of the trick in relatively old versions of Chrome, but I don’t expect any unpleasant surprises from them.

Processor time is spent on creating and completing a workflow, so in order to optimize, you need to terminate the stream only after it has accumulated a lot of data, in my case about 10 megabytes.

Trick source code:

 // HACK Firefox 49:   ,    asm.js //   ,     out of memory. const _ = (function() { const _ = 10e6; let _ = ''; let _ = null; let _ = 0; function () { if (typeof  !== 'object' ||  === null) { return; } if (.buffer) {  = .buffer; } if (.byteLength) { console.log(`[]  ${.byteLength} `); if (!_) { if (!_) { _ = URL.createObjectURL(new Blob( [` 'use strict'; self.onmessage = function() { if (!.data) { self.close(); } }; `], {type: 'application/javascript'} )); } _ = new Worker(_); } _ += .byteLength; _.postMessage(, []); if (_ > _) { (); } } } function () { if (_) { console.log(`[]  ${_} `); // terminate()  ,     //    . _.postMessage(null); _ = null; _ = 0; } } return {, }; })(); 

The () function can be called to empty the trash can after it has been used. In an extension, the function is called after the translation is completed.

Let's see the result after applying the trick:



The maximum memory consumption in Firefox dropped to 70 MB, and in Chrome - to 310 MB. No comments.

Update: this trick in Firefox causes a leak of virtual address space.

SPEED


Measuring the time of such rapid processes is not an easy task. The capabilities of the JavaScript profiler are not enough due to its low accuracy and smearing of the code under test in different contexts, some of which are quickly destroyed. First of all, I was interested in the question: by how many percent will the extension work time after adding code to it to free up memory.

Testing was conducted as follows. The processor turned off power saving (C-states and lowering the frequency of Intel). An extension was launched in a minimized window. Most of the time, the processor was idle because the video card was decoding the video. After 40 minutes in Process Explorer , the process in which the extension operates, checked the number of processor cycles spent (CPU cycles).

For both tricks, the number of cycles has changed within the measurement error, so I don’t worry about speed. In the synthetic test in Firefox, the trick with MessageChannel was several times slower than the trick with Worker . First of all, speed depends on the implementation in the browser of data transfer between contexts within one process. By the way, in Chrome, the performance of MessageChannel was recently raised .

CONCLUSION


As you can see, the described tricks are useful, but under quite specific conditions. Most people who work with JavaScript, fortunately, they will never come in handy.

And for those who are interested in the problem, I will give one more piece of advice: try as little as possible to free the “thick” buffers. For example, in the extension, I do not throw away the used buffer, but put it on the "balcony". If you need to allocate memory for data, the balcony is first searched around, and the buffer found there is used, if possible, even if its size is larger than the required one. In my case, the balcony reduced memory consumption by almost half without the use of the above tricks.

Concerning Cyrillic in source code
  • Russian is my native language.
  • I do not like foreign languages ​​(and also casserole).
  • The code was written for myself, for the money I will write even Swahili.
  • I do not impose anything on anyone.
  • Decent people do not argue about tastes.
  • Waiting for the original sparkling jokes about 1C.

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


All Articles