Skip to content

feat: graph refactor chapter I#939

Open
poneciak57 wants to merge 30 commits intomainfrom
feat/graph_refactor_1
Open

feat: graph refactor chapter I#939
poneciak57 wants to merge 30 commits intomainfrom
feat/graph_refactor_1

Conversation

@poneciak57
Copy link
Contributor

@poneciak57 poneciak57 commented Feb 5, 2026

Closes #

Base for many features that can not be implemented with current graph processing structure. This refactor does not change anything. It aims to implement new structure for graph processing which will allow:

  • assynchronous fast updates
  • multiple outputs from node
  • cycles with delay node
  • performance improvement
  • it will allow to get rid of all allocations on audio thread
  • it will allow to utilize fully asynchronous communication between JS and audio threads

This pull requests contains only not integrated implementations and a lot of unit tests

⚠️ Breaking changes ⚠️

Introduced changes

Checklist

  • Linked relevant issue
  • Updated relevant documentation
  • Added/Conducted relevant tests
  • Performed self-review of the code
  • Updated Web Audio API coverage
  • Added support for web
  • Updated old arch android spec file

@poneciak57 poneciak57 changed the title feat: graph refactor part 1 feat: graph refactor chapter I Feb 5, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Bro's inteligence needs to be studied

This pull request introduces the foundational infrastructure for a new graph processing system that will enable asynchronous updates, multiple outputs, cycles with delay nodes, and allocation-free audio thread operation. The changes are non-integrated, consisting of new data structures, algorithms, and comprehensive unit tests.

Changes:

  • Implements new thread-safe graph system with separate HostGraph (main thread) and AudioGraph (audio thread) coordination
  • Adds allocation-free InputPool for edge storage and custom allocators/disposers
  • Creates comprehensive test infrastructure with allocation tracking, fuzz testing, and concurrency tests

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
AudioGraph.hpp Implements cache-friendly topological sort and compaction with O(1) extra space
HostGraph.hpp Main-thread graph mirror with cycle detection via DFS
Graph.hpp Thread-safe coordinator bridging host/audio graphs via SPSC channel
InputPool.hpp Free-list pool for allocation-free edge storage during audio processing
FatFunction.hpp Fixed-size function wrapper avoiding heap allocation
NodeHandle.hpp Shared handle bridging HostGraph and AudioGraph ownership
HostNode.hpp RAII base class for host-side audio nodes
Disposer.hpp Off-thread disposal system for old buffers
AudioNode.h/.cpp Adds canBeDestructed() method for graph compaction control
Test files Comprehensive unit, fuzz, and concurrency tests with allocation tracking
Build/CI files Separate test suite with sanitizer support and Docker environment

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Member

@michalsek michalsek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels awkward to write just LGTM under this PR, so here are lyrics for Queens We are the champions. Great job! (And check the copilot comments 🙂)

