rohd icon indicating copy to clipboard operation
rohd copied to clipboard

Allow immediate scheduling of an action within the same delta cycle

Open AdamRose66 opened this issue 1 year ago • 5 comments

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 avatar Mar 04 '24 15:03 AdamRose66

@AdamRose66 can you help me understand the difference in functionality and use case between this new registerImmediateAction and injectAction?

mkorbel1 avatar Mar 05 '24 20:03 mkorbel1

@AdamRose66 can you help me understand the difference in functionality and use case between this new registerImmediateAction and injectAction?

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',
        ...
      ];

AdamRose66 avatar Mar 05 '24 22:03 AdamRose66

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.

AdamRose66 avatar Mar 05 '24 22:03 AdamRose66

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.

mkorbel1 avatar Mar 05 '24 22:03 mkorbel1

Possibly !

But after the await, you would be in the shadow land between one delta cycle and the next.

I will experiment with that.

AdamRose66 avatar Mar 05 '24 23:03 AdamRose66

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.

AdamRose66 avatar Mar 06 '24 08:03 AdamRose66