How We Work with Resources in Web Applications

1C Developer team

18.04.2024 18 min

web.png

1C:Enterprise applications can be deployed in a three-tier architecture (Client - Application Server - Database). The client part of the application can run, in particular, as a web client in a browser. The web client is a rather complex JavaScript framework responsible for displaying the user interface and running client code in the 1C:Enterprise language. One of the tasks that we faced during the development of the web client was the correct handling of various kinds of resources (primarily their timely release).

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

ArchitectureEn.png

Here is the list of the resources that need to be controlled and optimized.

●       Subscriptions to application events inside 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 respond to changes in the DOM tree and element sizes. However, their correct use requires efforts to release resources after observation is complete.

●       WebSocket

●       And just subscriptions to browser events

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

●       Filtered data, search

●       Object locks

●       Objects that exist outside of the web page itself, such as 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 because it means we need to be able to run practically any code written by application developers.

CannotStartEditing.png

The editing lock is also a resource stored on the server, and it must be released in a timely manner.

The Key Methods to Manage Resources

There are several fundamental resource management strategies, which we will explore in more detail below:

●       Full manual call of dispose() / unsubscribe() / close()

●       Reactivity (Mobx and similar)

●       Reference 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 a resource's lifetime. In React, resource acquisition is typically placed in componentDidMount/componentWillUnmount, or useEffect() is utilized for functional components.

This approach is great for straightforward tasks like handling window resize events in a component. But for more complex scenarios, it can get tricky. You might find yourself needing to move the resource up the component tree and then figure out a way to pass it back down.

Reactivity

Reactivity provided by libraries such as Mobx or $mol opens up new possibilities and allows you to solve a significant part of the complexities associated with resource management in web applications. In many cases, resources are only needed by the current live component, and their lifetime is effectively managed by the framework or library in use.

By using reactive libraries like Mobx or $mol, developers can offload the burden of resource lifetime management. These libraries provide ways to automatically track dependencies and release resources when they're no longer needed. This frees developers to focus on core application logic without worrying too much about 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 consider an example of using the fromResource helper from the Mobx library. This helper provides a convenient way to create a resource that only exists for as long as it is actively used in the observed context, such as a rendered component.

When the component is rendered and starts using the resource, Mobx automatically takes into account the dependency between the component and the resource. If the component stops using the resource, Mobx releases it, thus managing the lifetime of the resource.

The fromResource function takes three parameters:

●       Resource creator: This function takes an update function as its first parameter. This update function is used to modify the resource's value.

●       Resource releaser: This function is responsible for releasing the resource when it's no longer needed.

●       Initial value: This defines the starting value for the resource.

In the example below, we create an object that returns the current date. This date is automatically by means of setInterval. The interval gets cleared automatically when it is no longer needed.

// Simplified implementation of `now()` from 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()

   );

}


Here is an example of how to use this timer:

@observer

class Timer extends React.Component    {

   private timer = makeTimer();

   public render()    {

       return <span>

           {this.timer.current()}

       </span>;

   }

           }

The workflow can be described as follows:

  1. The component is activated and starts rendering its content. Inside the component's render() method, the resource value is obtained (this.timer.current()), and the timer starts ticking.
  2. Once the component is no longer used and is removed from the tree (for example, it goes out of view), Mobx detects that the resource is no longer needed in the given context since there has been no attempt to get its value.
  3. Mobx automatically releases the resource, and accordingly, the timer stops since there are no longer any active dependencies on this resource.

Reference Counting

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

Advantages:

●       Full control: Developers have full control over resources and their lifetimes, which allows them to precisely determine when resources should be released. Resources are released immediately when the last reference disappears, without having to wait for the garbage collector to collect them, etc.

●       Flexibility: Custom implementation of reference counters and wrappers allows for the creation of more complex and specific resource management scenarios. This lets you handle complex situations, like avoiding unnecessary object copies. For instance, if the operation you're performing removes the last reference to an object, you wouldn't need to create a copy at all.

Disadvantages:

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

●       If custom reference counters are used carelessly, the problem of circular references can occur, which will lead to memory leaks. Careful monitoring and prevention of circular references are crucial.

Garbage Collector

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

●       When a resource is created, it is registered in a global resource registry. This allows you to track 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 stage, resources are marked as unreachable.

●       After the check is started, the application analyzes the paths by which resources can be reached. These paths can be associated with application entry points, such as controllers, components, and other entities.

●       Resources for which no path from the entry points is found are considered unreachable and are removed from the registry. This frees up memory and resources that are no longer used.

This approach effectively eliminates memory leaks and frees up 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 the application entry points to ensure that all necessary resources are reachable and not deleted by mistake.

Let's illustrate this approach with a simple two-page web application: a presentation page (slides) and a user page.

1_en.png

The presentations page contains an array of presentations, including SysDevCon.pptx and SysDevConBackup.pptx. These presentations are the resources that need to be released in a timely manner.

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

Each object that can store a resource utilizes the IResourceHolder interface, which allows you to mark a resource as used. The application consists of two pages and calls the corresponding methods on each page in its markResources() method.

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

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

The collectUnusedResources() method releases unused resources (see an example in the code below). The method starts by marking all resources as unused. Then, the markResources() method is called from all starting points of the application. This method recursively marks all resources that are actually used. Finally, any resources that haven't been marked as used (meaning they're not needed) are deleted.

// Resource state: used or not.

enum ResourceState {

   Unused = 0,

   Used = 1

}

// All objects that require lifetime control must implement this interface.

interface IResource {

   // Method for releasing resources. It is called by the garbage collector when it finds that the resource is unreachable from all entry points of the application

   dispose(): void;

   // Get and set the resource state

   setSate(stateResourceState): void;

   getState(): ResourceState;

}

// And this interface implements entry points in all applications, all containers, all collections, etc.

interface IResourceHolder    {

   markResources(): void;

}

/* Page */

abstract class Page implements IResourceHolder    {

   public abstract markResources(): void;

}

/* Application */

class App implements IResourceHolder    {

   private presentations!Page;

   private users!Page;

   public markResources(): void    {

       // The application is an entry point and owns the presentation and user pages

       this.presentations.markResources();

       this.users.markResources();

   }

}

class PresentationPage extends Page    {

       // The presentation page owns the files, which are the resources

   private filesIResource[] = [];

   public markResources(): void    {

       for (const file of this.files)    {

           files.setState(ResourceState.Used);

       }

   }

}

class ResourceRoot    {

   private allResourcesIResource[] = [];

   private app!App;

       // All resource creation goes through the global registry, or resources must be registered in it

   public createFile(namestring): IResource    {

       const file = new File(name);

       this.allResources.push(file);

       return file;

   }

   public collectUnusedResources(): void

   {

       // Step 1: Mark all resources as unused

       for (const res of this.allResources)    {

           res.setState(ReosurceState.Unused);

       }

       // Step 2: Iterate over all entry points in the application (in this example it is only the application itself) and call the process to mark the resources reachable from the entry points

       this.app.markResources();

       // Step 3: Check if there are resources still not marked as Used. All such resources can be deleted

       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 removed from the array of presentations.

2_En.png

The recursive traversal won't find this resource in use, and the system will automatically dispose of it at step 3 by calling res.dispose().

FinalizationRegistry

FinalizationRegistry is a modern browser API designed to manage the lifetime of objects and resources in JavaScript applications. FinalizationRegistry acts as a registry for objects. It keeps track of them and automatically calls a callback function to release their resources once there are no longer any direct references to those objects.

FinalizationRegistry interacts with WeakRef, which represents a weak reference to an object. Weak references do not hold objects in memory, and if there are no strong references to the object, it is eligible for garbage collection.

This API is currently implemented in most popular browsers.

8.png

Let's consider a service built using the traditional approach where resources are released with explicit calls to dispose(). We'll analyze this service and then demonstrate how to adapt it to leverage the new FinalizationRegistry mechanism.

This service provides methods for subscribing to and unsubscribing from notifications about updates to any of the entities.

abstract class EntityNotifyService {

   public static INSTANCEEntityNotifyService;

   private listenersSet<((eventEvent=> void)> = new Set();

   public subscribeListUpdate(listener: (eventEvent=> void    {

       this.listeners.add(listener);

   }

   public unsubscribeListUpdate(listener: (eventEvent=> void): void    {

       this.listeners.delete(listener);

   }

}

The DynamicList class, which displays lists of entities, utilizes this service. It subscribes to updates in the constructor and unsubscribes in the dispose() method. In this case, you must 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 how the service and the DynamicList object can be used:
In the componentDidmount() method, a DynamicList object is created. In the componentWillUnmount() method, it is necessary to remember to call list.stop(). The render() method displays the data obtained 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>;

   }

}

Functional components work similarly. The useEffect hook creates and manages the list object, and the cleanup function calls stop() to ensure proper cleanup.

useEffect(() => {

   list.current = new DynamicList();

   return () =>    {

       list.current?.stop();

   }

}, []);

 The figure below shows a graph of object references.

3_en.png

The EntityNotifyService stores a reference to the subscriber. The subscriber, through the closure, has a reference to the DynamicList class, which contains a back reference to the subscriber. If you call the dispose method, the link between the service and the subscriber will be broken, as a result of which the DyanamicList object will be disposed of by the garbage collector, since there will be no other references to it.

4_en.png

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

Consider the WeakRef class. It includes two methods: the first is the constructor, which takes an object, and the second is the deref() method, which returns the object itself or undefined if the object has been deleted by the garbage collector.

declare class WeakRef<T extends object> {

   constructor(target?T);

   /** returns the target object, or undefined if the target was collected by the garbage collector*/

   deref(): T | undefined;

}

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

We use WeakRef to store a reference to the subscriber. By creating a weak reference to the subscriber object listener, we allow the garbage collector to delete the listener object if there are no other active references to it. When an event occurs that the listener subscribed to, we simply call the deref() method on the weak reference. 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 INSTANCEEntityNotifyService;

   private listenersSet<((eventEvent=> void)> = new Set();

   public subscribeListUpdate(listener: (eventEvent=> void): void    {

       const weakListener = new WeakRef(listener);

       this.listeners.add((event=>    {

           weakListener.deref()?.(event);

       });      

   }

   public unsubscribeListUpdate(listener: (eventEvent=> void): void    {

       this.listeners.delete(listener);

   }

}


Below is the reference graph for this new version.

5.png

Note that the arrow from WeakRef to the listener is dotted, which means that the reference is weak, and if there are no references left to DynamicList, it can be collected by the garbage collector.

6.png

After that, WeakRef.unref() will start returning undefined, but this raises a problem: the WeakRef itself remains in the subscriber array, and it would be nice to remove it from there.

7.png

The FinalizationRegistry is designed for exactly this purpose. Let's consider its interface:


declare class FinalizationRegistry    {

   constructor(cleanupCallback: (heldValueany=> void);

   /** Registers an object in the registry

   Parameters: target is the target object

              heldValue is the value that will be passed to the finalizer

              unregisterToken is a token that can be used to unregister */

   register(targetobjectheldValueanyunregisterToken?object): void;

   /** Unregisters an object by the passed token

    * Parameters: unregisterToken is the token that was specified during registration

    */

   unregister(unregisterTokenobject): void;

}

A special cleanup function is passed to the FinalizationRegistry constructor, which will be called after the object is collected by the garbage collector. To start tracking the lifetime of an object, you need to call the register() function. This function takes three parameters: the target object, a special value that will be passed to the cleanup function, and a token that can be used to unsubscribe from the lifetime of the object.

Here's how we can use this in our service: we create a FinalizationRegistry that will call the unsubscribe function for the list update event in its cleanup method. In FinalizationRegistry, we keep track of the listener handler, so when it is destroyed (when it is collected by the garbage collector), the cleanup method will be called.
weakWrapper allows you to avoid storing hard 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: (eventEvent=> void): void {

       const weakListener = new WeakRef(listener);

       const weakWrapper = (eventEvent=> {

           weakListener.deref()?.(event);

       }

       // We specify weakWrapper as heldValue, which we will remove from the subscribers list

       this.listenersRegistry.register(listenerweakWrapper);

       this.listeners.add(weakWrapper);

   }

}

We no longer need to manually track the DynamicList object's lifetime. When React removes the component using DynamicList, the garbage collector automatically removes it since there are no references to it. Our FinalizationRegistry detects this and calls the service's unsubscribe function.


@observer

class ListComponent2 extends React.Component {

   private list!DynamicList = new DynamicList();;

   public render() {

       return <span>

           {this.list.getData()}

       </span>;

   }

}

Limitations of FinalizationRegistry

FinalizationRegistry has the following limitations:

●       It only supports objects. It cannot be used to track the removal of non-object data types, such as strings.

●       The value of heldValue cannot be the object itself, as a strong reference is created to heldValue.

●       The unregisterToken must also be an object. If it is not specified, then it will be impossible to unregister.

There are also some peculiarities with the finalization callback:

●       The callback might occur not immediately after garbage collection.

●       The callback might occur not in the order the objects were deleted.

●       There might be no callback at all if

●       The entire page was completely closed.

●       The FinalizationRegistry object itself was deleted.

When using closures, you should be careful since you can create an additional reference to the object through them, which can prevent it from being cleaned up and garbage collected.

It is important to be careful not to "lose" the object. In the following example, a lambda function is passed as a subscriber, but there are no other references to this lambda function. As a result, it will be immediately deleted by the garbage collector (since there are only weak references through WeakRef inside the EntityNotifyService itself), and the DynamicList object will never be notified of any changes.


abstract class DynamicList {

   public constructor() {

       EntityNotifyService.INSTANCE.subscribeListUpdate(() => {

           console.log(‘never called’);

       )};

It's also important to keep in mind that React likes to keep component property values in internal caches and structures. If the object whose lifetime you want to track is used as a React component property, its lifetime may increase in an unpredictable way.

Debugging FinalizationRegistry

A few words about debugging FinalizationRegistry and catching memory leaks in Chrome. This web browser has developer tools that allow you to take a snapshot of the memory heap in a dedicated "Memory" tab.

Capture1.PNG

It shows all objects for which memory is allocated on the web page.

If we suspect that a memory leak occurs during some action, we can perform this action on the page and take a second memory snapshot, and then compare both snapshots by selecting the "Comparison" item in the menu:

9.png

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

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

For each object, you can see the path by which it is accessible. To do this, select a specific object in the upper list, and the object path will be shown in the lower panel. We cannot say it is a perfect tool. It shows a lot of extra and often duplicate information. It can even keep references to objects itself, preventing them from being released. But we haven't found anything better yet. 

Firefox also has similar tools, but they're not as functional or user-friendly.

In conclusion, let us mention that we use a garbage collector to manage resources in the 1C:Enterprise web client. We did not use FinalizationRegistry because it did not exist when the web client was written. With the release of FinalizationRegistry, we considered switching to it, but we have not yet made a final decision.

However, we do use FinalizationRegistry in the development of the 1C:Enterprise.Element technology.



Be the first to know tips & tricks on business application development!

A confirmation e-mail has been sent to the e-mail address you provided .

Click the link in the e-mail to confirm and activate the subscription.