Tag Provider Memory Issue

I am working on a module revolving around a realtime tag provider, and I'm using the 'default' tag provider for the sake of this post.

To test performance, I'm repeating cycles of 1) creating and writing to 4.5 million memory tags (with randomly generated paths and string values) into the tag provider, then 2) removing all the tags recursively, such that when a cycle is complete, the tag provider is empty. After each cycle, I wait for gateway memory use to decrease to below a threshold (e.g., 1024 MB, but it never gets there).

Every time I run a cycle, memory peaks while creating/writing and removing tags, then settles after the cycle completes. However, gateway memory use after every cycle when memory gets to a steady state progressively increases (see image below). After every cycle, I verify that all the tags were removed.

When the module starts, I am 1) subscribing to tag structure change events via the tag manager, 2) subscribing to all tags currently in the 'default' provider, and 3) unsubscribing when the module shuts down. Likewise, I am subscribing to tag structure changes via the tag manager, and 4) subscribing to new tags when they are added, 5) unsubscribing from moved tags and subscribing to them at their new location, and 6) unsubscribing from removed tags. The idea is to react to any and all tag change events in the tag provider.

To help identify where the memory issue is coming from, I'm dumping memory allocation by thread via the code below:

// Get and enable tracking of thread memory allocation
ThreadMXBean threadMXBean = (ThreadMXBean) ManagementFactory.getThreadMXBean();
if (!threadMXBean.isThreadAllocatedMemorySupported()) {
	return;
}
threadMXBean.setThreadAllocatedMemoryEnabled(true);

for (long threadId : threadMXBean.getAllThreadIds()) {
	String threadName = threadMXBean.getThreadInfo(threadId).getThreadName();
	long allocatedBytes = threadMXBean.getThreadAllocatedBytes(threadId);
	if (allocatedBytes <= 0)
	    continue;
	long allocatedMb = allocatedBytes / 1024L / 1024L;
	if (allocatedMb > 100) {
		_logger.info("High Memory-Allocation Thread | Allocated [MB]: " +
			String.format("%,d", allocatedMb) + " | Name: " + threadName +
			" | ID: " + threadId);
	}
}

When looking at the gateway memory trend (image below), these memory use numbers on this are (obviously) wrong (excessively high). However, in terms of relative distribution, they are indicating that the issue may be coming from subscription models and especially the default tag provider.

To be clear, I am being very diligent about subscribing and unsubscribing to/from tags.

As illustrated in the image below, either references aren't being released for garbage collection to pick up, or something else wonky is going on here.

If I don't restart the gateway, memory use will sit at the same level and not decrease. So, as I keep running more and more cycles, memory keeps increasing until I max out the memory and the gateway crashes.

If I restart my module without restarting the gateway, memory use will sit at the same level and not decrease. Same thing: more cycles, memory increases until the gateway crashes.

If I restart the gateway, memory resets to ~100 MB. When I start running through cycles again, it keeps progressively pumping up the memory every cycle and not reducing back down to a reasonable level.

Any pointers on how to better diagnose what is going on here?

I don't see anything in the SDK that would allow me to check or purge subscriptions, only subscribe or unsubscribe. The only indicator is when you try to subscribe to something you're already subscribed to, or unsubscribe from something you're not subscribed to, it spams the log about it.

Are you retrieving the results of the unsub requests? (Or using .whenComplete() to do so?) You probably need to be inspecting for errors on unsub.

It would also help if you put together a small, self-contained example project that can build a module that demonstrates the issue.

Also, you might find the TagActor infrastructure easier to use if you simply want to monitor all tag value traffic.

It looks like a CompletableFuture<Void> return on unsubscribe:

...
_tagManager = _gatewayContext.getTagManager();
...
var completableFutureVoid = _tagManager.unsubscribeAsync(removedTag.getFullPath(), this);
var voidReturn = _tagManager.unsubscribeAsync(removedTag.getFullPath(), this).get();

Same with .whenComplete(). I'm on SDK 8.1.40. Am I missing something?

I have it wrapped in a try to log exceptions, but none are popping up:

...
_tagManager = _gatewayContext.getTagManager();
...
try {
    _tagManager.unsubscribeAsync(removedTag.getFullPath(), this).get();
} catch (Exception e) {
    _logger.warn("Could not unsubscribe from removedTag tag " +
            removedTag.getFullPath().toStringFull() + " changes: " + e.getMessage(), e);
}

I'm not seeing

Has the documentation/examples on the actor infrastructure improved? ~6 months ago it looked really sparse.

No examples. But I puttered my way through it. :man_shrugging:

You register an actor factory and the tag property criteria of interest. It gets called for every possible tag right away, and new ones as created. You can selectively create an actor or not. Any you don't create, you have no further responsibility for. Any you do, will be called when it is time to reconfigure or destroy. That is, you have no tracking to do.

I'm puttering my way through creating a TagActorFactory and where I'm getting stuck is finding a NodeContext to pass into TagActorFactory.create(NodeContext nodeContext, PropertySet propertySet). Any pointers on where to find one, preferably scoped to a specific tag provider?

You don't call that method. Ignition calls it automatically for you, for the tags of interest (types, property combinations) you register with the factory.

Thank you, I think I have the pattern figured out. 'Subscribing' to all changes makes sense. But, what if I want to 'Write' tag values as well, is it beneficial to insert actors at a lower/earlier position and try to write that way? If so, I'm not seeing the pattern for 'writing' data.

Also a side question: I'm doing remote debugging, and it seems like there's a watchdog timer that restarts the gateway if I don't step within a specific period of time when I'm at a breakpoint. I'm having trouble identifying what gateway setting controls this. Is this configurable?

Do you have your debugger configured to suspend all threads instead of just current thread?

I think the only thing that would do is this the Java service wrapper ping / JVM timeout mechanism. Haven’t ever had an issue with it restarting a gateway on me while remote debugging, though.

Thank you, you hit the nail on the head, it was suspending all threads instead of only the current thread that was causing the gateway restart.

In addition to the 'writing' data question, when editing tags in the Designer, I'm noticing that for tags that were existing when the gateway was started, the TagActorFactory is only creating and using TagActors when values are edited via the Tag Editor, and not when a value is entered into the 'Value' column of the Tag Browser. However, once a tag has been edited via the Tag Editor and the TagActorFactory is triggered to create and use a TagActor, then writing a value in the 'Value' column of the Tag Browser will trigger the TagActorFactory and a TagActor will be created.

The behavior that I would expect is for all tags to trigger the TagActorFactory and create a TagActor (if it aligns with the set monitored properties/dependent properties) regardless of where the value is edited in the Designer.

Is there a way to ensure that actors are used regardless of where the values are edited at?

Value is the only thing that doesn't trigger actor creation, as the purpose of tag actors is to handle those value changes.

You must use property conditions to create actors for all relevant tags independent of their value at that time.

I was able to pretty well figure out subscribing to everything via tag actors, thank you for your help.

Now for the memory problem...

Using the TagActorFactory successfully sidestepped the memory issues related to subscriptions. Thank you @pturmel and @Kevin.Herron for your help.