Repository: cocotb/cocotb

Issue:
Waitables passed into First can leak Tasks
I've run into an issue where I had a simulation that slowed substantially as time passed. I eventually traced this to the fact that within the scheduler, there were an excessive number of coroutines waiting on the rising edge of my clock. Further investigation, with the help of @ktbarrett and @marlonjames revealed the issue related to the way I was using First. I was passing a Waitable into First, in this case the ClockCycles trigger. This first was awaited in a while loop and was frequently awaited upon. 

```python
while True:
   await First(ClockCycles, RisingEdge)
   #other logic here
```
The rising edge trigger frequently fired and the number of clock cycles in the ClockCycles trigger was very large. The issue appears to be with the fact  `First._wait` calls `wait_callback` which in turn makes a call to `scheduler._trigger_from_any`. I am told `scheduler._trigger_from_any` creates a new task and this task is not cleaned up by First. This is the mechanism by which First leaks tasks. 

Let me know if example code is helpful and I'll be happy to try to create a simple test case.

Environment:
- Cocotb 1.5.2 and 1.6.1
- Alma Linux 8.5
- Riviera Pro 2021.10 64 bit
- Custom Installation of Python 3.7




Discussion:
`First._wait` creates a new task for each trigger with the `_wait_callback` coroutine function (line 884):
https://github.com/cocotb/cocotb/blob/b4a2ce033fb21b10e2a2deb0dbb95d0b9e895e9c/cocotb/triggers.py#L873-L884

`_wait_callback` uses `scheduler._trigger_from_any`:
https://github.com/cocotb/cocotb/blob/b4a2ce033fb21b10e2a2deb0dbb95d0b9e895e9c/cocotb/triggers.py#L802-L809

`scheduler._trigger_from_any` calls `scheduler._trigger_from_waitable`, which creates another new `Task`:
https://github.com/cocotb/cocotb/blob/b4a2ce033fb21b10e2a2deb0dbb95d0b9e895e9c/cocotb/scheduler.py#L867-L868

When the `RisingEdge` trigger fires, `First._wait` kills the `Task` waiting in `_wait_callback`, but that does not kill the extra `Task` created from the `ClockCycles` waitable.

In this situation, `ClockCycles._wait` is a long-lived coroutine, so each time the loop executes, another leaked `Task` is created and the old ones are still running.
Is it possible to simply not use `_trigger_from_any` here? Presumably the object being awaited will know best how to implement a wait for that object.
That would solve the issue, since `_wait_callback` is only used by `First` and `Combine`, which check for valid trigger types on construction.
IMO `_trigger_from_waitable` is totally broken and should be removed. The only other use is in `Scheduler.schedule` and the `result` of `task._advance` cannot be a `Waitable` object, it can only be a `Trigger`, a `Task`, or a `list`. Maybe if we had typing we could prove that to ourselves easily.
Yeah, the only reason `_trigger_from_waitable` is ever called is because `_wait_callback` calls the private scheduler `_trigger_from_any`, and it only gets hit when a `Waitable` is inside another `Waitable`.
Actually I guess it's possible to yield `Waitable`s and coroutines and other things from generator-based coroutines. IMO this is something that the `@cocotb.coroutine` decorator should handle *not* the scheduler. It should bridge yielded objects using yield from the object's `__await__`, much like `asyncio.coroutine` does. More ideally, since we are Python 3.6+ now, we can drop the decorator after 1.7.

Resolving patch:
diff --git a/cocotb/triggers.py b/cocotb/triggers.py
--- a/cocotb/triggers.py
+++ b/cocotb/triggers.py
@@ -803,7 +803,6 @@ async def _wait_callback(trigger, callback):
     """
     Wait for a trigger, and call `callback` with the outcome of the await.
     """
-    trigger = cocotb.scheduler._trigger_from_any(trigger)
     try:
         ret = outcomes.Value(await trigger)
     except BaseException as exc:


Test patch: