How we work with resources in web applications

Applications created on the platform 1C:Enterprise, can be deployed in a three-tier architecture (Client – ​​Application Server – DBMS). The client part of the application can work, in particular, as a web client in a browser. A web client is a rather complex JavaScript framework responsible for displaying the user interface and executing client code on built-in language. One of the tasks that we faced during the development of the web client was correct work with various types of resources (first of all, their timely release).

We have analyzed existing approaches and want to tell you about it.

Let’s consider what resources need to be carefully controlled and optimized:

  • Subscriptions to application events within the application itself, for example, a subscription to update lists when elements are updated, etc.

  • Various browser APIs

    • MutationObserver and ResizeObserver. These tools provide the ability to observe and react to changes in the DOM tree and element sizes. But their proper use requires efforts to free up resources after monitoring is completed.

    • WebSocket

    • And just subscriptions to browser events

  • Some resources can exist on both the client and server sides. For example, when the server stores some state corresponding to the client:

    • Database selection, search

    • Object locks

  • Objects external to the page, for example, objects from browser extensions. In our case, this is an extension for working with cryptography, which has a native part that runs on the user’s computer

In our case, all this is further complicated by the presence of the built-in 1C:Enterprise language, which leads to the need to execute almost arbitrary code written by application developers.

An edit lock is also a resource stored on the server that must be released in a timely manner.

An edit lock is also a resource stored on the server that must be released in a timely manner.

Basic methods of resource management

There are several fundamental resource management strategies, we will look at them in more detail below:

  • Completely manual call to dispose() / unsubscribe() / close()

  • Using reactivity (Mobx and analogues)

  • Link counter

  • Garbage collector

  • FinalizationRegistry

Manual resource management

This is perhaps the simplest (but sometimes very time-consuming) way to manage resources. It allows you to fully control the resource lifetime. IN ReactTypically the resource grab is placed in componentDidMount/componentWillUnmount or used by useEffect() in the case of functional components.

But this approach only works well for simple things, for example, subscribing to window resizing in some component. If you implement this approach for something more complex, problems may arise: the resource may need to be moved somewhere higher in the hierarchy of components, and then somehow brought down.

Using reactivity

Reactivity provided by libraries such as Mobx or $mol, opens up new perspectives and allows us to resolve much of the complexity associated with resource management in web applications. In many cases, resources are needed solely by the current “live” component, and their lifetime is effectively managed by the framework or library being used.

This allows developers to shift the responsibility of resource lifetime management to these reactive libraries. For example, Mobx, $mol or other libraries provide mechanisms for reactive dependencies and automatic release of resources when the dependency is no longer needed. This way, developers can focus on the application logic without worrying about the details of resource management.

However, even with the use of reactivity, resource control remains relevant in the context of more complex scenarios and interactions with external systems.

Let’s look at an example of using the fromResource helper from the Mobx library. This helper provides a convenient way to create a resource that exists only while it is actively being used in an observable context, such as a rendered component.

When a component renders and starts using a given resource, Mobx automatically takes into account the dependency between the component and the resource. If a component stops using a resource, Mobx releases it, thereby managing the resource’s lifetime.

The fromResource function takes three parameters:

  • A resource creation function that takes as its first parameter a function to update the resource value. This function must be called to change the current value

  • Release function

  • Initial value

The example below creates an object that returns the current date. This date will be automatically updated thanks to setInterval; The interval will be automatically cleared when it is no longer needed.

//упрощенная реализация now() из mobx-utils
function makeTimer(): IResource<number>    {
    let timerId = -1;
    
    return fromResource(
        (udpateTimer) => {
            updateTimer(Date.now());
            timerId = setInterval(() => { updateTimer(Date.now()); }, kUpdateInterval);
        },
        () => { clearInterval(timerId); },
        Date.now()
    );
}

An example of using such a timer:

@observer
class Timer extends React.Component    {
    private timer = makeTimer();

    public render()    {
        return <span>
            {this.timer.current()}
        </span>;
    }
}

The work process can be described as follows:

  1. The component is activated and begins rendering its content. Inside the component’s render() method, the resource value is retrieved (this.timer.current()) and the timer starts ticking.

  2. Once a component is no longer in use and is removed from the tree (for example, it goes out of scope), Mobx detects that the resource is no longer needed in the given context since there was no attempt to retrieve its value.

  3. Mobx automatically releases the resource and accordingly the timer stops as there are no longer active dependencies on that resource.

Link count

Creating your own reference counter and managing resources through custom wrapper objects gives developers greater control over resource lifetimes. This approach has its advantages, but also comes with some disadvantages.

