Developer’s Guide¶
This guide contains information for developers contributing to dftracer utilities.
For more detailed development information, see the DEVELOPERS_GUIDE.md in the repository.
Development Setup¶
Clone the repository:
git clone https://github.com/LLNL/dftracer-utils.git cd dftracer-utils
Install development dependencies:
pip install -e ".[dev]"
Build the C++ components:
mkdir build && cd build cmake .. make
Running Tests¶
Python Tests¶
pytest tests/
C++ Tests¶
cd build
ctest
Code Coverage¶
To run tests with coverage:
./coverage.sh
Building Documentation¶
To build the documentation locally:
cd docs
make html
The built documentation will be in docs/build/html/.
Code Style¶
Python¶
This project uses ruff for linting/formatting and ty for type checking.
Both are run via uvx (no install needed):
# Lint and format check
make lint
# Type check
make typecheck
# Or directly
uvx ruff check python/ tests/python/
uvx ruff format --check python/ tests/python/
uvx ty check python/
# Auto-fix lint issues
uvx ruff check --fix python/ tests/python/
# Auto-format
uvx ruff format python/ tests/python/
Configuration is in pyproject.toml under [tool.ruff].
C++¶
This project uses clang-format (v19.1.7) for C++ code formatting:
make format # auto-fix
make check-format # check only (CI uses this)
Git Hooks¶
Install the project’s pre-commit hooks:
./scripts/git-hooks.sh install
The pre-commit hook runs:
C/C++:
clang-formaton staged.c/.cpp/.h/.hppfilesPython:
ruff check,ruff format --check, andty checkon staged.py/.pyifiles
Python checks require uvx or ruff in PATH; they are skipped gracefully if not available.
Contributing¶
Fork the repository
Create a feature branch
Install git hooks:
./scripts/git-hooks.sh installMake your changes
Run tests and ensure they pass (
make test && make test-py)Run lint and type check (
make lint && make typecheck)Submit a pull request
Coding Guidelines¶
Follow the existing code style
Write tests for new functionality
Update documentation as needed
Keep commits atomic and well-described
All Python code must pass
ruff checkandty checkAll C++ code must pass
clang-formatcheck
Coroutine Development Guidelines¶
dftracer utilities uses C++20 coroutines extensively for async I/O and concurrent pipeline processing. Coroutines require careful handling of object lifetimes and capture semantics because coroutine frames are heap-allocated and may outlive the caller’s stack.
Capture Rules for Coroutine Lambdas¶
Scalars (int, bool, size_t, enum): capture by value
Cheap and always safe. The scalar value is copied into the coroutine frame.
int event_id = 42;
auto task = [event_id](CoroScope& scope) -> coro::CoroTask<void> {
// Safe: event_id is copied into the coroutine frame
co_await channel.send(event_id);
};
Owning types (std::string, shared_ptr, unique_ptr): capture by value
Safe because the coroutine owns a copy. Automatic cleanup on coroutine destruction.
std::string filename = "trace.pfw.gz";
auto task = [filename](CoroScope& scope) -> coro::CoroTask<void> {
// Safe: coroutine owns a copy of the string
std::cout << "Processing " << filename << "\n";
co_await something_async();
};
Large containers (std::vector, std::map): use pointer-by-value
Avoid expensive deep copies. Use pointer-by-value (auto* ptr = &vec; [ptr](…)).
std::vector<Event> events = load_events();
auto* events_ptr = &events;
auto task = [events_ptr](CoroScope& scope) -> coro::CoroTask<void> {
// Safe: events_ptr points to the vector in the caller's scope
for (const auto& e : *events_ptr) {
co_await process_event(e);
}
};
// WRONG: Do NOT capture the entire vector
// auto task = [events](CoroScope& scope) -> coro::CoroTask<void> { // BAD!
// for (const auto& e : events) { ... }
// };
Non-owning views (string_view, span<T>, raw T*, iterators): NEVER capture by value
String_view and span are non-owning views. Capturing by value copies the view but NOT the underlying data. The underlying data will be freed before the coroutine runs, leading to use-after-free bugs. Use pointer-by-value instead.
std::string data = "important";
std::string_view view = data;
// WRONG: view points to freed memory
// auto task = [view](CoroScope& scope) -> coro::CoroTask<void> {
// std::cout << view << "\n"; // Use-after-free!
// };
// CORRECT: use pointer-by-value
auto* data_ptr = &data;
auto task = [data_ptr](CoroScope& scope) -> coro::CoroTask<void> {
std::string_view safe_view(*data_ptr);
std::cout << safe_view << "\n"; // Safe
};
References (&var): NEVER capture by reference in coroutine lambdas
References in coroutine lambdas dangle immediately. Use pointer-by-value or value capture instead.
int counter = 0;
// WRONG: reference dangles
// auto task = [&counter](CoroScope& scope) -> coro::CoroTask<void> {
// counter++; // Undefined behavior!
// };
// CORRECT: use pointer-by-value
auto* counter_ptr = &counter;
auto task = [counter_ptr](CoroScope& scope) -> coro::CoroTask<void> {
(*counter_ptr)++; // Safe
};
Default capture ([&]): NEVER use in coroutine lambdas
Default capture by reference captures all variables by reference, leading to dangling pointers. Always use explicit capture lists.
int event_id = 42;
std::string name = "event";
// WRONG: all variables captured by reference
// auto task = [&](CoroScope& scope) -> coro::CoroTask<void> { ... };
// CORRECT: explicit captures by value or pointer
auto* name_ptr = &name;
auto task = [event_id, name_ptr](CoroScope& scope) -> coro::CoroTask<void> {
co_await channel.send(event_id);
std::cout << *name_ptr << "\n";
};
CoroScope Lifetime Rules¶
Always ensure the CoroScope outlives all spawned tasks and channels.
auto task = make_task([](CoroScope& scope) -> coro::CoroTask<void> {
auto channel = coro::make_channel<Event>(100);
// Spawn producer -- producer() pre-registers the slot eagerly
scope.spawn([ch = channel->producer()](CoroScope& s) mutable
-> coro::CoroTask<void> {
auto guard = ch.guard();
for (int i = 0; i < 100; ++i) {
co_await ch.send(Event{i});
}
// ~ProducerGuard auto-releases; channel closes when last producer exits
co_return;
});
// Consumer reads until channel closes
while (auto event = co_await channel->receive()) {
process(*event);
}
co_return;
});
Channel Patterns¶
Use bounded channels for backpressure control:
// Bounded channel: send() blocks if queue is full
coro::Channel<Event> bounded_ch(1000);
// Unbounded channel: send() never blocks (use carefully)
coro::Channel<Event> unbounded_ch(0);
HasherUtility Pattern¶
For hot loops, reuse a single HasherUtility instance with reset():
// Create once, reuse many times
HasherUtility hasher;
for (const auto& event : events) {
hasher.reset(); // Clear state before each hash
hasher.update(event.data);
auto hash = hasher.finalize();
// ... use hash ...
}
// WRONG: allocating per-event is expensive
// for (const auto& event : events) {
// HasherUtility temp_hasher; // BAD!
// temp_hasher.update(event.data);
// auto hash = temp_hasher.finalize();
// }
Anti-Patterns to Avoid¶
Storing JsonValue / simdjson views beyond the parser’s lifetime
JsonValue (and the underlying simdjson::ondemand::value /
simdjson::dom::element) is a non-owning view into the parser’s buffer.
Never store it across the parser’s or the input buffer’s lifetime.
#include <simdjson.h>
// WRONG: parser/buffer destroyed, but view stored
JsonValue stored_value;
{
simdjson::ondemand::parser parser;
auto padded = simdjson::padded_string::load("config.json");
auto doc = parser.iterate(padded);
stored_value = doc.find_field("root").value();
}
// stored_value now points into freed parser/buffer memory!
// CORRECT: copy the data out before the parser goes out of scope
{
simdjson::ondemand::parser parser;
auto padded = simdjson::padded_string::load("config.json");
auto doc = parser.iterate(padded);
auto data = serialize_json_value(doc.find_field("root").value());
// data owns its copy; safe to use after the parser is destroyed
}
Instantiating IOExecutor directly
IOExecutor is internal to the Pipeline. Never create it directly; use Pipeline or task framework instead.
Per-event SQL indexing
Avoid querying the database for every event. Use bloom filters and per-chunk statistics instead.
// WRONG: N database queries for N events
for (const auto& event : events) {
auto result = db.query(event.key); // BAD!
}
// CORRECT: batch statistics with bloom filters
BloomIndex bloom;
for (const auto& chunk : chunks) {
bloom.add_chunk_stats(chunk);
}
Old Pipeline API
All new binaries must use the coroutine + channel pattern. Do not use the old synchronous Pipeline API.
Batch materialization
Stream through channels incrementally; avoid materializing entire batches into vectors.
// WRONG: materializes entire batch
std::vector<Event> batch;
while (auto event = co_await channel.receive()) {
batch.push_back(*event);
}
// process batch...
// CORRECT: process incrementally
while (auto event = co_await channel.receive()) {
co_await process_event(*event);
}