intel / rohd

The Rapid Open Hardware Development (ROHD) framework is a framework for describing and verifying hardware in the Dart programming language.
https://intel.github.io/rohd-website
BSD 3-Clause "New" or "Revised" License
370 stars 65 forks source link

Allow immediate scheduling of an action within the same delta cycle #472

Closed AdamRose66 closed 6 months ago

AdamRose66 commented 6 months ago

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.

mkorbel1 commented 6 months ago

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

AdamRose66 commented 6 months ago

@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:

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 commented 6 months ago

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.

mkorbel1 commented 6 months ago

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.

AdamRose66 commented 6 months ago

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 commented 6 months ago

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.