Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the docs #147

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
Empty file added docs2/Overview.md
Empty file.
Empty file added docs2/components/PowerPlant.md
Empty file.
6 changes: 6 additions & 0 deletions docs2/components/Reaction Tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- What is a reaction task
- How are they created
- When and where will they run
- Inline
- Thread Pool
- Priority
10 changes: 10 additions & 0 deletions docs2/components/Reactions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- What is a reaction
- What do they do
- How to create a reaction
- When a reaction will run

- Has the required data
- Has a bind event trigger

- Bind and unbind
- Disable
2 changes: 2 additions & 0 deletions docs2/components/Reactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- What is a reactor
- What they should do
41 changes: 41 additions & 0 deletions docs2/concepts/Bind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Bind

## Overview

In NUClear, the "bind" concept is a fundamental mechanism that allows functions to be executed when specific conditions are met. This is achieved by associating (or binding) functions to specific types or events. When the specified type or event occurs, the bound function is triggered and executed. This concept is essential for creating reactive systems where actions are performed in response to changes or events in the system.

## How Bind Works

The bind concept is implemented through various DSL (Domain Specific Language) words that define the conditions under which functions should be executed. These DSL words encapsulate the logic for binding functions to specific types or events, making it easier to create complex reactive behaviors.

Example: Trigger DSL Word
One example of a DSL word that implements the bind concept is the Trigger word. The Trigger word allows reactions to be triggered based on the emission of a specific type of data.

Usage
Here are some examples of how the Trigger DSL word can be used to bind functions to events:

Binding to a Specific Data Type

```cpp
on<Trigger<DataType>>().then([](const DataType& data) {
// Handle the received data
});
```

In this example, the function is bound to the emission of DataType. When data of this type is emitted, the function is triggered and executed.

Binding with Runtime Arguments
In this example, the function is bound to the emission of DataType and can also take runtime arguments. This allows for more dynamic and flexible reactions based on runtime conditions.

```cpp
on<Trigger<DataType>>(runtime_argument).then([](const DataType& data) {
// Handle the received data with the runtime argument
});
```

## Implementation

The Trigger DSL word is implemented in the src/dsl/word/Trigger.hpp file. It provides methods for binding functions to specific data types, including:

bind: Binds the function to the specified data type.
These methods encapsulate the logic for handling data emissions and provide a simple interface for binding functions to these events.
7 changes: 7 additions & 0 deletions docs2/concepts/Emit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- Talk about emit scopes
- Talk about how emit works
- Talk about multiple scopes

- Talk about emit being unique pointers so it can't be modified after emit

- Talk about emit shared and when/how it should be used
117 changes: 117 additions & 0 deletions docs2/concepts/Get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Get

The `Get` concept in NUClear's DSL (Domain Specific Language) is used to retrieve data required for a reaction.
It allows a reaction to specify dependencies on certain types of data, which will be provided when the reaction is executed.

Each of the items returned by a `get` call must be `truthy` for the reaction to execute, that is they must evaluate to `true` when cast to a boolean.
If any of the items are `falsy`, the reaction will not execute.

When these arguments are passed into the reaction function, not all of them need to be present.
If an argument is not requested by the reaction function, it not be passed in.
However, even if the argument is not requested, the get call will still occur and the result must be `truthy`.

## How does Get work

The `Get` concept works by defining a `get` function within a DSL word.
This function is called when the reaction is executed, and it retrieves the necessary data.
The retrieved data is then passed to the reaction function as arguments.

Typically implementers of the `Get` concept will use a data store to retrieve the data.
Or they may use a thread local variable that is set by another part of the system before the reaction is created.

The type which is requested by a reaction can either be the type returned by the `get` or the type which results from dereferencing the type returned by the `get`.
For example, if the `get` returns a `std::shared_ptr<T>`, the type requested by the reaction can be `T`.

## Example of a DSL word that implements Get

One example of a DSL word that implements the Get concept is the With word.
The With word retrieves the data of a specific type from NUClear's cache.

```cpp
namespace NUClear {
namespace dsl {
namespace word {
template <typename DataType>
struct With {
public:
template <typename DSL, typename T = DataType>
static std::shared_ptr<const T> get() {
return store::ThreadStore<std::shared_ptr<T>>::value == nullptr
? store::DataStore<DataType>::get()
: *store::ThreadStore<std::shared_ptr<T>>::value;
}
};
}
}
}
```

## Optional Parameters

The `Optional` keyword in NUClear's DSL (Domain Specific Language) is used to mark certain fields as optional in a `get` call. This allows the reaction function to execute even if some of the optional data is missing.

When using the `Optional` keyword, the return type of the `get` call is always "truthy", meaning it evaluates to `true` when cast to a boolean. This ensures that the reaction can execute even when it is missing some data.

By marking certain fields as optional, the reaction function can choose to use only the relevant arguments and ignore the rest. This is particularly useful when dealing with complex data structures where only a subset of the data is needed.

The `Optional` keyword ensures that the reaction function can still execute and perform its intended functionality, even if some of the optional data is not present.

### Example of Optional Parameters

In this example, we will demonstrate how to use the `Optional` keyword in conjunction with the `With` word in NUClear's DSL.

Let's say we have a reaction that needs to retrieve two types of data: `DataA` and `DataB`. However, `DataB` is optional and the reaction should still execute even if it is not present.

```cpp
on<Trigger<DataA>, Optional<With<DataB>>>().then([this](const DataA& a, const std::shared_ptr<const DataB>& b) {
// b will be null if DataB has not been emitted
});
```

## Transients in Get

The concept of transients in the context of `Get` refers to data that is only temporarily available.
When a type is marked as transient, the system caches the last copy of the data provided.
If no new data is available, the cached data is used instead.

This commonly occurs when the data is only available while the associated reaction is executing.
For example, if you were to use `on<UDP, Trigger<X>>` to receive a UDP packet, the data would be transient.
Then if the reaction were triggered by a `Trigger<X>` message, the previous data for the UDP packet would be provided.

### Flagging types as transient

To mark a type as transient, you need to define a trait that specifies that the type is transient.

For example, consider a type `TransientMessage` that represents a transient message.
The `TransientMessage` type is an example of a transient type.
It is marked as transient using the `is_transient` trait.

```cpp
namespace NUClear {
namespace dsl {
namespace trait {
template <>
struct is_transient<TransientMessage> : std::true_type {};
}
}
}
```

In the reaction, the `Get` for the TransientMessage retrieves the data, and if it is not available, it returns an empty message.
When the reaction is executed, the last known value of the TransientMessage is used if the data is not freshly available.
If no data has been received yet, the reaction will not execute.

```cpp
struct TransientGetter : NUClear::dsl::operation::TypeBind<TransientMessage> {
template <typename DSL>
static TransientMessage get(NUClear::threading::ReactionTask& task) {
auto raw = NUClear::dsl::operation::CacheGet<TransientMessage>::get<DSL>(task);
if (raw == nullptr) {
return {};
}
return *raw;
}
};
```

This ensures that the reaction can still execute even if the transient data is not freshly available, by using the last known value.
59 changes: 59 additions & 0 deletions docs2/concepts/Group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Group

The `Group` concept in NUClear is designed to ensure that across multiple threads, the concurrency of tasks flagged with a specific group is controlled.
This is particularly useful in scenarios where certain tasks should not run concurrently to avoid race conditions or to manage shared resources efficiently.

## How it Works

When a task is flagged with a `Group`, it is assigned to a specific group identified by a group type and a maximum concurrency level.
The Group concept ensures that no more than the specified number of tasks from the same group can run concurrently.
This is achieved through a combination of task queuing and locking mechanisms.

## Example: Sync<Group>

The `Sync<Group>` is a special case of the `Group` concept where the concurrency level is set to one.
This means that across multiple threads and pools, only a single reaction will be executing at the same time for the specified group.
This is particularly useful for ensuring that tasks that modify shared state do not run concurrently, thereby avoiding race conditions.

## Usage

To use the `Group` concept, you can specify it in the DSL of a reaction. For example:

