Updating every canvas asynchronously or optimizing the speed of Update() further?

Hello All!

So I have recently started learning how to work with ROOT in C++ for a project of mine. I have been using ROOT to plot data in real time from a simulation that is running in the background. One of the key parts of the simulation is that each loop must run within 16ms. Now when plotting with ROOT the longest part of updating the points is when calling for a canvas the Update() function. Now with one canvas this isn’t really an issue because it’s below the 16ms at around 10ms but when you start to compound a lot of the different canvas’s together each at about 10ms it means that updating all the figures ends up being in my case well above 200ms.

So I spent some time looking around and I couldn’t find anything on google or on these forums that was related to my issue so I figured I would make a post asking about it. Is there anyway to batch update the plots while in standalone mode? Or make Update() much faster?

I have tried several things and the closest idea was to create a thread for every canvas->Update(); but what I ran into is that while yes each canvas gets updated apparently, behind the scenes, there is a global pointer that can cause issues when trying to update every canvas at the same time as I was getting a Warning in <TROOT::Append>: Replacing existing TH1: Graph (Potential memory leak). error and each canvas were alternating between plotting.

Any ideas would be appreciated!

I have attached the important parts of my code below.

// Common Includes
#include "../includes/main.hpp"
#include "../includes/common.hpp"

// CTRL-C signal handler
static void sig_handler(int sig_number)
{
    printf("\n\nCTRL-C pressed!\nShutting down the Simulator!\n\n");
}

// Main Function
int main(int argc, char **argv)
{
    // Send the message that the simulator is starting
    std::cout << "Starting the Simulator...\n\nPress CTRL-C to quit...\n\n" << std::endl;

    // Initialize the ROOT plotting application
	// TApplication rootapp("SIM_CORE", &argc, argv);
    int itmp = (argc > 1 ? 1 : argc); 
    TApplication *app = new TApplication("SIM_CORE", &itmp, argv);

    // This activates implicit multi-threading for ROOT
    ROOT::EnableImplicitMT();
    ROOT::EnableThreadSafety();

    // Create the plot object
    SIM_PLOTTING *test_plots = new SIM_PLOTTING(150, false);

    // Create the first plot window
    int figure_one = test_plots->figure("Aircraft Linear Position", "Aircraft Linear Position", 1024, 768, 3);

    // Create the first plot, 0 on first figure, 0
    test_plots->create_graph(figure_one, 0);
    test_plots->set_labels(figure_one, 0, "x [ft]", "time [s]", "Linear Position, x [ft]");
    test_plots->set_graph_line(figure_one, 0, 632, 1, 1);
    test_plots->set_graph_marker(figure_one, 0, 632, 3, 1);

    // Create the second plot, 1 on first figure, 0
    test_plots->create_graph(figure_one, 1);
    test_plots->set_labels(figure_one, 1, "y [ft]", "time [s]", "Linear Position, y [ft]");
    test_plots->set_graph_line(figure_one, 1, 800, 1, 1);
    test_plots->set_graph_marker(figure_one, 1, 800, 3, 1);

    // Create the third plot, 2 on first figure, 0
    test_plots->create_graph(figure_one, 2);
    test_plots->set_labels(figure_one, 2, "z [ft]", "time [s]", "Linear Position, z [ft]");
    test_plots->set_graph_line(figure_one, 2, 860, 1, 1);
    test_plots->set_graph_marker(figure_one, 2, 860, 3, 1);

    // Display the figure
    test_plots->update_figure(figure_one);

    // Create the second graph, 1
    test_plots->figure("Aircraft Angular Position", "Aircraft Angular Position", 700, 500, 1);
    test_plots->create_graph(1, 0);
    test_plots->set_labels(1, 0, "p [deg/s]", "time [s]", "Angular Rate, p [deg/s]");
    test_plots->update_figure(1);

    int i = 0;
    while(true)
    {
        test_plots->update_plot(figure_one, 0, i, rand() % 100);
        test_plots->update_plot(figure_one, 1, i, rand() % 100);
        test_plots->update_plot(figure_one, 2, i, rand() % 100);

        test_plots->update_plot(1, 0, i, rand() % 100);

        // test_plots->update_figure(figure_one);
        // test_plots->update_figure(1);
        test_plots->update_figures();

        // Update i
        i++;
    }
    
    // Wait for Ctrl-C, the signal handler must be defined in order to properly shut down the sim...
    signal(SIGINT, sig_handler);
    pause();
}
/**
*   @file sim_plotting.cpp
*   @brief The main source file for the function definition of the plotting functions / classes
*/

#include "../../includes/core/sim_plotting.hpp"

/*****************************************************************************/
/*                             CONSTRUCTORS                                  */
/*****************************************************************************/

SIM_PLOTTING::SIM_PLOTTING()
{
    /* DO NOTHING - DEFAULT CONSTRUCTOR */
}

SIM_PLOTTING::SIM_PLOTTING(ssize_t num_points, bool save_flag)
{
    // Set the variables
    /*
    this->num_canvas = number_canvas;
    this->x_size = x_size;
    this->y_size = y_size;
    */
    this->number_points = num_points;
    this->save_flag = save_flag;

    /*
    // Create the canvas pointers if specified, can also not create the pointers and then create the canvas later
    if (this->num_canvas > 0)
    {
        this->create_canvas_pointers();
    }*/
}

/*****************************************************************************/
/*                              DESTRUCTORS                                  */
/*****************************************************************************/

SIM_PLOTTING::~SIM_PLOTTING()
{
    /* DO NOTHING - DEFAULT DESTRUCTOR */
}

/*****************************************************************************/
/*                          PUBLIC FUNCTIONS                                 */
/*****************************************************************************/

int SIM_PLOTTING::figure(char* plot_name, char* title, double x_size, double y_size, int num_plots)
{
    // Get the length of the canvas vector and convert to string
    std::string canvas_name = "C" + std::to_string((this->canvas_pointers).size() + 1);

    // Create the canvas
    TCanvas* single_canvas_pointer = new TCanvas(canvas_name.c_str(), plot_name, x_size, y_size);

    // Enable the grid on the canvas
    single_canvas_pointer->SetGrid();

    //  Divide the canvas vertically if num of plots is greater than 1
    if (num_plots > 1)
    {
        single_canvas_pointer->Divide(1, num_plots);
    }

    // Push back the pointer to the canvas
    (this->canvas_pointers).push_back(single_canvas_pointer);

    // Increment the number of canvas
    this->num_canvas++;

    // Create a vector to push back for graph pointers in this canvas
    std::vector<TGraphErrors*> canvas_graph_vector;

    // Push back a vector for the canvas graphs
    (this->graph_pointers).push_back(canvas_graph_vector);

    // Return the index of the canvas
    return this->num_canvas - 1;
}

void SIM_PLOTTING::create_graph(int canvas_index, int subgraph_index)
{
    // Create a vector of zeros to init plot with zeros
    std::vector<double> x_init_points;
    x_init_points.resize(this->number_points);
    std::fill(x_init_points.begin(), x_init_points.end(), 0.0);

    std::vector<double> y_init_points;
    y_init_points.resize(this->number_points);
    std::fill(y_init_points.begin(), y_init_points.end(), 0.0);

    // Create the graph
    TGraphErrors *single_graph_pointer = new TGraphErrors(this->number_points, &x_init_points[0], &y_init_points[0]);

    // Push back the pointer to the graph
    (this->graph_pointers[canvas_index]).push_back(single_graph_pointer);

    // Register Graph
    (this->canvas_pointers[canvas_index])->cd(subgraph_index + 1); // Set current canvas

	(this->canvas_pointers[canvas_index])->Pad()->SetLeftMargin(0.1);
	(this->canvas_pointers[canvas_index])->Pad()->SetRightMargin(0.05);
	(this->graph_pointers[canvas_index].back())->Draw("APL");
}

void SIM_PLOTTING::set_multigraph(int canvas_index, int multi_graph_index, ssize_t graph_index_start, ssize_t graph_index_end)
{
    if (multi_graph_index == 0)
    {
        // Create the multigraph
        TMultiGraph* single_multigraph_pointer = new TMultiGraph();

        // Create the multigraph vector
        std::vector<TMultiGraph*> single_multigraph_vector;

        // Push back the created multigraph pointer
        single_multigraph_vector.push_back(single_multigraph_pointer);

        // Push back the pointer vector for a new multigraph
        (this->multigraph_pointers).push_back(single_multigraph_vector);
    }
    else
    {
        // Create the multigraph
        TMultiGraph* single_multigraph_pointer = new TMultiGraph();

        // Push back the pointer vector for another multigraph in the same index
        (this->multigraph_pointers[canvas_index]).push_back(single_multigraph_pointer);
    }

    // Set the graphs in this multi-graph
    for (ssize_t i = graph_index_start; i < graph_index_end; i++)
    {
        // Add each graph specified to this multi-graph
        (this->multigraph_pointers[canvas_index][multi_graph_index])->Add(this->graph_pointers[canvas_index][i]);
    }
}

int SIM_PLOTTING::get_num_graphs_on_canvas(int canvas_index)
{
    return (this->graph_pointers[canvas_index]).size();
}

void SIM_PLOTTING::set_labels(int canvas_index, int graph_index, char* title, char* x_label, char* y_label)
{
    (this->graph_pointers[canvas_index][graph_index])->SetTitle(title);
	(this->graph_pointers[canvas_index][graph_index])->GetXaxis()->SetTitle(x_label);
	(this->graph_pointers[canvas_index][graph_index])->GetYaxis()->SetTitle(y_label);
}

void SIM_PLOTTING::set_graph_line(int canvas_index, int graph_index, int color, int style, int width)
{
    (this->graph_pointers[canvas_index][graph_index])->SetLineColor(color);
    (this->graph_pointers[canvas_index][graph_index])->SetLineStyle(style);
    (this->graph_pointers[canvas_index][graph_index])->SetLineWidth(width);
}

void SIM_PLOTTING::set_graph_marker(int canvas_index, int graph_index, int color, int style, int size)
{
    (this->graph_pointers[canvas_index][graph_index])->SetMarkerColor(color);
    (this->graph_pointers[canvas_index][graph_index])->SetMarkerStyle(style);
    (this->graph_pointers[canvas_index][graph_index])->SetMarkerSize(size);
}

void SIM_PLOTTING::update_plot(int canvas_index, int graph_index, double x, double y)
{
    // [DEPRECATED] Move every point down one
    // KEPT THIS DEPRECRATED IN FOR FUTURE REFERENCE
    /*for (ssize_t i = 0; i < this->number_points; i++)
    {
        // Get the index + 1 data point
        double x_1 = 0.0;
        double y_1 = 0.0;
        (this->graph_pointers[canvas_index][graph_index])->GetPoint(i + 1, x_1, y_1);

        // Set the current index point data based on the index + 1
        (this->graph_pointers[canvas_index][graph_index])->SetPoint(i, x_1, y_1);
    }*/

    // Remove the first index, to then append the last index to keep at number_points size
    // THIS METHOD REPLACED THE ABOVE [DEPRECATED] method
    (this->graph_pointers[canvas_index][graph_index])->RemovePoint(0);

    // Append the last index according to the new data point
    (this->graph_pointers[canvas_index][graph_index])->SetPoint(this->number_points - 1, x, y);
}

void SIM_PLOTTING::update_figures()
{
    std::vector<std::thread> threads;
    
    for (int i = 0; i < this->canvas_pointers.size(); i++) 
    {
        threads.push_back(std::thread(&SIM_PLOTTING::update_figure_thread, this, this->canvas_pointers[i]));
    }
    
    for (auto &th : threads) 
    {
        th.join();
    }

    // Process the system events
    gSystem->ProcessEvents();
}

void SIM_PLOTTING::update_figure_thread(TCanvas* canvas_pointer)
{
    canvas_pointer->cd();
    canvas_pointer->Modified();
    canvas_pointer->Update();
    // canvas_pointer->Pad()->Draw("APL");
}

void SIM_PLOTTING::update_figure(int canvas_index)
{
    // Update the Figure
    (this->canvas_pointers[canvas_index])->cd();
	(this->canvas_pointers[canvas_index])->Update();
    (this->canvas_pointers[canvas_index])->Pad()->Draw("APL");

    // Process the system events
    gSystem->ProcessEvents();
}
/* sim_plotting.hpp */
/* This file is used to declare all the plotting functions / classes for the simulation */

#ifndef SIM_PLOTTING_HPP
#define SIM_PLOTTING_HPP

// Includes
#include "../common.hpp"

class SIM_PLOTTING
{
    public:

    /************************/
    /*     CONSTRUCTORS     */
    /************************/

    // Default Constructor
    SIM_PLOTTING();

    // Realtime (Dynamic) Plotting Constructor
    // SIM_PLOTTING(ssize_t number_canvas, ssize_t number_points, double x_size, double y_size, bool save_flag);
    SIM_PLOTTING(ssize_t num_points, bool save_flag);

    // Static Plotting Constructor
    SIM_PLOTTING(char* plot_name, bool save_flag);


    /************************/
    /*      DESTRUCTORS     */
    /************************/

    ~SIM_PLOTTING();                        // Default Destructor


    /************************/
    /*    PUBLIC FUNCTIONS  */
    /************************/


    int figure(char* plot_name, char* title, double x_size, double y_size, int num_plots);

    void create_graph(int canvas_index, int subgraph_index);
    void set_multigraph(int canvas_index, int multi_graph_index, ssize_t graph_index_start, ssize_t graph_index_end);
    int get_num_graphs_on_canvas(int canvas_index);

    // Plot Editing Functions
    void set_labels(int canvas_index, int graph_index, char* title, char* x_label, char* y_label);
    void set_graph_line(int canvas_index, int graph_index, int color, int style, int width);
    void set_graph_marker(int canvas_index, int graph_index, int color, int style, int size);

    // Updating functions
    void update_plot(int canvas_index, int graph_index, double x, double y);
    void update_figures();
    void update_figure_thread(TCanvas* canvas_pointer);
    void update_figure(int canvas_index);


    /************************/
    /*    PUBLIC VARIABLES  */
    /************************/

    private:

    /************************/
    /*   PRIVATE FUNCTIONS  */
    /************************/

    void create_canvas_pointers();

    /************************/
    /*   PRIVATE VARIABLES  */
    /************************/

    ssize_t num_canvas {0};                             // The number of canvases to make (how many different windows essentially)
    ssize_t number_points {0};                          // The number of points to display at one time in the graph
    double x_size {0.0};                                // The x size of the canvas in pixels
    double y_size {0.0};                                // The y size of the canvas in pixels

    bool separate_canvas {false};                       // Seperate the Canvas
    bool save_flag {false};                             // Whether or not to save the canvas


    std::vector<TCanvas*> canvas_pointers;                          // A vector to store all the canvas pointers

    // Multi-layer vectors, first layer is the layer is canvas(window) index, second layer are the graphs for the first layer
    std::vector<std::vector<TMultiGraph*>> multigraph_pointers;     // A vector to store all the multigraph pointers
    std::vector<std::vector<TGraphErrors*>> graph_pointers;         // A vector to store all the graph pointers

    protected:
};
#endif

ROOT Version: 6.26/10
Platform: Ubuntu Server 20.04.1 LTS x86_64
Kernel: Linux 6.0.5-rt14 SMP PREEMPT_RT
Compiler: g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0


Welcome to the ROOT Forum.

The error message you get suggests you are recreating an object (graph or histogram) without having deleted it. It is not obvious just looking at your code where that can be. Have you identified, when running your code, which line produces this warning?

Hello Couet!

Apologies for the delay in responding but I have indeed identified the line in which the error gets generated.

Essentially, how the code works is that I create two canvases. The first canvas has 3 graphs subdivided into their respective plots on the first canvas and the second canvas just has one graph on it. I keep track of the pointers for everything by sticking everything into their respective vectors.

Then once the canvases are all initialized I go into the while loop that will update the plots (randomly for now while I adjust my class) and then update the canvases in the update_figures() function. Now the issue happens in here.

What update_figures() does is that it creates a thread for each canvas that it needs to update and then proceeds to do the canvas_pointer->Update() in each thread for each canvas. I have checked that each thread is fed a unique pointer that corresponds to its respective canvas and whenever I call that canvas_pointer->Update() is when I get that issue. So the issue as far as I can tell isn’t in my code but in the ROOT library unless I am doing something wrong.