I've paid my dues
Time after time
I've done my sentence
But committed no crime
And bad mistakes
I've made a few
I've had my share of sand kicked in my face
But I've come through
(We're gonna go on and on and on and on)
We are the champions, my friend
And we'll keep on fighting till the end
We are the champions
We are the champions
No time for losers
Cause we are the champions
Of the world
I've taken my bows
And my curtain calls
You brought me fame and fortune and everything that goes with it
I thank you all
But it's been no bed of roses
No pleasure cruise
I consider it a challenge before the whole human race
And I ain't gonna lose
(We're gonna go on and on and on and on)
We are the champions, my friends
And we'll keep on fighting till the end
We are the champions
We are the champions
No time for losers
Cause we are the champions of the world
We are the champions, my friends
And we'll keep on fighting till the end
We are the champions
We are the champions
No time for losers
Cause we are the champions
Of the world

poneciak57 and others added 3 commits February 24, 2026 19:08
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…hCycleDebugTest.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…hCycleDebugTest.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@poneciak57
Copy link
Contributor Author

poneciak57 commented Feb 24, 2026

ok there is memory leak possibly due to some time race in tests but seems like a real deal on these actions
https://github.com/software-mansion/react-native-audio-api/actions/runs/22363798273/job/64724228204
https://github.com/software-mansion/react-native-audio-api/actions/runs/22364191757/job/64725634184?pr=939

it needs to be checked, possible cause stale grow pool event in events queue is not properly destructed
possible fix: wrap it in unique_ptr

Edit: Done

@@ -0,0 +1,226 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Comment on lines +30 to +32
constexpr size_t dataSize = 64 + sizeof(void*) * 3;
constexpr size_t alignment = alignof(std::max_align_t);
constexpr size_t expectedSize = (dataSize + alignment - 1) & ~(alignment - 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain more in a code comments why dataSize is like that and expectedSize can be made more readable, we don't need fast bit tricks, since these are only tests, which do not benefit from blazingly fast code

Graph class is an orhestrator that provides high level API and manages internal structures like `AudioGraph`, `HostGraph`, `SPSC`, `Disposer` and pool capacity.

## HostGraph
`HostGraph.hpp` contains the `HostGraph` class which is responsible for managing the graph structure and providing low level API for it. It keeps track of nodes, edges, their connections, cycles and other internal metadata. It also provides API for adding, removing and connecting nodes. Each modification returns a `AGEvent` which is a closure that can be executed to apply the same modification on corelated `AudioGraph`. This move allows us to keep `HostGraph` in a state after uptade while sending async event to Audio thread, as we know if events are applied in order, we can be sure that the state of `AudioGraph` in time frame T1 will be the same as `HostGraph` in the same time frame T1.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`HostGraph.hpp` contains the `HostGraph` class which is responsible for managing the graph structure and providing low level API for it. It keeps track of nodes, edges, their connections, cycles and other internal metadata. It also provides API for adding, removing and connecting nodes. Each modification returns a `AGEvent` which is a closure that can be executed to apply the same modification on corelated `AudioGraph`. This move allows us to keep `HostGraph` in a state after uptade while sending async event to Audio thread, as we know if events are applied in order, we can be sure that the state of `AudioGraph` in time frame T1 will be the same as `HostGraph` in the same time frame T1.
`HostGraph.hpp` contains the `HostGraph` class which is responsible for managing the graph structure and providing low level API for it. It keeps track of nodes, edges, their connections, cycles and other internal metadata. It also provides API for adding, removing and connecting nodes. Each modification returns a `AGEvent` which is a closure that can be executed to apply the same modification on corelated `AudioGraph`. This move allows us to keep `HostGraph` in a state after update while sending async event to Audio thread, as we know if events are applied in order, we can be sure that the state of `AudioGraph` in time frame T1 will be the same as `HostGraph` in the same time frame T1.

> So in worst case HostGraph can silent out edge creation if it thinks it would create a cycle, this is very rare case and false negative is acceptable compared to false negative. I don't even know if this is reachable in any realistic scenario, but it is worth to mention.
## AudioGraph
`AudioGraph.hpp` contains the `AudioGraph` class which is responsible for processing the graph structure on the audio thread. It keeps track of nodes, edges, their inputs and has topological order. It minimazes memory usage, speed of processing and limits memory fragmentation by using `InputPool` to manage edges. It also provides API for processing the graph structure and applying events from `HostGraph`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`AudioGraph.hpp` contains the `AudioGraph` class which is responsible for processing the graph structure on the audio thread. It keeps track of nodes, edges, their inputs and has topological order. It minimazes memory usage, speed of processing and limits memory fragmentation by using `InputPool` to manage edges. It also provides API for processing the graph structure and applying events from `HostGraph`.
`AudioGraph.hpp` contains the `AudioGraph` class which is responsible for processing the graph structure on the audio thread. It keeps track of nodes, edges, their inputs and has topological order. It minimizes memory usage, speed of processing and limits memory fragmentation by using `InputPool` to manage edges. It also provides API for processing the graph structure and applying events from `HostGraph`.

`InputPool.hpp` contains the `InputPool` class which is responsible for managing the pool of edges. It is designed to be as efficient as possible, so it uses a free list to manage the pool of edges. It also provides API for allocating and deallocating edges. It is designed to be used on the audio thread, so it is not thread safe and should be used within the `AudioGraph` class. Or its events.

## NodeHandle
`NodeHandle.hpp` contains the `NodeHandle` class which is responsible for managing node index in AudioGraph. Indexes are 32 bit to minimize memory usage and maximize cache efficiency. So it is only to be used in `AGEvent` closures to reference nodes in `AudioGraph` only having a handle to it.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`NodeHandle.hpp` contains the `NodeHandle` class which is responsible for managing node index in AudioGraph. Indexes are 32 bit to minimize memory usage and maximize cache efficiency. So it is only to be used in `AGEvent` closures to reference nodes in `AudioGraph` only having a handle to it.
`NodeHandle.hpp` contains the `NodeHandle` class which is responsible for managing node index in AudioGraph. Indices are 32 bit to minimize memory usage and maximize cache efficiency. So it is only to be used in `AGEvent` closures to reference nodes in `AudioGraph` only having a handle to it.


## Tips and tricks
As running docker tests takes forevewer it is recommended to relly on CI/CD. Tests without docker does not have ASAN and may not catch memory leaks or address realated issues. So if anny of these issues occur in CI/CD here is how you can run single test with docker to debug it locally:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
As running docker tests takes forevewer it is recommended to relly on CI/CD. Tests without docker does not have ASAN and may not catch memory leaks or address realated issues. So if anny of these issues occur in CI/CD here is how you can run single test with docker to debug it locally:
As running docker tests takes forevewer it is recommended to relly on CI/CD. Tests without docker does not have ASAN and may not catch memory leaks or address realated issues. So if any of these issues occur in CI/CD here is how you can run single test with docker to debug it locally:

@@ -0,0 +1,24 @@

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can delete it

void sendPoolGrowIfNeeded() {
auto edges = static_cast<std::uint32_t>(hostGraph.edgeCount());
if (edges > poolCapacity_ / 2 || (poolCapacity_ == 0 && edges > 0)) {
std::uint32_t newCap = std::max(static_cast<std::uint32_t>(edges * 2), std::uint32_t{64});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's include that constraint directly in inputPool ctor

Comment on lines +176 to +177
for (Node *n : nodes)
delete n;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (Node *n : nodes)
delete n;
for (Node *n : nodes) {
delete n;
}

Comment on lines +272 to +273
if (start == end)
return true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (start == end)
return true;
if (start == end) {
return true;
}

Comment on lines +105 to +118
void *operator new(std::size_t size) {
audioapi::test::AudioThreadGuard::recordAllocation();
void *p = std::malloc(size);
if (!p) throw std::bad_alloc();
return p;
}

void *operator new[](std::size_t size) {
audioapi::test::AudioThreadGuard::recordAllocation();
void *p = std::malloc(size);
if (!p) throw std::bad_alloc();
return p;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double implementation

Comment on lines +106 to +107
// Drain events (grow + graph-mutation). May allocate — unguarded.
graph_.processEvents();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it is allowed to allocate here? as I understand it is mock of audio engine render function, so allocations are forbidden.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover comment will be fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants