Skip to content

Commit 5750fb9

Browse files
authored
Merge pull request #10 from dgoeries/nogil-ltob
feat: Add nogil option for LTOB and extend memory leak test
2 parents 1c17dd2 + 1080951 commit 5750fb9

2 files changed

Lines changed: 50 additions & 26 deletions

File tree

src/downsample/_ltob.c

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
22
#include <Python.h>
33
#include <math.h>
4-
#define PY_SSIZE_T_CLEAN
54
#include <numpy/arrayobject.h>
65
#include <numpy/npy_math.h>
76

@@ -57,25 +56,27 @@ static PyObject *largest_triangle_one_bucket(PyObject *self, PyObject *args) {
5756
double *x = (double *)PyArray_DATA(x_array);
5857
double *y = (double *)PyArray_DATA(y_array);
5958

60-
npy_intp dims[1] = {threshold};
61-
PyArrayObject *x_result = (PyArrayObject *)PyArray_Empty(
62-
1, dims, PyArray_DescrFromType(NPY_DOUBLE), 0);
63-
PyArrayObject *y_result = (PyArrayObject *)PyArray_Empty(
64-
1, dims, PyArray_DescrFromType(NPY_DOUBLE), 0);
65-
66-
double *x_result_data = (double *)PyArray_DATA((PyArrayObject *)x_result);
67-
double *y_result_data = (double *)PyArray_DATA((PyArrayObject *)y_result);
59+
double *result_x = (double *)malloc(threshold * sizeof(double));
60+
double *result_y = (double *)malloc(threshold * sizeof(double));
61+
if (!result_x || !result_y) {
62+
PyErr_SetString(PyExc_MemoryError,
63+
"Failed to allocate memory for result arrays.");
64+
free(result_x);
65+
free(result_y);
66+
goto fail;
67+
}
6868

6969
// Add the first point and last
70-
x_result_data[0] = npy_isfinite(x[0]) ? x[0] : 0.0;
71-
y_result_data[0] = npy_isfinite(y[0]) ? y[0] : 0.0;
72-
x_result_data[threshold - 1] =
70+
result_x[0] = npy_isfinite(x[0]) ? x[0] : 0.0;
71+
result_y[0] = npy_isfinite(y[0]) ? y[0] : 0.0;
72+
result_x[threshold - 1] =
7373
npy_isfinite(x[len_points - 1]) ? x[len_points - 1] : 0.0;
74-
y_result_data[threshold - 1] =
74+
result_y[threshold - 1] =
7575
npy_isfinite(y[len_points - 1]) ? y[len_points - 1] : 0.0;
7676

7777
double bucket_size = (double)(len_points - 2) / (double)(threshold - 2);
7878

79+
Py_BEGIN_ALLOW_THREADS;
7980
// Main loop
8081
for (npy_intp i = 1; i < threshold - 1; i++) {
8182
npy_intp start_index = (npy_intp)floor((double)i * bucket_size);
@@ -95,18 +96,24 @@ static PyObject *largest_triangle_one_bucket(PyObject *self, PyObject *args) {
9596
max_area_index = j;
9697
}
9798
}
98-
x_result_data[i] = (double)x[max_area_index];
99-
y_result_data[i] = (double)y[max_area_index];
99+
result_x[i] = (double)x[max_area_index];
100+
result_y[i] = (double)y[max_area_index];
100101
}
102+
Py_END_ALLOW_THREADS;
101103

102-
// Return x and y arrays as a tuple
103-
PyObject *result = PyTuple_Pack(2, x_result, y_result);
104-
// Clean up references
104+
npy_intp dims[1] = {threshold};
105+
PyObject *npx =
106+
PyArray_SimpleNewFromData(1, dims, NPY_DOUBLE, (void *)result_x);
107+
PyObject *npy =
108+
PyArray_SimpleNewFromData(1, dims, NPY_DOUBLE, (void *)result_y);
109+
PyArray_ENABLEFLAGS((PyArrayObject *)npx, NPY_ARRAY_OWNDATA);
110+
PyArray_ENABLEFLAGS((PyArrayObject *)npy, NPY_ARRAY_OWNDATA);
111+
112+
PyObject *result = PyTuple_Pack(2, npx, npy);
105113
Py_DECREF(x_array);
106114
Py_DECREF(y_array);
107-
Py_DECREF(x_result);
108-
Py_DECREF(y_result);
109-
115+
Py_DECREF(npx);
116+
Py_DECREF(npy);
110117
return result;
111118

112119
fail:
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
11
import tracemalloc
2+
23
import numpy as np
3-
from downsample import largest_triangle_three_buckets
4+
import pytest
5+
6+
from downsample import ltob, lttb
7+
48

9+
@pytest.mark.parametrize("func", [lttb, ltob])
10+
def test_memory_leak(func):
11+
"""
12+
Test memory leak for different LTTB functions.
513
6-
def test_memory_leak():
14+
Args:
15+
func (callable): The function to test.
16+
"""
717
tracemalloc.start()
818

19+
# Test parameters (shared for all functions)
920
size = 1_000_000
21+
threshold = 100
22+
iterations = 1_000
23+
24+
# Generate test data
1025
x = np.linspace(0, 10, size)
1126
y = np.sin(x)
12-
threshold = 100
1327

28+
# Snapshot before function execution
1429
before_snapshot = tracemalloc.take_snapshot()
1530

16-
for _ in range(1_000):
17-
result = largest_triangle_three_buckets(x, y, threshold)
31+
for _ in range(iterations):
32+
result = func(x, y, threshold)
1833
del result
1934
import gc
2035
gc.collect()
2136

37+
# Snapshot after function execution
2238
after_snapshot = tracemalloc.take_snapshot()
2339
tracemalloc.stop()
2440

41+
# Calculate memory usage
2542
before_size = sum(
2643
stat.size for stat in before_snapshot.statistics("filename"))
2744
after_size = sum(

0 commit comments

Comments
 (0)