Cloning vs lazy_static with Arc/Mutex for efficiency

I have the following structs:

// All the trading algorithms rely on candlestick charts.
#[derive(Clone, Debug)]
#[pyclass]
pub struct CandleStick {
    #[pyo3(get, set)]
    pub timestamp: u64,
    #[pyo3(get, set)]
    pub open: f64,
    #[pyo3(get, set)]
    pub close: f64,
    #[pyo3(get, set)]
    pub high: f64,
    #[pyo3(get, set)]
    pub low: f64,
}

// A TradeAlgorithm has a Python-script containing the algorithm we want to test.
// Each algorithm gets a specific amount of funds assigned to play with.
#[derive(Clone)]
pub struct TradeAlgorithm {
    pub description: String,
    pub id: String,
    pub start_funds: f64,
    pub interval: String,
    pub run_every_sec: i32,
    pub data: std::vec::Vec<CandleStick>,
}

My app has different tradealgorithms with code in Python which can be executed. The tradealgorithms take Candlestick-charts from exchange websocket APIs as input

When the algorithm is started I each time a new candlestick from the websocket is received we execute the Python code:

        // Create seperate thread. In this thread the algorithm is executed each time
        // data is received from the receiver.
        let counter_clone_for_async = Arc::clone(&counter);
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|py| {
            // Keep history of incoming data so the algorithm has access to it.
            //let mut data : std::vec::Vec<CandleStick> = std::vec::Vec::new();
                let mut self_clone = self.clone();
            tokio::spawn(async move {
                
                let api_clone = api.clone();
                let ws_send_clone = ws_send.clone();
                while let Some(n) = datastream.lock().await.recv().await {    
                    self_clone.data.push(n);
                    match self_clone.clone().execute(psql.clone(),api_clone.clone(), ws_send_clone.clone(), counter_clone_for_async.clone()).await {
                        Ok(_) => (),
                        Err(_) => (),
                    }
                }
            })
        })

Python code executed like this:

        let data = self.clone().data;
        // Call Python function func.
        let result = Python::with_gil(|py| {
            let py_candlesticks: Vec<Py<CandleStick>> = data.into_iter().map(|candlestick| {
                Py::new(py, candlestick.clone()).unwrap()
            }).collect();
            let py_list = PyList::new(py, py_candlesticks);

            
            let fun: Py<PyAny> = match PyModule::from_code(
                py,
                &*python_code,
                "",
                "",
            ) {
                Ok(f) => {
                    match f.getattr("func") {
                        Ok(a) => a.into(),
                        Err(e) => {
                            return Err(tradealgorithm::Error::PythonCodeError(format!("Error with Python gil {}", e)));
                        }
                    }
                },
                Err(e) => {
                    return Err(tradealgorithm::Error::PythonCodeError(format!("Error with Python gil {}", e)));
                }
            };

            let f = match fun.call1(py, (py_list,)) {
                Ok(f) => f,
                Err(e) => {
                    return Err(tradealgorithm::Error::PythonCodeError(format!("Error with Python gil {}", e)));
                }
            };
            let result = match f.extract::<f64>(py) {
                Ok(r) => r,
                Err(e) => {
                    return Err(tradealgorithm::Error::PythonCodeError(format!("Error with Python gil {}", e)));
                }
            };
            

            Ok(result)
        });

The problem is that I can't pass the data as reference because PyO3 needs ownership of data. It would be possible to pass as reference if I could make PyO3 objects in the part where the data is appended to the array like here, but that is not possible in multi-threaded environments.

Currently I have solved it by adding a attribute data to the tradealgorithm struct. This works. But is this efficient? Data can contain thousands of data-points at a certain point. Cloning these multiple times seems very inefficient.

Would it be better to make some kind of lazy_static Arc/Mutex hashmap with the algorithm as key and the data vector as value. So that I don't have to clone as much?

The only meaningful way to answer these questions is empirically, by benchmarking & profiling your code using realistic data and looking at the results.

2 Likes