```cpp
struct MyGroup {
static constexpr int max_concurrency = 2;
};

on<Trigger<T, ...>, Group<MyGroup>>()
```

In this example, at most two tasks from the `MyGroup` group can run concurrently.

For the `Sync<MySyncGroup>` case:

```cpp
on<Trigger<T, ...>, Sync<MySyncGroup>>()
```

In this example, only one task from the MySyncGroup group can run at any given time.

## Implementation Details

The `Group` concept is implemented using a combination of descriptors and locks.
Each group has a descriptor that specifies its name and maximum concurrency level.
When a task is flagged with a group, it is added to a queue managed by the group.
The group uses a locking mechanism to ensure that no more than the specified number of tasks are running concurrently.

### Group Descriptor

The GroupDescriptor struct holds the name and maximum concurrency level of a group:

```cpp
struct GroupDescriptor {
GroupDescriptor(std::string name, const int& thread_count)
: name(std::move(name)), thread_count(thread_count) {}

std::string name;
int thread_count;
};
```
77 changes: 77 additions & 0 deletions docs2/concepts/Inline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Inline

The `Inline` concept in NUClear allows you to control whether a reaction should be executed inline or in its target thread pool. This can be useful for optimizing performance and ensuring that certain tasks are executed in a specific manner.

## What is an inline task

An inline task is a reaction that is executed in the same thread as the emitter, rather than being scheduled in a separate thread pool. This can reduce the overhead of context switching and improve performance for lightweight tasks.

## How are they created

Inline tasks are created using the `emit<Scope::INLINE>` function or by specifying the `Inline::ALWAYS` keyword in the reaction definition.

## When will a task run inline

A task will run inline based on the following conditions:

### Inline emit

Using `emit<Scope::INLINE>` will attempt to run the emitted task inline. However, this can be overridden by the reaction's inline level.

### Inline::NEVER

The `Inline::NEVER` keyword can be used in the reaction definition to ensure that the reaction is never executed inline, even if it is emitted with `Scope::INLINE`.

```cpp
on<Trigger<SimpleMessage>, Inline::NEVER>().then([](const SimpleMessage& message) {
// This reaction will never run inline
});
```

### Inline::ALWAYS

The `Inline::ALWAYS` keyword can be used in the reaction definition to ensure that the reaction is always executed inline, even if it is not emitted with `Scope::INLINE`.

```cpp
on<Trigger<SimpleMessage>, Inline::ALWAYS>().then([](const SimpleMessage& message) {
// This reaction will always run inline
});
```

### Groups

Inlining will never happen if the group lock prevents it from being inlined. This ensures that tasks that require synchronization are not executed inline, which could lead to race conditions or other concurrency issues.

## Example

Here is an example of how to use the `Inline` concept in a NUClear reactor:

```cpp
class TestReactor : public NUClear::Reactor {
public:
TestReactor(std::unique_ptr<NUClear::Environment> environment) : Reactor(std::move(environment)) {

on<Trigger<SimpleMessage>, Inline::ALWAYS>().then([](const SimpleMessage& message) {
// This reaction will always run inline
});

on<Trigger<SimpleMessage>, Inline::NEVER>().then([](const SimpleMessage& message) {
// This reaction will never run inline
});

on<Trigger<SimpleMessage>>().then([](const SimpleMessage& message) {
// This reaction will run inline based on the emit scope
});

emit<Scope::INLINE>(std::make_unique<SimpleMessage>("Inline Message"));
}
};
```

In this example, the first reaction will always run inline, the second reaction will never run inline, and the third reaction will run inline based on the emit scope.

## Summary

The `Inline` concept provides fine-grained control over how reactions are executed in NUClear. By using `Inline::NEVER` and `Inline::ALWAYS`, you can ensure that reactions are executed in the desired manner. Additionally, the `emit<Scope::INLINE>` function allows you to emit tasks inline, but this can be overridden by the reaction's inline level. Finally, inlining will never happen if the group lock prevents it from being inlined, ensuring that tasks requiring synchronization are not executed inline.

This documentation provides a comprehensive overview of the `Inline` concept, including how it works, how it can be controlled, and examples of its usage.
Loading
Loading