rohd
rohd copied to clipboard
Allow immediate scheduling of an action within the same delta cycle
Description & Motivation
This is a Rohme inspired enhancement to enable things like external IO inside a non Future timer callback.
// an async* stream generator, consuming actual wall clock time
var publisher = publish(loops, Duration(milliseconds: 250));
Simulator simulator = Simulator(clockPeriod: SimDuration(picoseconds: 10));
simulator.run((simulator) async {
Future.delayed(tickTime(5), () async {
expect(simulator.elapsedTicks, 5);
// subscribe(.) consumes actual wall clock time, needed to receive the entire stream
// but does not consume simulation time.
await simulator.immediate(() => subscribe(publisher) );
expect(simulator.elapsedTicks, 5);
});
Future.delayed(tickTime(10), () async {
expect(simulator.elapsedTicks, 10);
});
});
await simulator.elapse(SimDuration(picoseconds: 1000));
The Rohme call simulator.immediate( action ) is a thinish wrapper around the new Rohd call Simulator.registerImmediateAction( action ). It just wraps a Completer around the underlying action:
import 'package:rohd/rohd.dart' as rohd show Simulator;
typedef _RohdSim = rohd.Simulator;
class Simulator
{
...
Future<void> immediate(dynamic Function() action) async {
Completer<void> completer = Completer();
_RohdSim.registerImmediateAction(() async {
await action();
completer.complete();
});
await completer.future;
}
}
This preserves the ability to do things like
simulator.run((simulator) async {
Future.delayed( tickTime(10) , () async {
a();
await Future.delayed( tickTime( 10 ) );
b();
});
Future.delayed( tickTime(11) , () async {
c();
await Future.delayed( tickTime( 10 ) );
d();
});
});
in Rohme, but also provides the ability to do "genuinely asynchronous" communication in zero simulation time, as shown above.
Testing
Two new test in test/simulator_test.dart.
Backwards-compatibility
No issues expected. All existing tests pass and API changes are incremental.
Documentation
Documentation is inline.
@AdamRose66 can you help me understand the difference in functionality and use case between this new registerImmediateAction and injectAction?
@AdamRose66 can you help me understand the difference in functionality and use case between this new
registerImmediateActionandinjectAction?
I tend to think in terms of delta cycles:
- An immediate action is implemented in this delta cycle. If we await it, neither time nor delta cycle has advanced. This gives us the ability to consume ( await ) actual wall clock time without consuming simulation time.
- injectAction schedules an action on the boundary between one delta cycle and the next. All the processes scheduled for this delta ( and any immediate actions that they themselves may have scheduled ) have completed before the first injected action starts. This might be used to tidy up or summarise everything that happened in the previous delta, or otherwise prepare for the next delta knowing for certain that the previous delta has finished. For example, we can trigger "value changed" events in here, confident that no further changes will take place in this delta.
- And finally, registerAction( currentTime ) schedules an action for the next delta cycle, ie, at the same time but on the other side of the injectedAction boundary.
The name of this test describes it quite well:
test(
'immediate action occurs at same time, before injected'
' and before next delta',
The test deliberately registers the events in the opposite order to the one in which they will occur:
// next delta
Simulator.registerAction(100, () {
expect(Simulator.time, 100);
testLog.add('delta');
});
// next microtask ( end of this delta )
Simulator.injectAction(() {
expect(Simulator.time, 100);
testLog.add('injected');
});
// immediate
Simulator.registerImmediateAction(() {
expect(Simulator.time, 100);
testLog.add('immediate');
});
And then the test checks the correct ordering:
final List<String> expectedLog = [
...
'immediate',
'injected',
'delta',
...
];
Really only injectAction and registerAction are actual modelling constructs. registerImmediate is a way to await "genuine time consuming functions" without advancing simulation time or the delta cycle. It's a kind of temporary opt out from the simulator.
So in the Rohme code:
simulator.run((simulator) async {
Future.delayed(tickTime(5), () async {
expect(simulator.elapsedTicks, 5);
// subscribe(.) consumes actual wall clock time, needed to receive the entire stream
// but does not consume simulation time.
await simulator.immediate(() => subscribe(publisher) );
expect(simulator.elapsedTicks, 5);
});
Future.delayed(tickTime(10), () async {
expect(simulator.elapsedTicks, 10);
});
});
subscribe() consumes actual wall clock time ( it reads the whole of an async* stream where the data are separated by a quarter of a second of real, wall clock time ) but "await simulator.immediate(() => subscribe(publisher) );" consumes no simulation time.
Doesn't injectAction offer the same capabilities? A function that does not return a Future passed to injectAction will not block progress on the Simulator, and one that does return a Future will block progress.
Possibly !
But after the await, you would be in the shadow land between one delta cycle and the next.
I will experiment with that.
You're right. injectAction( action ) works. It also works if we schedule it in the next delta cycle using registerAction( _RohdSimulator.time , action ). So no modifications to the Rohd simulator are needed.