Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ concurrency:
env:
CIBW_SKIP: >
pp*
PYAWAITABLE_OPTIMIZED: 1

jobs:
binary-wheels-standard:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/memory_leak.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ env:
PYTHONUNBUFFERED: "1"
FORCE_COLOR: "1"
PYTHONIOENCODING: "utf8"
PYTHONMALLOC: malloc

jobs:
memory-leaks:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ env:
PYTHONUNBUFFERED: "1"
FORCE_COLOR: "1"
PYTHONIOENCODING: "utf8"
PYAWAITABLE_OPTIMIZED: 1

jobs:
run-tests:
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

- Significantly reduced awaitable object size by dynamically allocating it.
- Reduced memory footprint by removing preallocated awaitable objects.
- Objects returned by a PyAwaitable object's `__await__` are now garbage collected (*i.e.*, they don't leak with rare circular references).
- Removed limit on number of stored callbacks or values.
- Switched some user-error messages to `RuntimeError` instead of `SystemError`.

## [1.3.0] - 2024-10-26

- Added support for `async with` via `pyawaitable_async_with`.
Expand Down
258 changes: 258 additions & 0 deletions include/pyawaitable/array.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
#ifndef PYAWAITABLE_ARRAY_H
#define PYAWAITABLE_ARRAY_H

#include <Python.h>
#include <stdlib.h>

#define pyawaitable_array_DEFAULT_SIZE 16

/*
* Deallocator for items on a pyawaitable_array structure. A NULL pointer
* will never be given to the deallocator.
*/
typedef void (*pyawaitable_array_deallocator)(void *);

/*
* Internal only dynamic array for CPython.
*/
typedef struct
{
/*
* The actual items in the dynamic array.
* Don't access this field publicly to get
* items--use pyawaitable_array_GET_ITEM() instead.
*/
void **items;
/*
* The length of the actual items array allocation.
*/
Py_ssize_t capacity;
/*
* The number of items in the array.
* Don't use this field publicly--use pyawaitable_array_LENGTH()
*/
Py_ssize_t length;
/*
* The deallocator, set by one of the initializer functions.
* This may be NULL.
*/
pyawaitable_array_deallocator deallocator;
} pyawaitable_array;


/* Zero out the array */
static inline void
pyawaitable_array_ZERO(pyawaitable_array *array)
{
assert(array != NULL);
array->deallocator = NULL;
array->items = NULL;
array->length = 0;
array->capacity = 0;
}

static inline void
pyawaitable_array_ASSERT_VALID(pyawaitable_array *array)
{
assert(array != NULL);
assert(array->items != NULL);
}

static inline void
pyawaitable_array_ASSERT_INDEX(pyawaitable_array *array, Py_ssize_t index)
{
// Ensure the index is valid
assert(index < array->length);
assert(index >= 0);
}

/*
* Initialize a dynamic array with an initial size and deallocator.
*
* If the deallocator is NULL, then nothing happens to items upon
* removal and upon array clearing.
*
* Returns -1 upon failure, 0 otherwise.
*/
int
pyawaitable_array_init_with_size(
pyawaitable_array *array,
pyawaitable_array_deallocator deallocator,
Py_ssize_t initial
);

/*
* Append to the array.
*
* Returns -1 upon failure, 0 otherwise.
* If this fails, the deallocator is not ran on the item.
*/
int pyawaitable_array_append(pyawaitable_array *array, void *item);

/*
* Insert an item at the target index. The index
* must currently be a valid index in the array.
*
* Returns -1 upon failure, 0 otherwise.
* If this fails, the deallocator is not ran on the item.
*/
int
pyawaitable_array_insert(
pyawaitable_array *array,
Py_ssize_t index,
void *item
);

/* Remove all items from the array. */
void
pyawaitable_array_clear_items(pyawaitable_array *array);

/*
* Clear all the fields on the array.
*
* Note that this does *not* free the actual dynamic array
* structure--use pyawaitable_array_Free() for that.
*
* It's safe to call pyawaitable_array_init() or init_with_size() again
* on the array after calling this.
*/
void pyawaitable_array_clear(pyawaitable_array *array);

/*
* Set a value at index in the array.
*
* If an item already exists at the target index, the deallocator
* is called on it, if the array has one set.
*
* This cannot fail.
*/
void
pyawaitable_array_set(pyawaitable_array *array, Py_ssize_t index, void *item);

/*
* Remove the item at the index, and call the deallocator on it (if the array
* has one set).
*
* This cannot fail.
*/
void
pyawaitable_array_remove(pyawaitable_array *array, Py_ssize_t index);

/*
* Remove the item at the index *without* deallocating it, and
* return the item.
*
* This cannot fail.
*/
void *
pyawaitable_array_pop(pyawaitable_array *array, Py_ssize_t index);

/*
* Clear all the fields on a dynamic array, and then
* free the dynamic array structure itself.
*
* The array must have been created by pyawaitable_array_new()
*/
static inline void
pyawaitable_array_free(pyawaitable_array *array)
{
pyawaitable_array_ASSERT_VALID(array);
pyawaitable_array_clear(array);
PyMem_RawFree(array);
}

/*
* Equivalent to pyawaitable_array_init_with_size() with a default size of 16.
*
* Returns -1 upon failure, 0 otherwise.
*/
static inline int
pyawaitable_array_init(
pyawaitable_array *array,
pyawaitable_array_deallocator deallocator
)
{
return pyawaitable_array_init_with_size(
array,
deallocator,
pyawaitable_array_DEFAULT_SIZE
);
}

/*
* Allocate and create a new dynamic array on the heap.
*
* The returned pointer should be freed with pyawaitable_array_free()
* If this function fails, it returns NULL.
*/
static inline pyawaitable_array *
pyawaitable_array_new_with_size(
pyawaitable_array_deallocator deallocator,
Py_ssize_t initial
)
{
pyawaitable_array *array = PyMem_Malloc(sizeof(pyawaitable_array));
if (array == NULL)
{
return NULL;
}

if (pyawaitable_array_init_with_size(array, deallocator, initial) < 0)
{
PyMem_Free(array);
return NULL;
}

pyawaitable_array_ASSERT_VALID(array); // Sanity check
return array;
}

/*
* Equivalent to pyawaitable_array_new_with_size() with a size of 16.
*
* The returned array must be freed with pyawaitable_array_free().
* Returns NULL on failure.
*/
static inline pyawaitable_array *
pyawaitable_array_new(pyawaitable_array_deallocator deallocator)
{
return pyawaitable_array_new_with_size(
deallocator,
pyawaitable_array_DEFAULT_SIZE
);
}

/*
* Get an item from the array. This cannot fail.
*
* If the index is not valid, this is undefined behavior.
*/
static inline void *
pyawaitable_array_GET_ITEM(pyawaitable_array *array, Py_ssize_t index)
{
pyawaitable_array_ASSERT_VALID(array);
pyawaitable_array_ASSERT_INDEX(array, index);
return array->items[index];
}

/*
* Get the length of the array. This cannot fail.
*/
static inline Py_ssize_t
pyawaitable_array_LENGTH(pyawaitable_array *array)
{
pyawaitable_array_ASSERT_VALID(array);
return array->length;
}

/*
* Pop the item at the end the array.
* This function cannot fail.
*/
static inline void *
pyawaitable_array_pop_top(pyawaitable_array *array)
{
return pyawaitable_array_pop(array, pyawaitable_array_LENGTH(array) - 1);
}

#endif
39 changes: 13 additions & 26 deletions include/pyawaitable/awaitableobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
#include <Python.h>
#include <stdbool.h>

#include <pyawaitable/array.h>

typedef int (*awaitcallback)(PyObject *, PyObject *);
typedef int (*awaitcallback_err)(PyObject *, PyObject *);
#define CALLBACK_ARRAY_SIZE 128
#define VALUE_ARRAY_SIZE 32

typedef struct _pyawaitable_callback
{
Expand All @@ -21,31 +21,23 @@ struct _PyAwaitableObject
{
PyObject_HEAD

// Callbacks
pyawaitable_callback aw_callbacks[CALLBACK_ARRAY_SIZE];
Py_ssize_t aw_callback_index;

// Stored Values
PyObject *aw_values[VALUE_ARRAY_SIZE];
Py_ssize_t aw_values_index;

// Arbitrary Values
void *aw_arb_values[VALUE_ARRAY_SIZE];
Py_ssize_t aw_arb_values_index;

// Integer Values
long aw_int_values[VALUE_ARRAY_SIZE];
Py_ssize_t aw_int_values_index;
pyawaitable_array aw_callbacks;
pyawaitable_array aw_object_values;
pyawaitable_array aw_arbitrary_values;
pyawaitable_array aw_integer_values;

// Awaitable State
/* Index of current callback */
Py_ssize_t aw_state;
/* Is the awaitable done? */
bool aw_done;
/* Was the awaitable awaited? */
bool aw_awaited;
bool aw_used;

// Misc
/* Strong reference to the result of the coroutine. */
PyObject *aw_result;
/* Strong reference to the genwrapper. */
PyObject *aw_gen;
/* Set to 1 if the object was cancelled, for introspection against callbacks */
int aw_recently_cancelled;
};

typedef struct _PyAwaitableObject PyAwaitableObject;
Expand Down Expand Up @@ -78,9 +70,4 @@ pyawaitable_await_function_impl(
...
);

int
alloc_awaitable_pool(void);
void
dealloc_awaitable_pool(void);

#endif
3 changes: 1 addition & 2 deletions include/pyawaitable/genwrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ genwrapper_next(PyObject *self);

int genwrapper_fire_err_callback(
PyObject *self,
PyObject *await,
pyawaitable_callback *cb
awaitcallback_err err_callback
);

PyObject *
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
from glob import glob

from setuptools import Extension, setup
import os

if __name__ == "__main__":
setup(
name="pyawaitable",
license="MIT",
version="1.3.0",
version="1.4.0-dev",
ext_modules=[
Extension(
"_pyawaitable",
glob("./src/_pyawaitable/*.c"),
include_dirs=["./include/", "./src/pyawaitable/"],
extra_compile_args=["-g", "-O3"],
extra_compile_args=["-g", "-O3" if os.environ.get("PYAWAITABLE_OPTIMIZED") else "-O0"],
)
],
package_dir={"": "src"},
Expand Down
Loading
Loading