My suspicions is that there is some global pointer somewhere in the ROOT library that if I try to update the canvases, asynchronously at the same time, is locked on another canvas.

Drawing and updating a canvas may take several hundred milliseconds. That’s normal on multitasking / multithreading / X11 systems.

Hi Wile!

Thank you for answering! I understand that which is why my question, along the lines of optimizing the speed of Update(), was more to see if I was missing something. The question that Couet is helping me answer is why when I put the Update() for each canvas in its own thread I get the error from above when executing the threads simultaneously going back to my suspicions of a global pointer somewhere.

Cheers,
japopich

Can you list what is inside your canvas canvas_pointer ?

So the canvas pointer is just the argument coming into the function with the type TCanvas*. The pointer that gets fed into the canvas is from:

// Create the canvas
TCanvas* single_canvas_pointer = new TCanvas(canvas_name.c_str(), plot_name, x_size, y_size);

Which is inside my create_figure() function that then feeds into a vector to store all my canvas pointers. The canvas name and plot names are unique and I have checked that every canvas pointer that is fed into the update_figure_thread() function is a unique canvas pointer that corresponds to the canvas it is trying to update.

This message comes from here:

Somehow you code tries to replace a TH1 named Graph.

I guess the magic “Graph” TH1 is automatically created when some “graph” is drawn. So, it seems that, for some reason, its reference gets added to the list of ROOT in-memory objects for the current file or directory (by default, this should not happen).

From diving through the documentation and based off of Couets response, from what I could find, “Graph” is the default name given to a graph if no name is given. None of the constructors for TGraph or TGraphErrors have an argument to set a name and therefore unless you specify a name with the SetName() function then each graph is given the name “Graph”. So I went ahead and set the name of each graph so that now they are all truly unique. The error however still shows up but now using the name of the last graph I happened to declare and define which in my case happens to be “G3”.

Warning in <TROOT::Append>: Replacing existing TH1: G3 (Potential memory leak)

What I am very curious about is why is this an issue when trying to update all the canvases at the same time in different threads? But when I update each canvas one after another this isn’t an issue?

Moreover, inside the update_figure_thread() function, which is the function that gets called in each thread with the respective unique canvas pointers, it should have the same content as the update_figure() function but if I uncomment the canvas_pointer->Pad()->Draw("APL") I get a seg fault. There has to be something I am missing?

As a side note, in my code above in main.cpp, you can see where I commented out updating each canvas (figure) sequentially and instead used the function that creates the threads for updating each canvas simultaneously (update_figures()).

yes, you should use TGraph::SetName to name a graph. But in your case, it seems Graph is a TH1 …

Correct me if i’m wrong but aren’t all graphs histograms (TH1)? From my understanding of the library and going through the TGraph.cxx the TH1 class is used to actually draw things and create the axis as the TGraph class is just a simple wrapper?

An underlying TH1 is created at Painting time. But a TGraph is not a TH1. TGraph is not a wrapper. See:

root [0] auto g = new TGraph()
root [1] g->AddPoint(1,2)
root [2] g->AddPoint(2,3)
root [3] g->Draw("al")
root [4] c1->ls()
Canvas Name=c1 Title=c1 Option=
 TCanvas fXlowNDC=0 fYlowNDC=0 fWNDC=1 fHNDC=1 Name= c1 Title= c1 Option=
  OBJ: TList	TList	Doubly linked list : 0
   TFrame  X1= 0.900000 Y1=1.900000 X2=2.100000 Y2=3.100000
   OBJ: TGraph		 : 0 at: 0x600001d9e890
root [5] 

Ahh I see, so is what I am seeing a bug with the library if the TH1 is not supposed to get added to the list of ROOT in-memory objects per Wile? Also this is only an issue whenever I try to update the canvas all together in their own threads. Why doesn’t this happen whenever I update the canvas one after another sequentially?

Apologies for my late response its been a hectic week.
japopich

g->GetHistogram()->Print("base"); // note the "Name"

From:

ROOT Manual → Basics → Multi-threading

ROOT::EnableImplicitMT

ROOT::EnableThreadSafety

It seems that:

(…) drawing or printing different canvases (and analogous operations …) from different threads is not thread-safe.

I guess some graphics and/or gui experts would need to provide help here.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.