Pros:

  • Full control: Developers have full control over resources and their lifetime, allowing them to determine exactly when resources should be released. Resources are freed immediately when the last link disappears, there is no need to wait for the garbage collector to collect them, etc.

  • Flexibility: Custom implementation of reference counter and wrappers makes it possible to create more complex and specific resource management scenarios. You can implement complex scenarios, for example, not copying an object if the reference to it was last and is lost after the operation is completed.

Minuses:

  • Using wrapper objects requires more routine and additional code. This can increase the amount of work involved in developing and maintaining the application.

  • If you use your own reference counters carelessly, you may experience the problem of circular references, which will lead to memory leaks. This needs to be monitored and circular references prevented.

Garbage collector

The simplest implementation of a garbage collector looks something like this:

  • Each time a resource is created, it is registered in the global resource registry. This allows you to keep track of all created resources in the application.

  • At a certain point in time, the application starts the process of checking the “reachability” of resources. At this point, resources are marked as unreachable.

  • After running the scan, the application analyzes the paths through which resources can be reached. These paths can be associated with entry points into the application, such as controllers, components, and other entities.

  • Resources for which no path from entry points can be found are considered unreachable and are removed from the registry. This frees up memory and resources that are no longer in use.

This approach is effective in eliminating memory leaks and freeing unused resources. It is especially useful in complex applications where many resources are created and destroyed during operation. However, it is important to carefully design application entry points to ensure that all necessary resources are reachable and are not removed by mistake.

Let’s consider this approach using the example of a simple web application of two pages – a presentation page (slides) and a user page.

The presentations page contains a presentation array of two presentations – SysDevCon.pptx and SysDevConBackup.pptx. These presentations are resources that must be released in a timely manner.

Let’s create a special ResourceState enumeration to store the state of the resource (used / unused). Each resource implements the IResource interface and has methods for setting state, getting state, and releasing state.

Every object that can hold a resource implements the IResourceHolder interface, which allows you to mark the resource as in use. The application consists of two pages and in its markResources() method it calls the corresponding methods on each page.

The presentation page calls markResources() on each of its files.

The global resource registry ResourceRoot allows you to create files by calling the createFile method; the file will be added to the global resource list.

The collectUnusedResources() method releases unused resources; its approximate implementation is given in the source code below. First, all resources are marked as unused, after which the markResources() method is called from all entry points into the application. This method must recursively mark all resources as in use, after which all resources that are marked as unused (not marked as in use by a traversal from all entry points into the application) are removed.

// Состояние ресурса - используемый или нет.
enum ResourceState {
    Unused = 0,
    Used = 1
}

// Все все объекты, требующие контроля за временем жизни должны реализовывать этот интерфейс. 
interface IResource {
    // Метод освобождения ресурсов, его вызывает сборщик мусора, когда обнаруживает, что ресурс не достижим из всех точек входа в приложение
    dispose(): void;

    // Получения и установка состояние ресурса
    setSate(state: ResourceState): void;
    getState(): ResourceState;
}


// А этот интерфейс реализуют все точки входа в приложения, все контейнеры, все коллекции, и т.д.
interface IResourceHolder    {
    markResources(): void;
}


/* Страница */
abstract class Page implements IResourceHolder    {
    public abstract markResources(): void;
}

/* Приложение */
class App implements IResourceHolder    {
    private presentations!: Page;
    private users!: Page;

    public markResources(): void    {
        // Приложение является точкой входа и владеет страницам презентаций и пользователей
        this.presentations.markResources();
        this.users.markResources();
    }
}

class PresentationPage extends Page    {
    // Страница презентаций владеет файлами, которые и являются ресурсами
    private files: IResource[] = [];

    public markResources(): void    {
        for (const file of this.files)    {
            files.setState(ResourceState.Used);
        }
    }
}

class ResourceRoot    {
    private allResources: IResource[] = [];
    private app!: App;
    
    // Всё создание ресурсов проходит через глобальный реестр, либо ресурсы должны в нём регистрироваться
    public createFile(name: string): IResource    {
        const file = new File(name);
        this.allResources.push(file);
        return file;
    }

    public collectUnusedResources(): void 
    {
        // Шаг 1: маркируем все ресурсы как неиспользуемые
        for (const res of this.allResources)    {
            res.setState(ReosurceState.Unused);
        }

        // Шаг 2: проходим по всем точкам входа в приложение, в данном примере это только само приложение, и вызываем маркировку ресурсов, которые достижимы из точек входа
        this.app.markResources();

        // Шаг 3: проверяем, какие из ресурсов остались не отмеченными как Используемые. Все такие ресурсы можно удалять
        for (const res of this.allResources)    {
            if (res.getState() != ResourceState.Used)    {
                res.dispose();
            }
        }
    }
}

Let’s see what happens if the second file SysDevConBackup.pptx is deleted from the presentation array.

Recursive reachability traversal will not be able to mark it as “In Use” and in step 3 the system will call res.dispose() on it.

