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

Document recommendations around threading #1601

Merged
merged 7 commits into from
Jun 18, 2024
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
2 changes: 1 addition & 1 deletion doc/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default defineConfig({
{ link: "Developer Guide/Continuous Integration/Readme.md", text: "Continuous Integration (CI/CD)" },
{ link: "Developer Guide/Attributes/Readme.md", text: "Attributes" },
{ link: "Developer Guide/Known Issues/Readme.md", text: "Known Issues" },
{ link: "Developer Guide/Appendix A/Readme.md", text: "Appendix A: Macro Strings" }
{ link: "Developer Guide/Appendix/Readme.md", text: "Appendix: Macro Strings" }
]
},
{
Expand Down
59 changes: 0 additions & 59 deletions doc/Developer Guide/Appendix A/Readme.md

This file was deleted.

Binary file added doc/Developer Guide/Appendix/3lockedDelays.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/Developer Guide/Appendix/4delays.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
268 changes: 268 additions & 0 deletions doc/Developer Guide/Appendix/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
Macro Strings
=========================

Sometimes certain elements need customizable text that can dynamically change depending on circumstances. These are known as macros and are identifiable by the use of the `<` and `>` symbols in text. Macros can be expanded by the plugin developer to use other macros with different values depending on the context. The class used for macro string properties is called `OpenTAP.MacroString`.

One example is the `<Date>` macro that is available to use in many Result Listeners, like the log or the CSV result listeners. Another example is the `<Verdict>` macro. These are both examples of macros that can be inserted into the file name of a log or CSV file like so: `Results/<Date>-<Verdict>.txt`. If you insert `<Date>` in the file name the macro will be replaced by the start date and time of the test plan execution.

When used with the MetaData attribute, a property of the ComponentSettings can be used to define a new macro. For example, all DUTs have an ID property that has been marked with the attribute `[MetaData("DUT ID")]`. This means that you can put `<DUT ID>` into the file path of a Text Log Listener to include the DUT ID in the log file's name.

In addition to macros using `<>`, environment variables such as `%USERPROFILE%` will also be expanded.

There are a few different contexts in which macro strings can be used.

## Test Steps

MacroStrings can be used in test steps. In this context the following macros are available:

- `<Date>`: The start date of the test plan execution
- `<TestPlanDir>`: The directory of the currently executing test plan
- MetaData attribute: Defines macro properties on parent test steps

Verdict is not available as a macro in the case of test steps, because at the time of execution the step does not yet have a verdict. However, it can be manually added by the developer if needed. In this case it is up to the plugin developer to provide documentation.

Below is an example of MacroString used with the `[FilePath]` attribute in a test step. This attribute provides the information that the text represents: a file path. In the GUI Editor this results in the `"..."` browse button being shown next to the text box.

```cs
public class MyTestStep: TestStep {

[FilePath] // A MacroString that is also a file path.
public MacroString Filename { get; set; }

public MyTestStep(){
// 'this' useful for TestStep instances.
// otherwise a MacroString can be created without constructor arguments.
Filename = new MacroString(this) { Text = "MyDefaultPath" };
}
public override void Run(){
Log.Info("The full path was '{0}'.", Path.GetFullPath(Filename.Expand(PlanRun)));
}
}
```

## Result Listeners

Result listeners have access to TestStepRun and TestPlanRun objects which contain variables that can be used as macros. An example is the previously mentioned DUT ID property, which is available if a DUT is used in the test plan. The following macros are available in the case of result listeners:

- `<Date>`: The start date and time of the executing test plan.
- `<Verdict>`: The verdict of the executing test plan. Only available in OnTestPlanRunCompleted.
- `<DUT ID>`: The ID (or ID's) of the DUT (or DUTs) used in the test plan.
- `<OperatorName>`: Normally the name of the user on the test station.
- `<StationName>`: The name of the test station.
- `<TestPlanName>`: The name of the executing test plan.
- `<ResultType>` (CSV only): The type of result being registered. This macro can be used if it is required to create multiple files, one for each type of results per test plan run.

## Other Uses

Macro strings can also be used in custom contexts defined by a plugin developer. In this case it is up to the plugin developer to provide documentation of the available macros.

One example is the session log. It can be configured in the **Engine** pane in the **Settings** panel. The session log only supports the `<Date>` macro, which is defined as the start date and time of the OpenTAP instance and not the test plan run. This is because the session is active for multiple test plan runs and needs to be loaded when OpenTAP starts, therefore, most macros are not applicable.

# Threading and Parallel Processing

Threading and parallelism are essential tools for enhancing the performance of test plan execution. By default, a test plan executes in a single thread, but it can branch off into multiple parallel threads as it progresses. Utilizing logging or Result Listeners can cause certain actions to execute in separate threads.

However, parallelism comes with some limitations due to the inherent complexity of managing multiple threads.

In OpenTAP and C#, parallelism can be implemented in several ways:

- **Parallel Steps**: The simplest form of parallelism, allowing test steps to be executed concurrently.
- **Deferred Processing**: Enables the processing of results in a separate thread.
- **TapThreads**: OpenTAP's own thread pool, which manages parent and child threads.
- **.NET Threads**: Basic threading provided by the .NET framework.
- **.NET Tasks**: Lightweight threads, also provided by the .NET framework.

## Parallel Steps

The parallel step is a test step from the OpenTAP basic plugins. It runs all the child steps in parallel threads.

In the below screen shot, you can see four delay steps running in parallel inside a Parallel step:

![Four Delays](./4delays.png)

It is best to use this step in a configuration where the resources used are not interferring with each other.


For example, you can have one branch setting up an instrument and one configuring a DUT. If threads needs to access the same instrument at the same time you might get unexpected behavior due to race conditions.

In order to control this, you can use the Lock step, which locks a named local or system-wide mutex.

In the below screenshot you can see how parallel steps with locks are evaluated. Notice that the total time was 3x the delay because none of the steps were executed in parallel.

![Locked Delays](3lockedDelays.png)


## Deferred Processing

Deferred results processing enables post-processing of results while the test plan execution continues. This approach is a form of limited parallelism, beneficial when performance is constrained by sequential data acquisition and processing, and there are spare computational resources available. Deferred processing is most effective when processing time significantly exceeds measurement time, but it can also be useful for shorter processing tasks.

### Sequential Processing

In traditional sequential processing, the order of operations is as follows:

![Sequential Processing](sequential.svg)

### Deferred Processing

When deferred processing is used, operations are handled in parallel, as shown in the diagram below:

![Parallel Processing](parallel.svg)

### Visualization in KS8400

In KS8400, you can visualize the parallelism to gain insights into the performance improvements. The image below shows three Measurement + Process steps with a blocking measurement part and a non-blocking processing part. The Flow column's bars indicate the blocking part of the execution in blue and the non-blocking part in dark gray.

![Deferred Parallelism](DeferredParallelism.png)

### Implementing Deferred Processing

To incorporate deferred processing within a test step's `Run` method, use the `Results.Defer` method. The example below demonstrates this:

```csharp
// This goes inside a Test Step implementation
public override void Run()
{
// Execute the blocking part of the test step
double[] data = instrument.DoMeasurement();

Results.Defer(() => {
// The non-blocking part of the execution is handled inside this anonymous function
var processedData = ProcessData(data);
Results.Publish(processedData);
var limitsPassed = CheckLimits(processedData);
if(limitsPassed)
UpgradeVerdict(Verdict.Pass);
else
UpgradeVerdict(Verdict.Fail);
});
}
```

In this example:
1. **Blocking Measurement**: The test step performs a measurement that blocks further execution.
2. **Deferred Processing**: The `Results.Defer` method queues the non-blocking processing operations to be executed concurrently.
3. **Processing**: Inside the deferred anonymous function, data is processed, results are published, and limits are checked.
4. **Verdict Upgrading**: Based on the processed data, the test verdict is upgraded to `Pass` or `Fail`.

By using deferred processing, you can optimize test execution, reducing the overall test plan duration and improving resource utilization.


## TapThreads

TapThreads function similarly to .NET Threads but include additional features tailored for OpenTAP plugins, enhancing their efficiency and manageability within the OpenTAP environment.

- **Thread Pools**: When a function is requested for execution, a thread is either retrieved from the pool or a new one is started. Once the function completes, the thread is returned to the pool. Unlike .NET Thread Pools, TapThreads are more proactive in starting new threads and are optimized for IO-bound applications, ensuring minimal latency and high performance in handling asynchronous tasks.

- **Hierarchical Structure**: TapThreads utilize thread-local storage to track the initiating thread, enabling data sharing across thread hierarchies. The `ThreadHierarchyLocal` class facilitates this data sharing. If an OpenTAP thread is aborted, its child threads also receive the abort signal. This mechanism is crucial for managing Sessions and maintaining data integrity across different layers of the thread hierarchy.

### Starting a TapThread

To start a TapThread, use `TapThread.Start()`, which provides an easy-to-use interface for running tasks asynchronously.

```csharp
TapThread.Start(() =>
{
// Perform a time-consuming task
TapThread.Sleep(100);
// Thread finishes and is returned to the pool
});
```

Threads from the pool start almost instantly due to the pre-allocation and management of live threads, ensuring efficient task execution.

### Obtaining Results

To obtain results from your thread, use the `TaskCompletionSource` object. This approach provides a robust and flexible way to handle asynchronous operations and their outcomes.

```csharp
var promise = new TaskCompletionSource<double>();
TapThread.Start(() =>
{
try
{
TapThread.Sleep(100); // Throws an exception if the thread is aborted.
promise.SetResult(9000.0);
}
catch (Exception e)
{
promise.SetException(e);
}
});

double resultValue = promise.Task.Result;
```

Using `TaskCompletionSource` ensures that your asynchronous code can handle both successful completion and exceptions in a structured manner.

Note, you can also use other synchronization mechanisms for getting the result, but this method is very robust and performant.

### Sleeping

You can make the current thread sleep with `TapThread.Sleep(TimeSpan duration)` or `TapThread.Sleep(int milliseconds)`. This pauses the thread for at least the specified time but may take a few extra milliseconds to wake up, making it unsuitable for highly precise waits.

```csharp
TapThread.Sleep(100); // Sleep for 100 milliseconds
```

If the thread is aborted while sleeping, an `OperationCancelledException` will be thrown. To ensure the thread sleeps for the specified duration regardless of abort signals, use `System.Threading.Thread.Sleep()`:

```csharp
System.Threading.Thread.Sleep(100); // Uninterruptible sleep
```

### Aborting

TapThreads can be aborted using the `TapThread.Abort()` method. This event propagates to all child threads, which also receive the abort notification. This is a 'soft' abort, meaning the threads must cooperate to be aborted. To detect if a thread has been aborted, you have several options:

1. **Throw an Exception**: Use `TapThread.ThrowIfAborted()` to throw an exception if the current TapThread has been aborted.
```csharp
TapThread.ThrowIfAborted();
```
2. **Check Abort Status**: Check if the thread is aborted via `TapThread.Current.AbortToken.IsCancellationRequested`.
```csharp
if (TapThread.Current.AbortToken.IsCancellationRequested)
{
// Handle abort
}
```
3. **Use AbortToken**: Use the `TapThread.Current.AbortToken` with calls that support it. Various .NET APIs accept a `CancellationToken` to enable canceling long-running operations.
```csharp
var token = TapThread.Current.AbortToken;
// Example with Task.Delay
await Task.Delay(1000, token);
```
4. **Register an Event**: Register an event to occur when the thread is aborted:
```csharp
using (TapThread.Current.AbortToken.Register(() =>
{
Log.Info("The thread was aborted!");
}))
{
// Do something that takes time.
}
```

These options provide flexibility in managing thread termination and ensuring resources are cleaned up properly.

### Creating New Thread Contexts

In rare cases, you might need to run code in a context that cannot be aborted or can be aborted separately. For this, use `TapThread.WithNewContext`. It runs inside the same physical thread but creates a new temporary context where some code can be executed. You can also control which parent thread the new context has.

```csharp
var firstThread = TapThread.Current;
TapThread.WithNewContext(() =>
{
var secondThread = TapThread.Current;
// firstThread != secondThread
},
// Specify the parent thread. Null means the root thread of the application.
null);
```

Creating new thread contexts allows for isolated execution environments within the same physical thread, providing greater control over task execution and abort behavior.

## .NET Threads and Tasks

Generally, using the default .NET Threads and Tasks is not recommended. Threads are expensive to start, and tasks can exhibit unexpected behaviors that make them unsuitable for many use cases.

For OpenTAP plugins, it is recommended to use other parallelism techniques unless .NET Threads or Tasks are strictly necessary.
Loading
Loading