Memory Management
Visualize is a retained-mode visualization component, and system memory is allocated in order to store the contents of the scene graph and support the renderer. Since your application makes Visualize API calls to populate the scene graph, your application code is effectively responsible for the amount of memory that Visualize uses, similar to other non-Visualize data structures which are allocated by your application. While Visualize does monitor video memory usage, it does not monitor available system memory or make any attempts to limit system memory usage. It will attempt to allocate any system memory that is required as a result of your application’s Visualize API calls. Note that file load operations also consist of Visualize API calls which populate the scene graph.
Your application should ideally contain its own memory management logic, which could include one or more of the following items:
Tracking memory usage for all of its data structures and subcomponents, including Visualize
Providing end users with recommendations about the size or number of data files that can be loaded and interacted with simultaneously
Indicating to the end user the amount of available system memory
Visualize Memory Manager
The dynamic allocation and releasing of memory in Visualize is controlled by an internal memory manager. The memory manager may allocate memory in numerous situations, such as when objects are inserted into the database, an update is performed, or files are loaded. As items are removed from the database, the memory manager takes care of the freeing of the memory associated with them. Visualize allocates memory using its own memory pool and attempts to reuse memory chunks after entities are deleted.
Tracking and management of Visualize memory usage should be handled in your application logic, and is facilitated by calling HPS::Database::ShowMemoryUsage
which lets you query the amount of memory that Visualize currently has allocated, along with how much is currently being used.
Relinquishing Memory
If you find that there is a significant gap between the amount of memory that Visualize has allocated and the amount in use, and you wish to reclaim some of that memory, you can consider asking the system to relinquish memory by calling HPS::Database::RelinquishMemory
. This forces unused memory to be released back to the system, essentially overriding the normal function of the Visualize memory manager. This function should only be used in very specific situations, and also has major downsides on 32-bit systems. See the Reference Manual entry for more detailed information.
Handling Out-Of-Memory Conditions
Visualize provides an optional mechanism which allows it to inform you about its memory usage, and help avoid a case where it attempts to utilize more system memory than is currently available. Such a condition is important to plan for, as system instability can result if unhandled.
The mechanism involves the use of an HPS::EmergencyHandler
that you create. Visualize will call your custom handler’s HPS::EmergencyHandler::Handle
method if any emergency conditions are detected. Handle is called with a message and an error code. The values in HPS::Emergency::Code
describe the the various types of conditions that can cause an emergency.
NOTE: Emergency memory usage situations must be handled synchronously, and therefore the asynchronous event handler mechanism and its support for errors and warnings via
HPS::ErrorEvent
cannot be used. Additionally, calling any Visualize API function from yourEmergencyHandler::Handle
function (for any emergency code) may cause a deadlock.
Your emergency handler must derive from HPS::EmergencyHandler
. Here’s an example of a custom handler:
class MyEmergencyHandler: public HPS::EmergencyHandler {
public:
MyEmergencyHandler(): notif(nullptr) {}
~MyEmergencyHandler()
{
if (cancel_thread)
cancel_thread->join();
}
void Handle(char const*, Emergency::Code code) override
{
if (code == HPS::Emergency::Code::SoftMemoryLimit) {
mutex.lock();
if (!cancel_thread) {
cancel_thread = std::unique_ptr<std::thread>(new std::thread([this]() -> void {
notif->Cancel();
}));
}
mutex.unlock();
}
else if (code == HPS::Emergency::Code::HardMemoryLimit)
delete reserve_buffer; // free previously reserved emergency buffer
else if (code == HPS::Emergency::Code::Fatal)
abort(); // this case must not return
}
void SetNotifier(HPS::Stream::ImportNotifier* in_notif)
{
mutex.lock();
notif = in_notif;
mutex.unlock();
}
private:
std::unique_ptr<std::thread> cancel_thread;
std::mutex mutex;
HPS::Stream::ImportNotifier* notif;
};
class MyEmergencyHandler : HPS.EmergencyHandler
{
public MyEmergencyHandler(){ notif = null;}
public override void Handle(string message, HPS.Emergency.Code code)
{
if (code == HPS.Emergency.Code.SoftMemoryLimit)
{
lock (lockObj)
{
Action a = () =>
{
if (notif != null)
notif.Cancel();
};
new Task(a).Start();
}
}
else if (code == HPS.Emergency.Code.HardMemoryLimit)
delete_reserve_buffer(); // free previously reserved emergency buffer
else if (code == HPS.Emergency.Code.Fatal)
abort(); // this case must not return
}
public void SetNotifier(HPS.Stream.ImportNotifier in_notif)
{
lock (lockObj)
{
notif = in_notif;
}
}
private void abort() { /* Exit app */ }
private void delete_reserve_buffer() { /* free reserve buffer*/ }
private System.Object lockObj = new System.Object();
private HPS.Stream.ImportNotifier notif;
};
To enable the emergency handler created above, you need to call:
MyEmergencyHandler handler;
HPS::Database::SetEmergencyHandler(handler);
MyEmergencyHandler handler = new MyEmergencyHandler();
HPS.Database.SetEmergencyHandler(handler);
The three emergencies you should handle are described below:
Soft Memory Limit
The soft memory limit is an arbitrary value that you specify. When Visualize allocates more memory than the limit, it will call the HPS::EmergencyHandler
. However, the soft memory limit is just a warning - Visualize will continue to allocate memory - it is up to you to execute the logic (cancel the current operation, warn the user, close any open models) to reduce memory usage. A soft memory limit is set with HPS::Database::SetSoftMemoryLimit
.
In the sample handler above, the code is responding to the HPS::Emergency::Code::SoftMemoryLimit
code by aborting a file load, since continuing with a file load might eventually result in an out-of-memory condition as more memory is allocated. If a current file load was aborted, an application would likely be able to continue safely operating on any current Visualize scene graph. The handler cannot make any Visualize API calls directly or indirectly - the appropriate approach would be to post some sort of a message (or set some flag) which the separate file loading logic would check, and then abort its loading logic. Depending on your application memory management strategy, you may wish to:
abort any new file load
report to your end user that there is a limited amount of memory remaining
recommend to your end user that files or projects should be closed before opening new ones
HPS::Database::SetSoftMemoryLimit(memoryLimit);
ulong memoryLimit = 132581244 / 2;
HPS.Database.SetSoftMemoryLimit(memoryLimit);
Setting the memory limit value to zero will remove the soft memory limit.
Hard Memory Limit
The HPS::Emergency::Code::HardMemoryLimit
code occurs when Visualize attempts to allocate system memory and that allocation has FAILED. This is severe, and it is possible that system instability could occur. In this case, the sample custom handler elects to free a previously reserved emergency buffer. The sample code which reserved the emergency buffer is not shown, but it would consist of allocating a memory block with new
. Freeing a buffer and cancelling the current operation is usually the best method to regain system stability. However, returning to stability cannot be guaranteed. A reasonable amount of memory to reserve in the general case is 128 MB, although this will vary from system to system.
Fatal Errors
If allowed to continue, Visualize will internally attempt to relinquish the unused portion of its allocated memory (do not try to call HPS::Database::RelinquishMemory
yourself) before retrying the memory allocation. If the memory allocation after the relinquish fails, it is a fatal error.
If a fatal error occurs, the application CANNOT successfully return. The best you could do is have the application gracefully exit (and perhaps display a dialog box), which would require performing a longjmp
to a point in the code previously saved by setjmp
.
Handlers for Specific Operations
The above examples set an emergency handler for the general case. You can also set logic to handle a specific operation. For example, if you want to set a unique handler for file loading a particularly large file, you could use the following logic to do that:
HPS::Stream::ImportOptionsKit importKit;
importKit.SetSegment(importSeg);
// create your import notifier
HPS::Stream::ImportNotifier notif;
// add it to your emergency handler
handler.SetNotifier(¬if);
// perform import
HPS::Stream::File::Import(filename, importKit, notif);
notif.Wait();
handler.SetNotifier(nullptr); // set to null so that the handler does not try to use notif when it goes out of scope
HPS.Stream.ImportOptionsKit importKit = new HPS.Stream.ImportOptionsKit();
importKit.SetSegment(importSeg);
// create your import notifier
HPS.Stream.ImportNotifier notif = new HPS.Stream.ImportNotifier();
// add it to your emergency handler
handler.SetNotifier(notif);
// perform import
HPS.Stream.File.Import(filename,importKit, notif);
notif.Wait();
handler.SetNotifier(null); // set to null so that the handler does not try to use notif when it goes out of scope