FinalizationRegistry

FinalizationRegistry is a modern browser API designed to manage the lifetime of objects and resources in JavaScript applications. Using FinalizationRegistry, you can register objects for which a callback will be automatically called to finalize resources when there are no longer strong references to them.

FinalizationRegistry interacts with a WeakRef, which is a “weak reference” to an object. Weak references do not hold an object in memory, and if there are no strong references to an object, then it is subject to garbage collection.

Currently this API is implemented in most popular browsers.

Let’s consider its use using the example of a service implemented under the old mechanism with manual calls to dispose to release resources, and using its example, we will move on to using the new FinalizationRegistry mechanism.

This service has methods for subscribing and unsubscribing to update events of any of the entities.

abstract class EntityNotifyService {
    public static INSTANCE: EntityNotifyService;

    private listeners: Set<((event: Event) => void)> = new Set();

    public subscribeListUpdate(listener: (event: Event) => void    {
        this.listeners.add(listener);
    }

    public unsubscribeListUpdate(listener: (event: Event) => void): void    {
        this.listeners.delete(listener);
    }
}

The DynamicList class, which displays lists of entities, uses this service: in the constructor it subscribes to updates, and in the dispose() method it unsubscribes. In this case, you should always call the dispose() method to avoid memory leaks:

class DynamicList {
    public constructor() {
        EntityNotifyService.INSATNCE.subscribeListUpdate(this.listener)
    }

    public dispose {
        EntityNotifyService.INSATNCE.unsubscribeListUpdate(this.listener)
    }

    private listener = () => {
        this.refreshList()
    }
}

This is what using a service and a DynamicList object might look like:

In the componentDidmount() method a DynamicList object is created, in the componentWillUnmount() method you must remember to call list.stop(), the render() method displays the data received from this object.

@observer
class ListComponent extends React.Component    {
    private list!: DynamicList;

    /** @override */
    public componentDidMount() {
        this.list = new DynamicList();
    }

    /** @override */
    public componentWillUnmount() {
        this.list.stop();
    }

    public render() {
        return <span>
            {this.list.getData()}
        </span>;
    }
}

In the case of using functional components, everything is done in approximately the same way, useEffect is used, in which a list is created, the call ends with clearing, where the stop() method is called.

useEffect(() => {
    list.current = new DynamicList();

    return () =>    {
        list.current?.stop();
    }
}, []);

The figure below shows a graph of object references.

The EntityNotifyService service stores a link to the subscriber; the subscriber, through a closure, has a link to the DynamicList class, which contains a back link to the subscriber. If you call the dispose method, the connection between the service and the subscriber will be broken, causing the DyanamicList object to be garbage collected, since there will be no other references to it.

Let’s look at how FinalizationRegistry can simplify this process by eliminating the need to manually call the dispose() method.

Let’s look at the WeakRef class. It includes two methods: the first is a constructor that takes an object, and the second is a deref() method that returns the object itself or undefined in case the object was garbage collected.

declare class WeakRef<T extends object> {
    constructor(target?: T);

    /** возвращает объект target, или undefined, если target был собран сборщиком мусора*/
    deref(): T | undefined;
}

WeakRef does not create hard references to objects, and therefore the target object can be garbage collected if there are no hard references left to it.

We will use WeakRef to store the subscriber reference. By creating a weak reference to the listener subscriber object, we allow the garbage collector to delete the listener object if there are no other active references to it. When the event that the listener subscribed to occurs, we simply call the weak reference’s deref() method. If
the object still exists in memory, deref() will return a reference to it and we can successfully call the handler.

abstract class EntityNotifyService    {
    public static INSTANCE: EntityNotifyService;

    private listeners: Set<((event: Event) => void)> = new Set();

    public subscribeListUpdate(listener: (event: Event) => void): void    {
        const weakListener = new WeakRef(listener);
        this.listeners.add((event) =>    {
            weakListener.deref()?.(event);
        });        
    }

    public unsubscribeListUpdate(listener: (event: Event) => void): void    {
        this.listeners.delete(listener);
    }
}

Below is a graph of links for this new option.

Please note that the arrow from WeakRef to Listener is dotted, this means that the reference is weak, and if there are no references left to the DynamicList, it can be garbage collected:

After this, WeakRef.unref() will begin to return udefined, but a problem arises: the WeakRef itself remains in the subscribers array, and it would be nice to remove it from there.

FinalizationRegistry serves precisely for these purposes. Let’s look at its interface:

declare class FinalizationRegistry    {
    constructor(cleanupCallback: (heldValue: any) => void);
    
    /** Регистрирует объект в регистре
    Параметры: target – целевой объект
               heldValue – значение, которое будет передано в финализатор
               unregisterToken – токен, с помощью которого можно отменить регистрацию */
    register(target: object, heldValue: any, unregisterToken?: object): void;

    /** Разрегистрирует объект по переданному токену
     *  Параметры: unregisterToken – токен, который был указан при регистрации
     */
    unregister(unregisterToken: object): void;
}

The FinalizationRegistry constructor passes a special cleanup function that will be called after the object is garbage collected. In order for the system to start tracking the lifetime of an object, you need to call the register() function, where three parameters are passed: the target object, a special value that will be passed to the cleanup function, and a token with which you can unsubscribe from the lifetime of this object.

Here’s how we can use this in our service: We create a FinalizationRegistry that, in its cleanup method, will trigger the unsubscribe from the list update event. In the FinalizationRegistry, we keep track of the listener handler so that when it is destroyed (when the garbage collector collects it), the cleanup method is called.

weakWrapper allows you to avoid hard storing references to the listener so that the listener object can be destroyed and collected by the garbage collector.

abstract class EntityNotifyService    {
    
    public listenersRegistry = new FinalizationRegistry((listeners) => {
        this.unsubscribeLstUpdate(listener);
    });

    public subscribeListUpdate(listener: (event: Event) => void): void {
        const weakListener = new WeakRef(listener);
        const weakWrapper = (event: Event) => {
            weakListener.deref()?.(event);
        }
        // В качестве heldValue указываем weakWrapper, который мы и будем удалять из списка подписчиков
        this.listenersRegistry.register(listener, weakWrapper);
        this.listeners.add(weakWrapper);
    }
}

As a result, there is no longer any need to monitor the lifetime of the DynamicList object. Once React deletes the component object that used the DynamicList, the garbage collector will be able to collect it because there are no more links to it. Our FinalizationRegistry will know about this and call the service’s unsubscribe function.

@observer
class ListComponent2 extends React.Component {
    private list!: DynamicList = new DynamicList();;

    public render() {
        return <span>
            {this.list.getData()}
        </span>;
    }
}

FinalizationRegistry limitations

FinalizationRegistry has limitations:

  • FinalizationRegistry only supports objects. It cannot be used to track the deletion of non-object data types such as strings.

  • The value of heldValue cannot be the same as the object itself because a hard link is created to heldValue.

  • unregisterToken must also be an object. If you do not specify it, it will be impossible to cancel the registration.

There are also specifics to calling the finalization callback:

  • The callback may not be called immediately after garbage collection.

  • The callback may not be called in the order in which the objects were deleted.

  • The callback may not be called at all:

    • For example, if the entire page was completely closed.

    • Or if the FinalizationRegistry object itself was deleted.

You should be careful when using closures because they can create an additional reference to an object that can interfere with cleanup and garbage collection.

It is important to be careful not to “lose” the object. The following example passes a lambda function as the subscriber, but there are no other references to the lambda function. As a result, it will be immediately garbage collected (since within the EntityNotifyService itself there are only weak references via WeakRef), and the DynamicList object will never be notified of any changes.

abstract class DynamicList {
    public constructor() {
        EntityNotifyService.INSTANCE.subscribeListUpdate(() => {
            console.log(‘never called’);
        )};
    }
}

Another thing to keep in mind is that React likes to store component property values ​​in internal caches and structures. If the object whose lifetime you want to monitor is used as properties of a React component, its lifetime may increase in an unpredictable way.

Debugging FinalizationRegistry

A few words about debugging FinalizationRegistry and catching memory leaks in Chrome. Chrome has developer tools that have a separate Memory tab that allows you to take a snapshot of the memory heap.

It will show all the objects for which memory is allocated in the web page.

If we suspect that some action is leaking memory, we can perform this action on the page and take a second memory snapshot, and then compare both snapshots by selecting “Compare” in the menu:

The comparison will show all created and deleted objects and the amount of memory allocated to them.

There is also a special mode that allows you to see all the objects for which memory was allocated after the first snapshot until the moment of the second snapshot.

For each object, you can see the path by which it is accessible. To do this, you need to select a specific object in the top list and the path of the object will be shown in the bottom panel. We cannot say that this is an ideal tool, it shows a lot of “extra” and often “duplicate” information, sometimes it itself stores references to objects, preventing them from being released, but we have not yet been able to find anything better. If you know something more convenient, write in the comments!

The FireFox browser also has similar tools, however, they are much less functional and convenient.

In conclusion, let’s say that in the 1C:Enterprise 8 web client we use a garbage collector to manage resources. We did not use FinalizationRegistry because… At the time of writing, the FinalizationRegistry web client does not yet exist. With the advent of FinalizationRegistry, we thought about switching to it, but have not yet made a final decision.

When developing the technology 1C:Enterprise.Element we use FinalizationRegistry.

That’s all for today, see you again on our blog!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *