From 1109fb44e70d2f83e724b69bd8b161c5b8d3f489 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Sat, 25 Jan 2025 09:51:35 -0300 Subject: [PATCH 01/10] Methods for scheduling new events have been added to the EventContext. (#6) --- README.md | 2 +- example/example.dart | 2 +- lib/simdart.dart | 8 +-- lib/src/event.dart | 7 +- lib/src/internal/event_action.dart | 65 +++++++++++++++---- .../internal/event_scheduler_interface.dart | 52 +++++++++++++++ lib/src/internal/now_interface.dart | 7 ++ lib/src/internal/time_loop.dart | 4 +- ...op_mixin.dart => time_loop_interface.dart} | 6 +- lib/src/simdart.dart | 36 ++-------- test/process_test.dart | 9 +-- 11 files changed, 132 insertions(+), 66 deletions(-) create mode 100644 lib/src/internal/event_scheduler_interface.dart create mode 100644 lib/src/internal/now_interface.dart rename lib/src/internal/{time_loop_mixin.dart => time_loop_interface.dart} (92%) diff --git a/README.md b/README.md index f5d9e3a..d6a679c 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ void main() async { void _a(EventContext context) async { await context.wait(10); - context.sim.process(event: _c, delay: 1, name: 'C'); + context.process(event: _c, delay: 1, name: 'C'); } void _b(EventContext context) async { diff --git a/example/example.dart b/example/example.dart index 52169d6..c0e5d93 100644 --- a/example/example.dart +++ b/example/example.dart @@ -10,5 +10,5 @@ void main() async { void _a(EventContext context) async { await context.wait(2); - context.sim.process(event: _a, delay: 2, name: 'A'); + context.process(event: _a, delay: 2, name: 'A'); } diff --git a/lib/simdart.dart b/lib/simdart.dart index beb761e..f5bc856 100644 --- a/lib/simdart.dart +++ b/lib/simdart.dart @@ -1,8 +1,8 @@ -export 'src/start_time_handling.dart'; -export 'src/simdart.dart' hide SimDartHelper; export 'src/event.dart'; -export 'src/simulation_track.dart'; +export 'src/execution_priority.dart'; export 'src/interval.dart'; export 'src/observable.dart'; export 'src/resource_configurator.dart' hide ResourcesConfiguratorHelper; -export 'src/execution_priority.dart'; +export 'src/simdart.dart' hide SimDartHelper; +export 'src/simulation_track.dart'; +export 'src/start_time_handling.dart'; diff --git a/lib/src/event.dart b/lib/src/event.dart index fa3cb59..6363d6d 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:simdart/src/simdart.dart'; +import 'package:simdart/src/internal/event_scheduler_interface.dart'; /// The event to be executed. /// @@ -12,10 +12,7 @@ typedef Event = void Function(EventContext context); /// /// Encapsulates the information and state of an event being executed /// within the simulation. -mixin EventContext { - /// The simulation instance managing this event. - SimDart get sim; - +abstract interface class EventContext implements EventSchedulerInterface { /// Pauses the execution of the event for the specified [delay] in simulation time. /// /// The event is re-added to the simulation's event queue and will resume after diff --git a/lib/src/internal/event_action.dart b/lib/src/internal/event_action.dart index a30f20c..0fb9bb0 100644 --- a/lib/src/internal/event_action.dart +++ b/lib/src/internal/event_action.dart @@ -2,15 +2,16 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; -import 'package:simdart/src/internal/time_action.dart'; import 'package:simdart/src/internal/resource.dart'; +import 'package:simdart/src/internal/time_action.dart'; +import 'package:simdart/src/interval.dart'; import 'package:simdart/src/simdart.dart'; import 'package:simdart/src/simulation_track.dart'; @internal -class EventAction extends TimeAction with EventContext { +class EventAction extends TimeAction implements EventContext { EventAction( - {required this.sim, + {required SimDart sim, required super.start, required String? eventName, required this.event, @@ -18,7 +19,8 @@ class EventAction extends TimeAction with EventContext { required this.onTrack, required this.onReject, required this.secondarySortByName}) - : _eventName = eventName; + : _sim = sim, + _eventName = eventName; /// The name of the event. final String? _eventName; @@ -34,8 +36,7 @@ class EventAction extends TimeAction with EventContext { final Function? onReject; - @override - final SimDart sim; + final SimDart _sim; /// The resource id required by the event. final String? resourceId; @@ -64,7 +65,7 @@ class EventAction extends TimeAction with EventContext { if (resume != null) { if (onTrack != null) { onTrack!(SimDartHelper.buildSimulationTrack( - sim: sim, eventName: eventName, status: Status.resumed)); + sim: _sim, eventName: eventName, status: Status.resumed)); } // Resume the event if it is waiting, otherwise execute its action. resume.call(); @@ -72,7 +73,7 @@ class EventAction extends TimeAction with EventContext { } Resource? resource = - SimDartHelper.getResource(sim: sim, resourceId: resourceId); + SimDartHelper.getResource(sim: _sim, resourceId: resourceId); if (resource != null) { _resourceAcquired = resource.acquire(this); } @@ -83,7 +84,7 @@ class EventAction extends TimeAction with EventContext { status = Status.rejected; } onTrack!(SimDartHelper.buildSimulationTrack( - sim: sim, eventName: eventName, status: status)); + sim: _sim, eventName: eventName, status: status)); } if (_canRun) { @@ -94,12 +95,12 @@ class EventAction extends TimeAction with EventContext { _resourceAcquired = false; } // Event released some resource, others events need retry. - SimDartHelper.restoreWaitingEventsForResource(sim: sim); + SimDartHelper.restoreWaitingEventsForResource(sim: _sim); } }); } else { onReject?.call(); - SimDartHelper.queueOnWaitingForResource(sim: sim, action: this); + SimDartHelper.queueOnWaitingForResource(sim: _sim, action: this); } } @@ -109,9 +110,9 @@ class EventAction extends TimeAction with EventContext { return; } - start = sim.now + delay; + start = _sim.now + delay; // Adds it back to the loop to be resumed in the future. - SimDartHelper.addAction(sim: sim, action: this); + SimDartHelper.addAction(sim: _sim, action: this); final Completer resume = Completer(); _resume = () { @@ -124,4 +125,42 @@ class EventAction extends TimeAction with EventContext { Future _runEvent() async { return event(this); } + + @override + int get now => _sim.now; + + @override + void process( + {required Event event, + String? resourceId, + String? name, + int? start, + int? delay}) { + _sim.process( + event: event, + resourceId: resourceId, + name: name, + start: start, + delay: delay); + } + + @override + void repeatProcess( + {required Event event, + int? start, + int? delay, + required Interval interval, + RejectedEventPolicy rejectedEventPolicy = + RejectedEventPolicy.keepRepeating, + String? resourceId, + String? name}) { + _sim.repeatProcess( + event: event, + interval: interval, + start: start, + delay: delay, + rejectedEventPolicy: rejectedEventPolicy, + resourceId: resourceId, + name: name); + } } diff --git a/lib/src/internal/event_scheduler_interface.dart b/lib/src/internal/event_scheduler_interface.dart new file mode 100644 index 0000000..ef5a644 --- /dev/null +++ b/lib/src/internal/event_scheduler_interface.dart @@ -0,0 +1,52 @@ +import 'package:meta/meta.dart'; +import 'package:simdart/src/event.dart'; +import 'package:simdart/src/internal/now_interface.dart'; +import 'package:simdart/src/interval.dart'; +import 'package:simdart/src/simdart.dart'; + +@internal +abstract interface class EventSchedulerInterface implements NowInterface { + /// Schedules a new event to occur repeatedly based on the specified interval configuration. + /// + /// [event] is the function that represents the action to be executed when the event occurs. + /// [start] is the absolute time at which the event should occur. If null, the event will + /// occur at the [now] simulation time. + /// [delay] is the number of time units after the [now] when the event has been scheduled. + /// It cannot be provided if [start] is specified. + /// [interval] defines the timing configuration for the event, including its start time and + /// the interval between repetitions. The specific details of the interval behavior depend + /// on the implementation of the [Interval]. + /// [resourceId] is an optional parameter that specifies the ID of the resource required by the event. + /// [name] is an optional identifier for the event. + /// [rejectedEventPolicy] defines the behavior of the interval after a newly created event has been rejected. + /// + /// Throws an [ArgumentError] if the provided interval configuration is invalid, such as + /// containing negative or inconsistent timing values. + void repeatProcess( + {required Event event, + int? start, + int? delay, + required Interval interval, + RejectedEventPolicy rejectedEventPolicy = + RejectedEventPolicy.keepRepeating, + String? resourceId, + String? name}); + + /// Schedules a new event to occur at a specific simulation time or after a delay. + /// + /// [event] is the function that represents the action to be executed when the event occurs. + /// [start] is the absolute time at which the event should occur. If null, the event will + /// occur at the [now] simulation time. + /// [delay] is the number of time units after the [now] when the event has been scheduled. + /// It cannot be provided if [start] is specified. + /// [resourceId] is an optional parameter that specifies the ID of the resource required by the event. + /// [name] is an optional identifier for the event. + /// + /// Throws an [ArgumentError] if both [start] and [delay] are provided or if [delay] is negative. + void process( + {required Event event, + String? resourceId, + String? name, + int? start, + int? delay}); +} diff --git a/lib/src/internal/now_interface.dart b/lib/src/internal/now_interface.dart new file mode 100644 index 0000000..b8ed185 --- /dev/null +++ b/lib/src/internal/now_interface.dart @@ -0,0 +1,7 @@ +import 'package:meta/meta.dart'; + +@internal +abstract interface class NowInterface { + /// Gets the current simulation time. + int get now; +} diff --git a/lib/src/internal/time_loop.dart b/lib/src/internal/time_loop.dart index 6ed9578..acdc90e 100644 --- a/lib/src/internal/time_loop.dart +++ b/lib/src/internal/time_loop.dart @@ -4,12 +4,12 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:simdart/src/execution_priority.dart'; import 'package:simdart/src/internal/time_action.dart'; -import 'package:simdart/src/internal/time_loop_mixin.dart'; +import 'package:simdart/src/internal/time_loop_interface.dart'; import 'package:simdart/src/start_time_handling.dart'; /// Represents the temporal loop in the algorithm, managing the execution of actions at specified times. @internal -class TimeLoop with TimeLoopMixin { +class TimeLoop implements TimeLoopInterface { TimeLoop( {required int? now, required this.beforeRun, diff --git a/lib/src/internal/time_loop_mixin.dart b/lib/src/internal/time_loop_interface.dart similarity index 92% rename from lib/src/internal/time_loop_mixin.dart rename to lib/src/internal/time_loop_interface.dart index a2d1fc7..ea88397 100644 --- a/lib/src/internal/time_loop_mixin.dart +++ b/lib/src/internal/time_loop_interface.dart @@ -1,10 +1,11 @@ import 'package:meta/meta.dart'; import 'package:simdart/src/execution_priority.dart'; +import 'package:simdart/src/internal/now_interface.dart'; import 'package:simdart/src/start_time_handling.dart'; /// Represents the temporal loop in the algorithm, managing the execution of actions at specified times. @internal -mixin TimeLoopMixin { +abstract interface class TimeLoopInterface implements NowInterface { /// Specifies how the simulation handles start times in the past. StartTimeHandling get startTimeHandling; @@ -32,7 +33,4 @@ mixin TimeLoopMixin { /// /// The value will be `null` if the duration has not been calculated or set. int? get duration; - - /// Gets the current simulation time. - int get now; } diff --git a/lib/src/simdart.dart b/lib/src/simdart.dart index 24cd4d4..b224dc3 100644 --- a/lib/src/simdart.dart +++ b/lib/src/simdart.dart @@ -6,18 +6,19 @@ import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; import 'package:simdart/src/execution_priority.dart'; import 'package:simdart/src/internal/event_action.dart'; +import 'package:simdart/src/internal/event_scheduler_interface.dart'; import 'package:simdart/src/internal/repeat_event_action.dart'; +import 'package:simdart/src/internal/resource.dart'; import 'package:simdart/src/internal/time_action.dart'; import 'package:simdart/src/internal/time_loop.dart'; -import 'package:simdart/src/internal/time_loop_mixin.dart'; +import 'package:simdart/src/internal/time_loop_interface.dart'; import 'package:simdart/src/interval.dart'; -import 'package:simdart/src/internal/resource.dart'; import 'package:simdart/src/resource_configurator.dart'; import 'package:simdart/src/simulation_track.dart'; import 'package:simdart/src/start_time_handling.dart'; /// Represents a discrete-event simulation engine. -class SimDart with TimeLoopMixin { +class SimDart implements TimeLoopInterface, EventSchedulerInterface { /// Creates a simulation instance. /// /// - [now]: The starting time of the simulation. Defaults to `0` if null. @@ -104,22 +105,7 @@ class SimDart with TimeLoopMixin { return _loop.run(until: until); } - /// Schedules a new event to occur repeatedly based on the specified interval configuration. - /// - /// [event] is the function that represents the action to be executed when the event occurs. - /// [start] is the absolute time at which the event should occur. If null, the event will - /// occur at the [now] simulation time. - /// [delay] is the number of time units after the [now] when the event has been scheduled. - /// It cannot be provided if [start] is specified. - /// [interval] defines the timing configuration for the event, including its start time and - /// the interval between repetitions. The specific details of the interval behavior depend - /// on the implementation of the [Interval]. - /// [resourceId] is an optional parameter that specifies the ID of the resource required by the event. - /// [name] is an optional identifier for the event. - /// [rejectedEventPolicy] defines the behavior of the interval after a newly created event has been rejected. - /// - /// Throws an [ArgumentError] if the provided interval configuration is invalid, such as - /// containing negative or inconsistent timing values. + @override void repeatProcess( {required Event event, int? start, @@ -140,17 +126,7 @@ class SimDart with TimeLoopMixin { rejectedEventPolicy: rejectedEventPolicy); } - /// Schedules a new event to occur at a specific simulation time or after a delay. - /// - /// [event] is the function that represents the action to be executed when the event occurs. - /// [start] is the absolute time at which the event should occur. If null, the event will - /// occur at the [now] simulation time. - /// [delay] is the number of time units after the [now] when the event has been scheduled. - /// It cannot be provided if [start] is specified. - /// [resourceId] is an optional parameter that specifies the ID of the resource required by the event. - /// [name] is an optional identifier for the event. - /// - /// Throws an [ArgumentError] if both [start] and [delay] are provided or if [delay] is negative. + @override void process( {required Event event, String? resourceId, diff --git a/test/process_test.dart b/test/process_test.dart index e375ac9..8562dbd 100644 --- a/test/process_test.dart +++ b/test/process_test.dart @@ -34,8 +34,7 @@ void main() { helper = TestHelper(); helper.sim.process( event: (context) async { - context.sim - .process(event: TestHelper.emptyEvent, delay: 10, name: 'b'); + context.process(event: TestHelper.emptyEvent, delay: 10, name: 'b'); }, start: 5, name: 'a'); @@ -47,15 +46,13 @@ void main() { helper = TestHelper(); helper.sim.process( event: (context) async { - context.sim - .process(event: TestHelper.emptyEvent, delay: 10, name: 'b'); + context.process(event: TestHelper.emptyEvent, delay: 10, name: 'b'); }, start: 0, name: 'a'); helper.sim.process( event: (context) async { - context.sim - .process(event: TestHelper.emptyEvent, delay: 2, name: 'd'); + context.process(event: TestHelper.emptyEvent, delay: 2, name: 'd'); }, start: 2, name: 'c'); From 1db504e52558787d7a1d9cc8edd3ffff1d4f6606 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Sat, 25 Jan 2025 18:25:17 -0300 Subject: [PATCH 02/10] Class for simulation result. Tracks were added to the result and no longer as a listener. --- example/example.dart | 9 +- lib/src/internal/event_action.dart | 19 ++-- .../internal/sim_configuration_interface.dart | 30 ++++++ ...terface.dart => sim_result_interface.dart} | 17 +-- lib/src/internal/time_loop.dart | 30 +++++- lib/src/sim_result.dart | 20 ++++ lib/src/simdart.dart | 40 +++---- test/process_test.dart | 93 ++++++++-------- test/repeat_process_test.dart | 92 ++++++++-------- test/resource_test.dart | 102 ++++++++++-------- test/test_helper.dart | 33 ------ test/time_loop_test.dart | 1 + test/track_tester.dart | 19 ++++ test/wait_test.dart | 90 +++++++++------- 14 files changed, 333 insertions(+), 262 deletions(-) create mode 100644 lib/src/internal/sim_configuration_interface.dart rename lib/src/internal/{time_loop_interface.dart => sim_result_interface.dart} (53%) create mode 100644 lib/src/sim_result.dart delete mode 100644 test/test_helper.dart create mode 100644 test/track_tester.dart diff --git a/example/example.dart b/example/example.dart index c0e5d93..1676426 100644 --- a/example/example.dart +++ b/example/example.dart @@ -1,11 +1,16 @@ import 'package:simdart/simdart.dart'; +import 'package:simdart/src/sim_result.dart'; void main() async { - final SimDart sim = SimDart(onTrack: (track) => print(track)); + final SimDart sim = SimDart(includeTracks: true); sim.process(event: _a, name: 'A'); - await sim.run(until: 10); + SimResult result = await sim.run(until: 10); + + result.tracks?.forEach((track) => print(track)); + print('startTime: ${result.startTime}'); + print('duration: ${result.duration}'); } void _a(EventContext context) async { diff --git a/lib/src/internal/event_action.dart b/lib/src/internal/event_action.dart index 0fb9bb0..37edd0c 100644 --- a/lib/src/internal/event_action.dart +++ b/lib/src/internal/event_action.dart @@ -16,7 +16,6 @@ class EventAction extends TimeAction implements EventContext { required String? eventName, required this.event, required this.resourceId, - required this.onTrack, required this.onReject, required this.secondarySortByName}) : _sim = sim, @@ -26,10 +25,6 @@ class EventAction extends TimeAction implements EventContext { final String? _eventName; String get eventName => _eventName ?? hashCode.toString(); - /// A callback function used to track the progress of the simulation. - /// If provided, this function will be called with each [SimulationTrack] generated - /// during the simulation. This is useful for debugging or logging purposes. - final OnTrack? onTrack; /// The event to be executed. final Event event; @@ -62,10 +57,12 @@ class EventAction extends TimeAction implements EventContext { void execute() { final Function()? resume = _resume; + + if (resume != null) { - if (onTrack != null) { - onTrack!(SimDartHelper.buildSimulationTrack( - sim: _sim, eventName: eventName, status: Status.resumed)); + if (_sim.includeTracks) { + SimDartHelper.addSimulationTrack( + sim: _sim, eventName: eventName, status: Status.resumed); } // Resume the event if it is waiting, otherwise execute its action. resume.call(); @@ -78,13 +75,13 @@ class EventAction extends TimeAction implements EventContext { _resourceAcquired = resource.acquire(this); } - if (onTrack != null) { + if (_sim.includeTracks) { Status status = Status.executed; if (!_canRun) { status = Status.rejected; } - onTrack!(SimDartHelper.buildSimulationTrack( - sim: _sim, eventName: eventName, status: status)); + SimDartHelper.addSimulationTrack( + sim: _sim, eventName: eventName, status: status); } if (_canRun) { diff --git a/lib/src/internal/sim_configuration_interface.dart b/lib/src/internal/sim_configuration_interface.dart new file mode 100644 index 0000000..d996c4d --- /dev/null +++ b/lib/src/internal/sim_configuration_interface.dart @@ -0,0 +1,30 @@ +import 'package:meta/meta.dart'; +import 'package:simdart/src/execution_priority.dart'; +import 'package:simdart/src/start_time_handling.dart'; + + +@internal +abstract interface class SimConfigurationInterface { + /// Specifies how the simulation handles start times in the past. + StartTimeHandling get startTimeHandling; + + /// Defines the priority of task execution in the simulation. + /// + /// - `highPriority`: Uses `Future.microtask` for immediate execution, prioritizing + /// processing without blocking the UI. + /// - `lowPriority`: Uses `Future.delayed(Duration.zero)` to ensure non-blocking + /// execution, allowing the UI to remain responsive. + ExecutionPriority get executionPriority; + + /// Determines whether simulation tracks should be included in the simulation result. + /// + /// When set to `true`, the simulation will collect and return a list of [SimulationTrack] + /// objects as part of its result. If set to `false`, the tracks will not be collected, + /// and the list will be `null`. + /// + /// Default: `false` + bool get includeTracks; + + + +} diff --git a/lib/src/internal/time_loop_interface.dart b/lib/src/internal/sim_result_interface.dart similarity index 53% rename from lib/src/internal/time_loop_interface.dart rename to lib/src/internal/sim_result_interface.dart index ea88397..56ce86f 100644 --- a/lib/src/internal/time_loop_interface.dart +++ b/lib/src/internal/sim_result_interface.dart @@ -1,21 +1,8 @@ import 'package:meta/meta.dart'; -import 'package:simdart/src/execution_priority.dart'; -import 'package:simdart/src/internal/now_interface.dart'; -import 'package:simdart/src/start_time_handling.dart'; -/// Represents the temporal loop in the algorithm, managing the execution of actions at specified times. +/// Represents the simulation result. @internal -abstract interface class TimeLoopInterface implements NowInterface { - /// Specifies how the simulation handles start times in the past. - StartTimeHandling get startTimeHandling; - - /// Defines the priority of task execution in the simulation. - /// - /// - `highPriority`: Uses `Future.microtask` for immediate execution, prioritizing - /// processing without blocking the UI. - /// - `lowPriority`: Uses `Future.delayed(Duration.zero)` to ensure non-blocking - /// execution, allowing the UI to remain responsive. - ExecutionPriority get executionPriority; +abstract interface class SimResultInterface { /// The time, in simulated time units, when the simulation started. /// This is the moment at which the first event is scheduled to be processed. diff --git a/lib/src/internal/time_loop.dart b/lib/src/internal/time_loop.dart index acdc90e..ef2f34a 100644 --- a/lib/src/internal/time_loop.dart +++ b/lib/src/internal/time_loop.dart @@ -3,16 +3,21 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:simdart/src/execution_priority.dart'; +import 'package:simdart/src/internal/now_interface.dart'; +import 'package:simdart/src/internal/sim_result_interface.dart'; import 'package:simdart/src/internal/time_action.dart'; -import 'package:simdart/src/internal/time_loop_interface.dart'; +import 'package:simdart/src/internal/sim_configuration_interface.dart'; +import 'package:simdart/src/sim_result.dart'; +import 'package:simdart/src/simulation_track.dart'; import 'package:simdart/src/start_time_handling.dart'; /// Represents the temporal loop in the algorithm, managing the execution of actions at specified times. @internal -class TimeLoop implements TimeLoopInterface { +class TimeLoop implements SimConfigurationInterface,NowInterface,SimResultInterface { TimeLoop( {required int? now, required this.beforeRun, + required this.includeTracks, required this.executionPriority, required this.startTimeHandling}) { _now = now ?? 0; @@ -29,6 +34,9 @@ class TimeLoop implements TimeLoopInterface { final Function beforeRun; + @override + final bool includeTracks; + /// Queue that holds the [TimeAction] instances to be executed at their respective times. final PriorityQueue _actions = PriorityQueue( (a, b) { @@ -58,22 +66,24 @@ class TimeLoop implements TimeLoopInterface { int get now => _now; late int _now; + List? _tracks; + Completer? _terminator; /// Runs the simulation, processing actions in chronological order. - Future run({int? until}) async { + Future run({int? until}) async { if (until != null && _now > until) { throw ArgumentError('`now` must be less than or equal to `until`.'); } _until = until; if (_terminator != null) { - return; + return _buildResult(); } if (_actions.isEmpty) { _duration = 0; _startTime = 0; - return; + return _buildResult(); } _duration = null; _startTime = null; @@ -85,6 +95,11 @@ class TimeLoop implements TimeLoopInterface { await _terminator?.future; _duration = _now - (startTime ?? 0); _terminator = null; + return _buildResult(); + } + + SimResult _buildResult(){ + return SimResult(startTime: startTime, duration: duration, tracks: _tracks); } void _scheduleNextEvent() { @@ -105,6 +120,11 @@ class TimeLoop implements TimeLoopInterface { _actions.add(action); } + void addTrack(SimulationTrack track){ + _tracks ??= []; + _tracks!.add(track); + } + Future _consumeFirstEvent() async { _nextEventScheduled = false; if (_actions.isEmpty) { diff --git a/lib/src/sim_result.dart b/lib/src/sim_result.dart new file mode 100644 index 0000000..52cd3a9 --- /dev/null +++ b/lib/src/sim_result.dart @@ -0,0 +1,20 @@ +import 'dart:collection'; + +import 'package:simdart/src/internal/sim_result_interface.dart'; +import 'package:simdart/src/simulation_track.dart'; + +class SimResult implements SimResultInterface { + SimResult( + {required this.duration, + required this.startTime, + required List? tracks}) + : tracks = tracks != null ? UnmodifiableListView(tracks) : null; + + @override + final int? duration; + + @override + final int? startTime; + + final UnmodifiableListView? tracks; +} diff --git a/lib/src/simdart.dart b/lib/src/simdart.dart index b224dc3..bfba4f1 100644 --- a/lib/src/simdart.dart +++ b/lib/src/simdart.dart @@ -7,18 +7,20 @@ import 'package:simdart/src/event.dart'; import 'package:simdart/src/execution_priority.dart'; import 'package:simdart/src/internal/event_action.dart'; import 'package:simdart/src/internal/event_scheduler_interface.dart'; +import 'package:simdart/src/internal/now_interface.dart'; import 'package:simdart/src/internal/repeat_event_action.dart'; import 'package:simdart/src/internal/resource.dart'; import 'package:simdart/src/internal/time_action.dart'; import 'package:simdart/src/internal/time_loop.dart'; -import 'package:simdart/src/internal/time_loop_interface.dart'; +import 'package:simdart/src/internal/sim_configuration_interface.dart'; import 'package:simdart/src/interval.dart'; import 'package:simdart/src/resource_configurator.dart'; +import 'package:simdart/src/sim_result.dart'; import 'package:simdart/src/simulation_track.dart'; import 'package:simdart/src/start_time_handling.dart'; /// Represents a discrete-event simulation engine. -class SimDart implements TimeLoopInterface, EventSchedulerInterface { +class SimDart implements SimConfigurationInterface, EventSchedulerInterface,NowInterface { /// Creates a simulation instance. /// /// - [now]: The starting time of the simulation. Defaults to `0` if null. @@ -27,8 +29,7 @@ class SimDart implements TimeLoopInterface, EventSchedulerInterface { /// - [startTimeHandling]: Determines how to handle events scheduled with a start /// time in the past. The default behavior is [StartTimeHandling.throwErrorIfPast]. /// - /// - [onTrack]: The optional callback function that can be used to track the progress - /// of the simulation. + /// - [includeTracks]: Determines whether simulation tracks should be included in the simulation result. /// /// - [seed]: The optional parameter used to initialize the random number generator /// for deterministic behavior in the simulation. If provided, it ensures that the @@ -40,15 +41,15 @@ class SimDart implements TimeLoopInterface, EventSchedulerInterface { /// - [executionPriority]: Defines the priority of task execution in the simulation. SimDart( {StartTimeHandling startTimeHandling = StartTimeHandling.throwErrorIfPast, - OnTrack? onTrack, int? now, this.secondarySortByName = false, + this.includeTracks=false, ExecutionPriority executionPriority = ExecutionPriority.high, int? seed}) - : _onTrack = onTrack, - random = Random(seed) { + : random = Random(seed) { _loop = TimeLoop( now: now, + includeTracks: includeTracks, beforeRun: _beforeRun, executionPriority: executionPriority, startTimeHandling: startTimeHandling); @@ -56,6 +57,9 @@ class SimDart implements TimeLoopInterface, EventSchedulerInterface { late final TimeLoop _loop; + @override + final bool includeTracks; + /// Determines whether events with the same start time are sorted by their event name. /// /// The primary sorting criterion is always the simulated start time (`start`). If @@ -77,11 +81,6 @@ class SimDart implements TimeLoopInterface, EventSchedulerInterface { /// instantiate a new `Random` object for each event. late final Random random; - /// A callback function used to track the progress of the simulation. - /// If provided, this function will be called with each [SimulationTrack] generated - /// during the simulation. This is useful for debugging or logging purposes. - final OnTrack? _onTrack; - /// A queue that holds event actions that are waiting for a resource to become available. /// /// These events were initially denied the resource and are placed in this queue @@ -101,7 +100,7 @@ class SimDart implements TimeLoopInterface, EventSchedulerInterface { /// /// - [until]: The time at which execution should stop. Execution will include events /// scheduled at this time (inclusive). If null, execution will continue indefinitely. - Future run({int? until}) async { + Future run({int? until}) async { return _loop.run(until: until); } @@ -190,7 +189,6 @@ class SimDart implements TimeLoopInterface, EventSchedulerInterface { } else { _loop.addAction(EventAction( sim: this, - onTrack: _onTrack, start: start, eventName: name, event: event, @@ -200,25 +198,17 @@ class SimDart implements TimeLoopInterface, EventSchedulerInterface { } } - @override - int? get duration => _loop.duration; - @override ExecutionPriority get executionPriority => _loop.executionPriority; @override int get now => _loop.now; - @override - int? get startTime => _loop.startTime; @override StartTimeHandling get startTimeHandling => _loop.startTimeHandling; } -/// A function signature for tracking the progress of a simulation. -typedef OnTrack = void Function(SimulationTrack track); - /// Defines the behavior of the interval after a newly created event has been rejected. enum RejectedEventPolicy { /// Continues the repetition of the event at the specified intervals, even after the event was rejected. @@ -275,7 +265,7 @@ class SimDartHelper { return sim._resources[resourceId]; } - static SimulationTrack buildSimulationTrack( + static void addSimulationTrack( {required SimDart sim, required String eventName, required Status status}) { @@ -283,10 +273,10 @@ class SimDartHelper { for (Resource resource in sim._resources.values) { resourceUsage[resource.id] = resource.queue.length; } - return SimulationTrack( + sim._loop.addTrack(SimulationTrack( status: status, name: eventName, time: sim.now, - resourceUsage: resourceUsage); + resourceUsage: resourceUsage)); } } diff --git a/test/process_test.dart b/test/process_test.dart index 8562dbd..a919150 100644 --- a/test/process_test.dart +++ b/test/process_test.dart @@ -1,67 +1,74 @@ import 'package:simdart/simdart.dart'; +import 'package:simdart/src/sim_result.dart'; import 'package:test/test.dart'; -import 'test_helper.dart'; +import 'track_tester.dart'; + +Future emptyEvent(EventContext context) async {} void main() { group('Process', () { test('start', () async { - TestHelper helper = TestHelper(); - helper.sim.process(event: TestHelper.emptyEvent, name: 'a'); - await helper.sim.run(); - expect(helper.trackList.length, 1); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 0); + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process(event: emptyEvent, name: 'a'); + SimResult result = await sim.run(); + TrackTester tt = TrackTester(result); + tt.test(["[0][a][executed]"]); - helper = TestHelper(); - helper.sim.process(event: TestHelper.emptyEvent, start: 10, name: 'a'); - await helper.sim.run(); - expect(helper.trackList.length, 1); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 10); + sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process(event: emptyEvent, start: 10, name: 'a'); + result = await sim.run(); + tt = TrackTester(result); + tt.test(["[10][a][executed]"]); }); - test('delay', () async { - TestHelper helper = TestHelper(); - helper.sim.process(event: TestHelper.emptyEvent, delay: 0, name: 'a'); - await helper.sim.run(); - expect(helper.trackList.length, 1); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 0); - - helper = TestHelper(); - helper.sim.process(event: TestHelper.emptyEvent, delay: 10, name: 'a'); - await helper.sim.run(); - expect(helper.trackList.length, 1); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 10); - - helper = TestHelper(); - helper.sim.process( + test('delay 1', () async { + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process(event: emptyEvent, delay: 0, name: 'a'); + SimResult result = await sim.run(); + TrackTester tt = TrackTester(result); + tt.test(["[0][a][executed]"]); + }); + test('delay 2', () async { + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process(event: emptyEvent, delay: 10, name: 'a'); + SimResult result = await sim.run(); + TrackTester tt = TrackTester(result); + tt.test(["[10][a][executed]"]); + }); + test('delay 3', () async { + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process( event: (context) async { - context.process(event: TestHelper.emptyEvent, delay: 10, name: 'b'); + context.process(event: emptyEvent, delay: 10, name: 'b'); }, start: 5, name: 'a'); - await helper.sim.run(); - expect(helper.trackList.length, 2); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 5); - helper.testTrack(index: 1, name: 'b', status: Status.executed, time: 15); - - helper = TestHelper(); - helper.sim.process( + SimResult result = await sim.run(); + TrackTester tt = TrackTester(result); + tt.test(["[5][a][executed]", "[15][b][executed]"]); + }); + test('delay 4', () async { + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process( event: (context) async { - context.process(event: TestHelper.emptyEvent, delay: 10, name: 'b'); + context.process(event: emptyEvent, delay: 10, name: 'b'); }, start: 0, name: 'a'); - helper.sim.process( + sim.process( event: (context) async { - context.process(event: TestHelper.emptyEvent, delay: 2, name: 'd'); + context.process(event: emptyEvent, delay: 2, name: 'd'); }, start: 2, name: 'c'); - await helper.sim.run(); - expect(helper.trackList.length, 4); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'c', status: Status.executed, time: 2); - helper.testTrack(index: 2, name: 'd', status: Status.executed, time: 4); - helper.testTrack(index: 3, name: 'b', status: Status.executed, time: 10); + SimResult result = await sim.run(); + TrackTester tt = TrackTester(result); + tt.test([ + "[0][a][executed]", + "[2][c][executed]", + "[4][d][executed]", + "[10][b][executed]" + ]); }); }); } diff --git a/test/repeat_process_test.dart b/test/repeat_process_test.dart index e2bb108..226e53c 100644 --- a/test/repeat_process_test.dart +++ b/test/repeat_process_test.dart @@ -1,51 +1,53 @@ import 'package:simdart/simdart.dart'; +import 'package:simdart/src/sim_result.dart'; import 'package:test/test.dart'; -import 'test_helper.dart'; +import 'track_tester.dart'; void main() { group('Repeat process', () { test('Simple', () async { - TestHelper helper = TestHelper(); + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); - helper.sim.repeatProcess( + sim.repeatProcess( event: (context) {}, name: 'A', interval: Interval.fixed(fixedInterval: 1, untilTime: 2)); - await helper.sim.run(); - expect(helper.trackList.length, 3); - helper.testTrack(index: 0, name: 'A', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'A', status: Status.executed, time: 1); - helper.testTrack(index: 2, name: 'A', status: Status.executed, time: 2); + SimResult result = await sim.run(); + TrackTester tt = TrackTester(result); + tt.test(["[0][A][executed]", "[1][A][executed]", "[2][A][executed]"]); }); test('Wait', () async { - TestHelper helper = TestHelper(); + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); - helper.sim.repeatProcess( + sim.repeatProcess( event: (context) async { await context.wait(1); }, name: 'A', interval: Interval.fixed(fixedInterval: 1, untilTime: 2)); - await helper.sim.run(); - expect(helper.trackList.length, 6); - helper.testTrack(index: 0, name: 'A', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'A', status: Status.resumed, time: 1); - helper.testTrack(index: 2, name: 'A', status: Status.executed, time: 1); - helper.testTrack(index: 3, name: 'A', status: Status.resumed, time: 2); - helper.testTrack(index: 4, name: 'A', status: Status.executed, time: 2); - helper.testTrack(index: 5, name: 'A', status: Status.resumed, time: 3); + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + "[0][A][executed]", + "[1][A][resumed]", + "[1][A][executed]", + "[2][A][resumed]", + "[2][A][executed]", + "[3][A][resumed]" + ]); }); test('Resource - keep', () async { - TestHelper helper = TestHelper(); + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); - helper.sim.resources.limited(id: 'r'); + sim.resources.limited(id: 'r'); - helper.sim.repeatProcess( + sim.repeatProcess( event: (context) async { await context.wait(10); }, @@ -53,25 +55,28 @@ void main() { resourceId: 'r', interval: Interval.fixed(fixedInterval: 1, untilTime: 2)); - await helper.sim.run(); - expect(helper.trackList.length, 9); - helper.testTrack(index: 0, name: 'A', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'A', status: Status.rejected, time: 1); - helper.testTrack(index: 2, name: 'A', status: Status.rejected, time: 2); - helper.testTrack(index: 3, name: 'A', status: Status.resumed, time: 10); - helper.testTrack(index: 4, name: 'A', status: Status.executed, time: 10); - helper.testTrack(index: 5, name: 'A', status: Status.rejected, time: 10); - helper.testTrack(index: 6, name: 'A', status: Status.resumed, time: 20); - helper.testTrack(index: 7, name: 'A', status: Status.executed, time: 20); - helper.testTrack(index: 8, name: 'A', status: Status.resumed, time: 30); + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + "[0][A][executed]", + "[1][A][rejected]", + "[2][A][rejected]", + "[10][A][resumed]", + "[10][A][executed]", + "[10][A][rejected]", + "[20][A][resumed]", + "[20][A][executed]", + "[30][A][resumed]" + ]); }); test('Resource - stop', () async { - TestHelper helper = TestHelper(); + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); - helper.sim.resources.limited(id: 'r'); + sim.resources.limited(id: 'r'); - helper.sim.repeatProcess( + sim.repeatProcess( event: (context) async { await context.wait(2); }, @@ -80,13 +85,16 @@ void main() { interval: Interval.fixed(fixedInterval: 1, untilTime: 50), rejectedEventPolicy: RejectedEventPolicy.stopRepeating); - await helper.sim.run(); - expect(helper.trackList.length, 5); - helper.testTrack(index: 0, name: 'A', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'A', status: Status.rejected, time: 1); - helper.testTrack(index: 2, name: 'A', status: Status.resumed, time: 2); - helper.testTrack(index: 3, name: 'A', status: Status.executed, time: 2); - helper.testTrack(index: 4, name: 'A', status: Status.resumed, time: 4); + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + "[0][A][executed]", + "[1][A][rejected]", + "[2][A][resumed]", + "[2][A][executed]", + "[4][A][resumed]" + ]); }); }); } diff --git a/test/resource_test.dart b/test/resource_test.dart index 31c961b..e4889da 100644 --- a/test/resource_test.dart +++ b/test/resource_test.dart @@ -1,13 +1,14 @@ import 'package:simdart/simdart.dart'; +import 'package:simdart/src/sim_result.dart'; import 'package:test/test.dart'; -import 'test_helper.dart'; +import 'track_tester.dart'; void main() { group('Resource', () { test('Capacity 1', () async { - TestHelper helper = TestHelper(); - helper.sim.resources.limited(id: 'r'); + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.resources.limited(id: 'r'); fA(context) async { await context.wait(1); @@ -21,25 +22,28 @@ void main() { await context.wait(1); } - helper.sim.process(event: fA, name: 'A', resourceId: 'r'); - helper.sim.process(event: fB, name: 'B', resourceId: 'r'); - helper.sim.process(event: fC, name: 'C', resourceId: 'r'); - await helper.sim.run(); - expect(helper.trackList.length, 9); - helper.testTrack(index: 0, name: 'A', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'B', status: Status.rejected, time: 0); - helper.testTrack(index: 2, name: 'C', status: Status.rejected, time: 0); - - helper.testTrack(index: 3, name: 'A', status: Status.resumed, time: 1); - helper.testTrack(index: 4, name: 'B', status: Status.executed, time: 1); - helper.testTrack(index: 5, name: 'C', status: Status.rejected, time: 1); - helper.testTrack(index: 6, name: 'B', status: Status.resumed, time: 2); - helper.testTrack(index: 7, name: 'C', status: Status.executed, time: 2); - helper.testTrack(index: 8, name: 'C', status: Status.resumed, time: 3); + sim.process(event: fA, name: 'A', resourceId: 'r'); + sim.process(event: fB, name: 'B', resourceId: 'r'); + sim.process(event: fC, name: 'C', resourceId: 'r'); + + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + "[0][A][executed]", + "[0][B][rejected]", + "[0][C][rejected]", + "[1][A][resumed]", + "[1][B][executed]", + "[1][C][rejected]", + "[2][B][resumed]", + "[2][C][executed]", + "[3][C][resumed]" + ]); }); test('Capacity 2', () async { - TestHelper helper = TestHelper(); - helper.sim.resources.limited(id: 'r', capacity: 2); + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.resources.limited(id: 'r', capacity: 2); fA(context) async { await context.wait(1); @@ -53,23 +57,26 @@ void main() { await context.wait(1); } - helper.sim.process(event: fA, name: 'A', resourceId: 'r'); - helper.sim.process(event: fB, name: 'B', resourceId: 'r'); - helper.sim.process(event: fC, name: 'C', resourceId: 'r'); - await helper.sim.run(); - expect(helper.trackList.length, 7); - helper.testTrack(index: 0, name: 'A', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'B', status: Status.executed, time: 0); - helper.testTrack(index: 2, name: 'C', status: Status.rejected, time: 0); - - helper.testTrack(index: 3, name: 'A', status: Status.resumed, time: 1); - helper.testTrack(index: 4, name: 'C', status: Status.executed, time: 1); - helper.testTrack(index: 5, name: 'B', status: Status.resumed, time: 1); - helper.testTrack(index: 6, name: 'C', status: Status.resumed, time: 2); + sim.process(event: fA, name: 'A', resourceId: 'r'); + sim.process(event: fB, name: 'B', resourceId: 'r'); + sim.process(event: fC, name: 'C', resourceId: 'r'); + + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + "[0][A][executed]", + "[0][B][executed]", + "[0][C][rejected]", + "[1][A][resumed]", + "[1][C][executed]", + "[1][B][resumed]", + "[2][C][resumed]" + ]); }); test('Avoid unnecessary re-executing', () async { - TestHelper helper = TestHelper(); - helper.sim.resources.limited(id: 'r', capacity: 2); + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.resources.limited(id: 'r', capacity: 2); eventA(EventContext context) async { await context.wait(10); @@ -77,8 +84,6 @@ void main() { eventB(EventContext context) async {} - SimDart sim = helper.sim; - sim.resources.limited(id: 'resource', capacity: 2); sim.process(event: eventA, name: 'A1', resourceId: 'resource'); @@ -86,16 +91,19 @@ void main() { sim.process(event: eventA, name: 'A3', start: 2, resourceId: 'resource'); sim.process(event: eventB, name: 'B', start: 3); - await helper.sim.run(); - expect(helper.trackList.length, 8); - helper.testTrack(index: 0, name: 'A1', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'A2', status: Status.executed, time: 1); - helper.testTrack(index: 2, name: 'A3', status: Status.rejected, time: 2); - helper.testTrack(index: 3, name: 'B', status: Status.executed, time: 3); - helper.testTrack(index: 4, name: 'A1', status: Status.resumed, time: 10); - helper.testTrack(index: 5, name: 'A3', status: Status.executed, time: 10); - helper.testTrack(index: 6, name: 'A2', status: Status.resumed, time: 11); - helper.testTrack(index: 7, name: 'A3', status: Status.resumed, time: 20); + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + "[0][A1][executed]", + "[1][A2][executed]", + "[2][A3][rejected]", + "[3][B][executed]", + "[10][A1][resumed]", + "[10][A3][executed]", + "[11][A2][resumed]", + "[20][A3][resumed]" + ]); }); }); } diff --git a/test/test_helper.dart b/test/test_helper.dart deleted file mode 100644 index c15d96c..0000000 --- a/test/test_helper.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:collection'; - -import 'package:simdart/simdart.dart'; -import 'package:test/expect.dart'; - -class TestHelper { - TestHelper() { - sim = SimDart(onTrack: _onTrack, secondarySortByName: true); - } - - late final SimDart sim; - - final List _trackList = []; - - late final UnmodifiableListView trackList = - UnmodifiableListView(_trackList); - - void _onTrack(SimulationTrack track) { - _trackList.add(track); - } - - void testTrack( - {required int index, - required String? name, - required Status status, - required int time}) { - expect(trackList[index].name, name); - expect(trackList[index].status, status); - expect(trackList[index].time, time); - } - - static Future emptyEvent(EventContext context) async {} -} diff --git a/test/time_loop_test.dart b/test/time_loop_test.dart index 02cd771..46a25d3 100644 --- a/test/time_loop_test.dart +++ b/test/time_loop_test.dart @@ -19,6 +19,7 @@ void main() { group('TimeLoop', () { test('Loop', () async { TimeLoop loop = TimeLoop( + includeTracks: true, now: null, executionPriority: ExecutionPriority.high, beforeRun: () {}, diff --git a/test/track_tester.dart b/test/track_tester.dart new file mode 100644 index 0000000..7ec07ca --- /dev/null +++ b/test/track_tester.dart @@ -0,0 +1,19 @@ +import 'package:simdart/simdart.dart'; +import 'package:simdart/src/sim_result.dart'; +import 'package:test/expect.dart'; + +class TrackTester { + TrackTester(this.result); + + final SimResult result; + + int get length => result.tracks != null ? result.tracks!.length : 0; + + void test(List tracks) { + expect(tracks.length, result.tracks?.length); + for (int index = 0; index < tracks.length; index++) { + SimulationTrack? track = result.tracks?[index]; + expect(tracks[index], track?.toString()); + } + } +} diff --git a/test/wait_test.dart b/test/wait_test.dart index 04fed2b..c3e7278 100644 --- a/test/wait_test.dart +++ b/test/wait_test.dart @@ -1,72 +1,84 @@ import 'package:simdart/simdart.dart'; +import 'package:simdart/src/sim_result.dart'; import 'package:test/test.dart'; -import 'test_helper.dart'; +import 'track_tester.dart'; + +Future emptyEvent(EventContext context) async {} void main() { group('Wait', () { test('with await', () async { - TestHelper helper = TestHelper(); - helper.sim.process( + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process( event: (context) async { await context.wait(10); }, name: 'a'); - helper.sim.process(event: TestHelper.emptyEvent, start: 5, name: 'b'); - await helper.sim.run(); - expect(helper.trackList.length, 3); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'b', status: Status.executed, time: 5); - helper.testTrack(index: 2, name: 'a', status: Status.resumed, time: 10); + sim.process(event: emptyEvent, start: 5, name: 'b'); + + SimResult result = await sim.run(); - helper = TestHelper(); - helper.sim.process( + TrackTester tt = TrackTester(result); + tt.test(["[0][a][executed]", "[5][b][executed]", "[10][a][resumed]"]); + }); + test('with await 2', () async { + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process( event: (context) async { await context.wait(10); - helper.sim - .process(event: TestHelper.emptyEvent, delay: 1, name: 'c'); + sim.process(event: emptyEvent, delay: 1, name: 'c'); }, start: 0, name: 'a'); - helper.sim.process(event: TestHelper.emptyEvent, delay: 5, name: 'b'); - await helper.sim.run(); - expect(helper.trackList.length, 4); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'b', status: Status.executed, time: 5); - helper.testTrack(index: 2, name: 'a', status: Status.resumed, time: 10); - helper.testTrack(index: 3, name: 'c', status: Status.executed, time: 11); + sim.process(event: emptyEvent, delay: 5, name: 'b'); + + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + "[0][a][executed]", + "[5][b][executed]", + "[10][a][resumed]", + "[11][c][executed]" + ]); }); test('without await', () async { - TestHelper helper = TestHelper(); - helper.sim.process( + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process( event: (context) async { context.wait(10); }, name: 'a'); - helper.sim.process(event: TestHelper.emptyEvent, start: 5, name: 'b'); - await helper.sim.run(); - expect(helper.trackList.length, 3); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'b', status: Status.executed, time: 5); - helper.testTrack(index: 2, name: 'a', status: Status.resumed, time: 10); + sim.process(event: emptyEvent, start: 5, name: 'b'); - helper = TestHelper(); - helper.sim.process( + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test(["[0][a][executed]", "[5][b][executed]", "[10][a][resumed]"]); + }); + + test('without await 2', () async { + SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + sim.process( event: (context) async { context.wait(10); - helper.sim - .process(event: TestHelper.emptyEvent, delay: 1, name: 'c'); + sim.process(event: emptyEvent, delay: 1, name: 'c'); }, start: 0, name: 'a'); - helper.sim.process(event: TestHelper.emptyEvent, delay: 5, name: 'b'); - await helper.sim.run(); - expect(helper.trackList.length, 4); - helper.testTrack(index: 0, name: 'a', status: Status.executed, time: 0); - helper.testTrack(index: 1, name: 'c', status: Status.executed, time: 1); - helper.testTrack(index: 2, name: 'b', status: Status.executed, time: 5); - helper.testTrack(index: 3, name: 'a', status: Status.resumed, time: 10); + sim.process(event: emptyEvent, delay: 5, name: 'b'); + + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + "[0][a][executed]", + "[1][c][executed]", + "[5][b][executed]", + "[10][a][resumed]" + ]); }); }); } From f4e261d991a0961911fa0f2977042f63e3e1b1a9 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Sat, 25 Jan 2025 18:40:11 -0300 Subject: [PATCH 03/10] Formatting. --- lib/src/internal/event_action.dart | 3 --- lib/src/internal/sim_configuration_interface.dart | 4 ---- lib/src/internal/sim_result_interface.dart | 1 - lib/src/internal/time_loop.dart | 11 ++++++----- lib/src/simdart.dart | 11 +++++++---- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/lib/src/internal/event_action.dart b/lib/src/internal/event_action.dart index 37edd0c..ceceebd 100644 --- a/lib/src/internal/event_action.dart +++ b/lib/src/internal/event_action.dart @@ -25,7 +25,6 @@ class EventAction extends TimeAction implements EventContext { final String? _eventName; String get eventName => _eventName ?? hashCode.toString(); - /// The event to be executed. final Event event; @@ -57,8 +56,6 @@ class EventAction extends TimeAction implements EventContext { void execute() { final Function()? resume = _resume; - - if (resume != null) { if (_sim.includeTracks) { SimDartHelper.addSimulationTrack( diff --git a/lib/src/internal/sim_configuration_interface.dart b/lib/src/internal/sim_configuration_interface.dart index d996c4d..eac42de 100644 --- a/lib/src/internal/sim_configuration_interface.dart +++ b/lib/src/internal/sim_configuration_interface.dart @@ -2,7 +2,6 @@ import 'package:meta/meta.dart'; import 'package:simdart/src/execution_priority.dart'; import 'package:simdart/src/start_time_handling.dart'; - @internal abstract interface class SimConfigurationInterface { /// Specifies how the simulation handles start times in the past. @@ -24,7 +23,4 @@ abstract interface class SimConfigurationInterface { /// /// Default: `false` bool get includeTracks; - - - } diff --git a/lib/src/internal/sim_result_interface.dart b/lib/src/internal/sim_result_interface.dart index 56ce86f..659d138 100644 --- a/lib/src/internal/sim_result_interface.dart +++ b/lib/src/internal/sim_result_interface.dart @@ -3,7 +3,6 @@ import 'package:meta/meta.dart'; /// Represents the simulation result. @internal abstract interface class SimResultInterface { - /// The time, in simulated time units, when the simulation started. /// This is the moment at which the first event is scheduled to be processed. /// diff --git a/lib/src/internal/time_loop.dart b/lib/src/internal/time_loop.dart index ef2f34a..fcdd2e4 100644 --- a/lib/src/internal/time_loop.dart +++ b/lib/src/internal/time_loop.dart @@ -13,11 +13,12 @@ import 'package:simdart/src/start_time_handling.dart'; /// Represents the temporal loop in the algorithm, managing the execution of actions at specified times. @internal -class TimeLoop implements SimConfigurationInterface,NowInterface,SimResultInterface { +class TimeLoop + implements SimConfigurationInterface, NowInterface, SimResultInterface { TimeLoop( {required int? now, required this.beforeRun, - required this.includeTracks, + required this.includeTracks, required this.executionPriority, required this.startTimeHandling}) { _now = now ?? 0; @@ -35,7 +36,7 @@ class TimeLoop implements SimConfigurationInterface,NowInterface,SimResultInterf final Function beforeRun; @override - final bool includeTracks; + final bool includeTracks; /// Queue that holds the [TimeAction] instances to be executed at their respective times. final PriorityQueue _actions = PriorityQueue( @@ -98,7 +99,7 @@ class TimeLoop implements SimConfigurationInterface,NowInterface,SimResultInterf return _buildResult(); } - SimResult _buildResult(){ + SimResult _buildResult() { return SimResult(startTime: startTime, duration: duration, tracks: _tracks); } @@ -120,7 +121,7 @@ class TimeLoop implements SimConfigurationInterface,NowInterface,SimResultInterf _actions.add(action); } - void addTrack(SimulationTrack track){ + void addTrack(SimulationTrack track) { _tracks ??= []; _tracks!.add(track); } diff --git a/lib/src/simdart.dart b/lib/src/simdart.dart index bfba4f1..4241bed 100644 --- a/lib/src/simdart.dart +++ b/lib/src/simdart.dart @@ -20,7 +20,11 @@ import 'package:simdart/src/simulation_track.dart'; import 'package:simdart/src/start_time_handling.dart'; /// Represents a discrete-event simulation engine. -class SimDart implements SimConfigurationInterface, EventSchedulerInterface,NowInterface { +class SimDart + implements + SimConfigurationInterface, + EventSchedulerInterface, + NowInterface { /// Creates a simulation instance. /// /// - [now]: The starting time of the simulation. Defaults to `0` if null. @@ -43,10 +47,10 @@ class SimDart implements SimConfigurationInterface, EventSchedulerInterface,NowI {StartTimeHandling startTimeHandling = StartTimeHandling.throwErrorIfPast, int? now, this.secondarySortByName = false, - this.includeTracks=false, + this.includeTracks = false, ExecutionPriority executionPriority = ExecutionPriority.high, int? seed}) - : random = Random(seed) { + : random = Random(seed) { _loop = TimeLoop( now: now, includeTracks: includeTracks, @@ -204,7 +208,6 @@ class SimDart implements SimConfigurationInterface, EventSchedulerInterface,NowI @override int get now => _loop.now; - @override StartTimeHandling get startTimeHandling => _loop.startTimeHandling; } From 6a5da0ebc44dd94fb9a45c9e965dec8de4701d68 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Sun, 26 Jan 2025 10:11:28 -0300 Subject: [PATCH 04/10] Using a counter to prioritize between microtask and delayed.#8 (#9) --- lib/simdart.dart | 2 +- lib/src/event.dart | 6 ++++ lib/src/execution_priority.dart | 18 ---------- lib/src/internal/event_action.dart | 4 +++ .../internal/sim_configuration_interface.dart | 16 ++++----- lib/src/internal/time_loop.dart | 36 +++++++++---------- lib/src/simdart.dart | 11 +++--- test/time_loop_test.dart | 2 +- 8 files changed, 40 insertions(+), 55 deletions(-) delete mode 100644 lib/src/execution_priority.dart diff --git a/lib/simdart.dart b/lib/simdart.dart index f5bc856..385c099 100644 --- a/lib/simdart.dart +++ b/lib/simdart.dart @@ -1,8 +1,8 @@ export 'src/event.dart'; -export 'src/execution_priority.dart'; export 'src/interval.dart'; export 'src/observable.dart'; export 'src/resource_configurator.dart' hide ResourcesConfiguratorHelper; export 'src/simdart.dart' hide SimDartHelper; export 'src/simulation_track.dart'; export 'src/start_time_handling.dart'; +export 'src/sim_result.dart'; diff --git a/lib/src/event.dart b/lib/src/event.dart index 6363d6d..bca21f0 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:simdart/src/internal/event_scheduler_interface.dart'; @@ -20,4 +21,9 @@ abstract interface class EventContext implements EventSchedulerInterface { /// /// Throws an [ArgumentError] if the delay is negative. Future wait(int delay); + + /// The instance of the random number generator used across the simulation. + /// It is initialized once and reused to improve performance, avoiding the need to + /// instantiate a new `Random` object for each event. + Random get random; } diff --git a/lib/src/execution_priority.dart b/lib/src/execution_priority.dart deleted file mode 100644 index c019434..0000000 --- a/lib/src/execution_priority.dart +++ /dev/null @@ -1,18 +0,0 @@ -/// Enum that defines the priority of task execution in the system. -/// -/// - `highPriority`: Execution is given high priority, and it will be -/// processed using `Future.microtask` between events, allowing for -/// immediate execution without blocking the UI. -/// -/// - `lowPriority`: Execution is given lower priority, using `Future.delayed(Duration.zero)`, -/// which allows for non-blocking execution and ensures that the UI is not blocked, allowing -/// for smoother interactions with the user interface. -enum ExecutionPriority { - /// High priority execution, will use `Future.microtask` between events. - /// This ensures that the task runs immediately without blocking other operations or the UI. - high, - - /// Low priority execution, will use `Future.delayed(Duration.zero)`. - /// This ensures that the task is executed with minimal blocking, allowing the UI to remain responsive. - low -} diff --git a/lib/src/internal/event_action.dart b/lib/src/internal/event_action.dart index ceceebd..46237a4 100644 --- a/lib/src/internal/event_action.dart +++ b/lib/src/internal/event_action.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; @@ -32,6 +33,9 @@ class EventAction extends TimeAction implements EventContext { final SimDart _sim; + @override + Random get random => _sim.random; + /// The resource id required by the event. final String? resourceId; diff --git a/lib/src/internal/sim_configuration_interface.dart b/lib/src/internal/sim_configuration_interface.dart index eac42de..ef9552a 100644 --- a/lib/src/internal/sim_configuration_interface.dart +++ b/lib/src/internal/sim_configuration_interface.dart @@ -1,5 +1,4 @@ import 'package:meta/meta.dart'; -import 'package:simdart/src/execution_priority.dart'; import 'package:simdart/src/start_time_handling.dart'; @internal @@ -7,14 +6,6 @@ abstract interface class SimConfigurationInterface { /// Specifies how the simulation handles start times in the past. StartTimeHandling get startTimeHandling; - /// Defines the priority of task execution in the simulation. - /// - /// - `highPriority`: Uses `Future.microtask` for immediate execution, prioritizing - /// processing without blocking the UI. - /// - `lowPriority`: Uses `Future.delayed(Duration.zero)` to ensure non-blocking - /// execution, allowing the UI to remain responsive. - ExecutionPriority get executionPriority; - /// Determines whether simulation tracks should be included in the simulation result. /// /// When set to `true`, the simulation will collect and return a list of [SimulationTrack] @@ -23,4 +14,11 @@ abstract interface class SimConfigurationInterface { /// /// Default: `false` bool get includeTracks; + + /// Determines how often `Future.delayed` is used instead of `microtask` during event execution. + /// + /// - `0`: Always uses `microtask`. + /// - `1`: Alternates between `microtask` and `Future.delayed`. + /// - `N > 1`: Executes `N` events with `microtask` before using `Future.delayed`. + int get executionPriorityCounter; } diff --git a/lib/src/internal/time_loop.dart b/lib/src/internal/time_loop.dart index fcdd2e4..e8cdf99 100644 --- a/lib/src/internal/time_loop.dart +++ b/lib/src/internal/time_loop.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; -import 'package:simdart/src/execution_priority.dart'; import 'package:simdart/src/internal/now_interface.dart'; import 'package:simdart/src/internal/sim_result_interface.dart'; import 'package:simdart/src/internal/time_action.dart'; @@ -19,19 +19,18 @@ class TimeLoop {required int? now, required this.beforeRun, required this.includeTracks, - required this.executionPriority, - required this.startTimeHandling}) { - _now = now ?? 0; - _priorityScheduler = executionPriority == ExecutionPriority.high - ? _highPrioritySchedule - : _lowPrioritySchedule; - } + required int executionPriorityCounter, + required this.startTimeHandling}) + : executionPriorityCounter = math.max(executionPriorityCounter, 0), + _now = now ?? 0; @override final StartTimeHandling startTimeHandling; @override - final ExecutionPriority executionPriority; + final int executionPriorityCounter; + + int _executionCount = 0; final Function beforeRun; @@ -57,8 +56,6 @@ class TimeLoop int? get duration => _duration; int? _duration; - late final Function _priorityScheduler; - bool _nextEventScheduled = false; late int? _until; @@ -106,15 +103,14 @@ class TimeLoop void _scheduleNextEvent() { assert(!_nextEventScheduled, 'Multiple schedules for the next event.'); _nextEventScheduled = true; - _priorityScheduler.call(); - } - - void _highPrioritySchedule() { - Future.microtask(_consumeFirstEvent); - } - - void _lowPrioritySchedule() { - Future.delayed(Duration.zero, _consumeFirstEvent); + if (executionPriorityCounter == 0 || + _executionCount < executionPriorityCounter) { + _executionCount++; + Future.microtask(_consumeFirstEvent); + } else { + _executionCount = 0; + Future.delayed(Duration.zero, _consumeFirstEvent); + } } void addAction(TimeAction action) { diff --git a/lib/src/simdart.dart b/lib/src/simdart.dart index 4241bed..78ce43f 100644 --- a/lib/src/simdart.dart +++ b/lib/src/simdart.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; -import 'package:simdart/src/execution_priority.dart'; import 'package:simdart/src/internal/event_action.dart'; import 'package:simdart/src/internal/event_scheduler_interface.dart'; import 'package:simdart/src/internal/now_interface.dart'; @@ -48,19 +47,22 @@ class SimDart int? now, this.secondarySortByName = false, this.includeTracks = false, - ExecutionPriority executionPriority = ExecutionPriority.high, + this.executionPriorityCounter = 0, int? seed}) : random = Random(seed) { _loop = TimeLoop( now: now, includeTracks: includeTracks, beforeRun: _beforeRun, - executionPriority: executionPriority, + executionPriorityCounter: executionPriorityCounter, startTimeHandling: startTimeHandling); } late final TimeLoop _loop; + @override + final int executionPriorityCounter; + @override final bool includeTracks; @@ -202,9 +204,6 @@ class SimDart } } - @override - ExecutionPriority get executionPriority => _loop.executionPriority; - @override int get now => _loop.now; diff --git a/test/time_loop_test.dart b/test/time_loop_test.dart index 46a25d3..f841ba1 100644 --- a/test/time_loop_test.dart +++ b/test/time_loop_test.dart @@ -21,7 +21,7 @@ void main() { TimeLoop loop = TimeLoop( includeTracks: true, now: null, - executionPriority: ExecutionPriority.high, + executionPriorityCounter: 0, beforeRun: () {}, startTimeHandling: StartTimeHandling.throwErrorIfPast); From 461e26e2ad9314ff44e542ea6844f22cb6637087 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Tue, 11 Feb 2025 16:02:29 -0300 Subject: [PATCH 05/10] Removing nullable from simulation result. #2 --- lib/src/internal/sim_result_interface.dart | 6 ++---- lib/src/internal/time_loop.dart | 10 +++++----- lib/src/sim_result.dart | 6 +++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/src/internal/sim_result_interface.dart b/lib/src/internal/sim_result_interface.dart index 659d138..2561f35 100644 --- a/lib/src/internal/sim_result_interface.dart +++ b/lib/src/internal/sim_result_interface.dart @@ -9,14 +9,12 @@ abstract interface class SimResultInterface { /// For example, if the first process is scheduled to occur at time 10, /// then the simulation start time would be 10. This value helps track when /// the simulation officially begins its execution in terms of the simulation time. - int? get startTime; + int get startTime; /// The duration, in simulated time units, that the simulation took to execute. /// /// This value represents the total time elapsed during the processing of the simulation, /// from the start to the completion of all event handling, in terms of the simulated environment. /// It is used to track how much time has passed in the simulation model, not real-world time. - /// - /// The value will be `null` if the duration has not been calculated or set. - int? get duration; + int get duration; } diff --git a/lib/src/internal/time_loop.dart b/lib/src/internal/time_loop.dart index e8cdf99..395f6a6 100644 --- a/lib/src/internal/time_loop.dart +++ b/lib/src/internal/time_loop.dart @@ -49,12 +49,12 @@ class TimeLoop ); @override - int? get startTime => _startTime; + int get startTime => _startTime ?? 0; int? _startTime; @override - int? get duration => _duration; - int? _duration; + int get duration => _duration; + int _duration = 0; bool _nextEventScheduled = false; @@ -83,7 +83,7 @@ class TimeLoop _startTime = 0; return _buildResult(); } - _duration = null; + _duration = 0; _startTime = null; beforeRun(); @@ -91,7 +91,7 @@ class TimeLoop _terminator = Completer(); _scheduleNextEvent(); await _terminator?.future; - _duration = _now - (startTime ?? 0); + _duration = _now - startTime; _terminator = null; return _buildResult(); } diff --git a/lib/src/sim_result.dart b/lib/src/sim_result.dart index 52cd3a9..d04b176 100644 --- a/lib/src/sim_result.dart +++ b/lib/src/sim_result.dart @@ -11,10 +11,10 @@ class SimResult implements SimResultInterface { : tracks = tracks != null ? UnmodifiableListView(tracks) : null; @override - final int? duration; + final int duration; @override - final int? startTime; + final int startTime; - final UnmodifiableListView? tracks; + final List? tracks; } From 135c240b06bf31806f1f3443726ff61811aef601 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Wed, 12 Feb 2025 19:35:34 -0300 Subject: [PATCH 06/10] Add support for accumulating simulation results (#10) * Removing unnecessary import. #2 * Adding num and counter properties. #4 * Removing unnecessary import * Removing unnecessary imports. --- CHANGELOG.md | 5 + example/example.dart | 1 - lib/simdart.dart | 4 + lib/src/event.dart | 24 +-- lib/src/event_context.dart | 36 ++++ lib/src/internal/event_action.dart | 14 ++ lib/src/internal/resource.dart | 2 +- .../internal/sim_configuration_interface.dart | 4 +- lib/src/internal/time_loop.dart | 152 --------------- lib/src/sim_counter.dart | 37 ++++ lib/src/sim_num.dart | 123 ++++++++++++ lib/src/sim_property.dart | 18 ++ lib/src/sim_result.dart | 13 +- lib/src/simdart.dart | 179 +++++++++++++++--- test/process_test.dart | 1 - test/property_test.dart | 105 ++++++++++ test/repeat_process_test.dart | 1 - test/resource_test.dart | 1 - test/time_loop_test.dart | 30 +-- test/track_tester.dart | 1 - test/wait_test.dart | 1 - 21 files changed, 523 insertions(+), 229 deletions(-) create mode 100644 lib/src/event_context.dart delete mode 100644 lib/src/internal/time_loop.dart create mode 100644 lib/src/sim_counter.dart create mode 100644 lib/src/sim_num.dart create mode 100644 lib/src/sim_property.dart create mode 100644 test/property_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 41575e3..72412ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.2.0 + +* Setting to determine how often `Future.delayed` is used instead of `Future.microtask` during event execution to allow GUI refresh. +* Adding numeric properties and counter. + ## 0.1.0 * Initial release diff --git a/example/example.dart b/example/example.dart index 1676426..11ee7aa 100644 --- a/example/example.dart +++ b/example/example.dart @@ -1,5 +1,4 @@ import 'package:simdart/simdart.dart'; -import 'package:simdart/src/sim_result.dart'; void main() async { final SimDart sim = SimDart(includeTracks: true); diff --git a/lib/simdart.dart b/lib/simdart.dart index 385c099..68e6f2f 100644 --- a/lib/simdart.dart +++ b/lib/simdart.dart @@ -1,4 +1,5 @@ export 'src/event.dart'; +export 'src/event_context.dart'; export 'src/interval.dart'; export 'src/observable.dart'; export 'src/resource_configurator.dart' hide ResourcesConfiguratorHelper; @@ -6,3 +7,6 @@ export 'src/simdart.dart' hide SimDartHelper; export 'src/simulation_track.dart'; export 'src/start_time_handling.dart'; export 'src/sim_result.dart'; +export 'src/sim_num.dart'; +export 'src/sim_property.dart'; +export 'src/sim_counter.dart'; diff --git a/lib/src/event.dart b/lib/src/event.dart index bca21f0..df8a9de 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,29 +1,7 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:simdart/src/internal/event_scheduler_interface.dart'; +import 'package:simdart/simdart.dart'; /// The event to be executed. /// /// A function that represents an event in the simulation. It receives an /// [EventContext] that provides data about the event's execution state and context. typedef Event = void Function(EventContext context); - -/// Represents the context of an event in the simulation. -/// -/// Encapsulates the information and state of an event being executed -/// within the simulation. -abstract interface class EventContext implements EventSchedulerInterface { - /// Pauses the execution of the event for the specified [delay] in simulation time. - /// - /// The event is re-added to the simulation's event queue and will resume after - /// the specified delay has passed. - /// - /// Throws an [ArgumentError] if the delay is negative. - Future wait(int delay); - - /// The instance of the random number generator used across the simulation. - /// It is initialized once and reused to improve performance, avoiding the need to - /// instantiate a new `Random` object for each event. - Random get random; -} diff --git a/lib/src/event_context.dart b/lib/src/event_context.dart new file mode 100644 index 0000000..ed5a65e --- /dev/null +++ b/lib/src/event_context.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:simdart/simdart.dart'; +import 'package:simdart/src/internal/event_scheduler_interface.dart'; + +/// Represents the context of an event in the simulation. +/// +/// Encapsulates the information and state of an event being executed +/// within the simulation. +abstract interface class EventContext implements EventSchedulerInterface { + /// Pauses the execution of the event for the specified [delay] in simulation time. + /// + /// The event is re-added to the simulation's event queue and will resume after + /// the specified delay has passed. + /// + /// Throws an [ArgumentError] if the delay is negative. + Future wait(int delay); + + /// The instance of the random number generator used across the simulation. + /// It is initialized once and reused to improve performance, avoiding the need to + /// instantiate a new `Random` object for each event. + Random get random; + + /// Creates a new [SimCounter] instance with the given name. + /// + /// - [name]: The name of the counter. This is used to identify the counter in logs or reports. + /// - Returns: A new instance of [SimCounter]. + SimCounter counter(String name); + + /// Creates a new [SimNum] instance with the given name. + /// + /// - [name]: The name of the numeric metric. This is used to identify the metric in logs or reports. + /// - Returns: A new instance of [SimNum]. + SimNum num(String name); +} diff --git a/lib/src/internal/event_action.dart b/lib/src/internal/event_action.dart index 46237a4..9d83024 100644 --- a/lib/src/internal/event_action.dart +++ b/lib/src/internal/event_action.dart @@ -3,9 +3,12 @@ import 'dart:math'; import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; +import 'package:simdart/src/event_context.dart'; import 'package:simdart/src/internal/resource.dart'; import 'package:simdart/src/internal/time_action.dart'; import 'package:simdart/src/interval.dart'; +import 'package:simdart/src/sim_counter.dart'; +import 'package:simdart/src/sim_num.dart'; import 'package:simdart/src/simdart.dart'; import 'package:simdart/src/simulation_track.dart'; @@ -24,6 +27,7 @@ class EventAction extends TimeAction implements EventContext { /// The name of the event. final String? _eventName; + String get eventName => _eventName ?? hashCode.toString(); /// The event to be executed. @@ -161,4 +165,14 @@ class EventAction extends TimeAction implements EventContext { resourceId: resourceId, name: name); } + + @override + SimCounter counter(String name) { + return SimDartHelper.counter(sim: _sim, name: name); + } + + @override + SimNum num(String name) { + return SimDartHelper.num(sim: _sim, name: name); + } } diff --git a/lib/src/internal/resource.dart b/lib/src/internal/resource.dart index b268525..014db3b 100644 --- a/lib/src/internal/resource.dart +++ b/lib/src/internal/resource.dart @@ -1,5 +1,5 @@ import 'package:meta/meta.dart'; -import 'package:simdart/src/event.dart'; +import 'package:simdart/src/event_context.dart'; @internal abstract class Resource { diff --git a/lib/src/internal/sim_configuration_interface.dart b/lib/src/internal/sim_configuration_interface.dart index ef9552a..dd5d311 100644 --- a/lib/src/internal/sim_configuration_interface.dart +++ b/lib/src/internal/sim_configuration_interface.dart @@ -15,10 +15,10 @@ abstract interface class SimConfigurationInterface { /// Default: `false` bool get includeTracks; - /// Determines how often `Future.delayed` is used instead of `microtask` during event execution. + /// Determines how often `Future.delayed` is used instead of `Future.microtask` during events execution. /// /// - `0`: Always uses `microtask`. /// - `1`: Alternates between `microtask` and `Future.delayed`. /// - `N > 1`: Executes `N` events with `microtask` before using `Future.delayed`. - int get executionPriorityCounter; + int get executionPriority; } diff --git a/lib/src/internal/time_loop.dart b/lib/src/internal/time_loop.dart deleted file mode 100644 index 395f6a6..0000000 --- a/lib/src/internal/time_loop.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; -import 'package:simdart/src/internal/now_interface.dart'; -import 'package:simdart/src/internal/sim_result_interface.dart'; -import 'package:simdart/src/internal/time_action.dart'; -import 'package:simdart/src/internal/sim_configuration_interface.dart'; -import 'package:simdart/src/sim_result.dart'; -import 'package:simdart/src/simulation_track.dart'; -import 'package:simdart/src/start_time_handling.dart'; - -/// Represents the temporal loop in the algorithm, managing the execution of actions at specified times. -@internal -class TimeLoop - implements SimConfigurationInterface, NowInterface, SimResultInterface { - TimeLoop( - {required int? now, - required this.beforeRun, - required this.includeTracks, - required int executionPriorityCounter, - required this.startTimeHandling}) - : executionPriorityCounter = math.max(executionPriorityCounter, 0), - _now = now ?? 0; - - @override - final StartTimeHandling startTimeHandling; - - @override - final int executionPriorityCounter; - - int _executionCount = 0; - - final Function beforeRun; - - @override - final bool includeTracks; - - /// Queue that holds the [TimeAction] instances to be executed at their respective times. - final PriorityQueue _actions = PriorityQueue( - (a, b) { - final primaryComparison = a.start.compareTo(b.start); - if (primaryComparison != 0) { - return primaryComparison; - } - return a.secondaryCompareTo(b); - }, - ); - - @override - int get startTime => _startTime ?? 0; - int? _startTime; - - @override - int get duration => _duration; - int _duration = 0; - - bool _nextEventScheduled = false; - - late int? _until; - - @override - int get now => _now; - late int _now; - - List? _tracks; - - Completer? _terminator; - - /// Runs the simulation, processing actions in chronological order. - Future run({int? until}) async { - if (until != null && _now > until) { - throw ArgumentError('`now` must be less than or equal to `until`.'); - } - _until = until; - - if (_terminator != null) { - return _buildResult(); - } - if (_actions.isEmpty) { - _duration = 0; - _startTime = 0; - return _buildResult(); - } - _duration = 0; - _startTime = null; - - beforeRun(); - - _terminator = Completer(); - _scheduleNextEvent(); - await _terminator?.future; - _duration = _now - startTime; - _terminator = null; - return _buildResult(); - } - - SimResult _buildResult() { - return SimResult(startTime: startTime, duration: duration, tracks: _tracks); - } - - void _scheduleNextEvent() { - assert(!_nextEventScheduled, 'Multiple schedules for the next event.'); - _nextEventScheduled = true; - if (executionPriorityCounter == 0 || - _executionCount < executionPriorityCounter) { - _executionCount++; - Future.microtask(_consumeFirstEvent); - } else { - _executionCount = 0; - Future.delayed(Duration.zero, _consumeFirstEvent); - } - } - - void addAction(TimeAction action) { - _actions.add(action); - } - - void addTrack(SimulationTrack track) { - _tracks ??= []; - _tracks!.add(track); - } - - Future _consumeFirstEvent() async { - _nextEventScheduled = false; - if (_actions.isEmpty) { - _terminator?.complete(); - return; - } - - TimeAction action = _actions.removeFirst(); - - // Advance the simulation time to the action's start time. - if (action.start > now) { - _now = action.start; - if (_until != null && now > _until!) { - _startTime ??= now; - _terminator?.complete(); - return; - } - } else if (action.start < now) { - action.start = now; - } - - _startTime ??= now; - - action.execute(); - - _scheduleNextEvent(); - } -} diff --git a/lib/src/sim_counter.dart b/lib/src/sim_counter.dart new file mode 100644 index 0000000..af721b4 --- /dev/null +++ b/lib/src/sim_counter.dart @@ -0,0 +1,37 @@ +import 'package:simdart/src/sim_property.dart'; + +/// A class to track event counts in a discrete event simulation. +/// +/// This class extends [SimProperty] and provides methods to increment and reset a counter. +/// It is useful for counting occurrences of specific events, such as arrivals, departures, or errors. +class SimCounter extends SimProperty { + int _value = 0; + + /// Creates a new [SimCounter] instance. + /// + /// Optionally, provide a [name] to identify the counter. + SimCounter({super.name}); + + /// The current value of the counter. + int get value => _value; + + /// Increments the counter by 1. + void inc() { + _value++; + } + + /// Increments the counter by a specified value. + /// + /// - [value]: The value to increment the counter by. + void incBy(int value) { + if (value > 0) { + _value += value; + } + } + + /// Resets the counter to 0. + @override + void reset() { + _value = 0; + } +} diff --git a/lib/src/sim_num.dart b/lib/src/sim_num.dart new file mode 100644 index 0000000..a8f7818 --- /dev/null +++ b/lib/src/sim_num.dart @@ -0,0 +1,123 @@ +import 'dart:math'; + +import 'package:simdart/src/sim_property.dart'; + +/// A class to track numeric metrics in a discrete event simulation. +/// +/// This class allows you to store and update numeric values (both integers and doubles), +/// while automatically tracking minimum, maximum, and average values. +class SimNum extends SimProperty { + num? _value; + num? _min; + num? _max; + num _total = 0; + int _count = 0; + num _sumOfSquares = 0; + + final Map _frequencyMap = {}; + + /// Creates a new [SimNum] instance. + /// + /// Optionally, provide a [name] to identify the metric. + SimNum({super.name}); + + /// The current value of the metric. + /// + /// Returns `null` if no value has been set. + num? get value => _value; + + /// Sets the current value of the metric. + /// + /// If the value is not `null`, it updates the minimum, maximum, total, and count. + set value(num? value) { + _value = value; + + if (value != null) { + _frequencyMap[value] = (_frequencyMap[value] ?? 0) + 1; + + // Update min and max + _min = (_min == null || value < _min!) ? value : _min; + _max = (_max == null || value > _max!) ? value : _max; + + // Update total, sum of squares, and count + _total += value; + _sumOfSquares += value * value; + _count++; + } + } + + @override + void reset() { + _value = null; + _min = null; + _max = null; + _total = 0; + _count = 0; + _sumOfSquares = 0; + _frequencyMap.clear(); + } + + /// The minimum value recorded. + /// + /// Returns `null` if no value has been set. + num? get min => _min; + + /// The maximum value recorded. + /// + /// Returns `null` if no value has been set. + num? get max => _max; + + /// The average of all recorded values. + /// + /// Returns `null` if no value has been set. + num? get average => _count > 0 ? _total / _count : null; + + /// Calculates the rate of the current value relative to a reference value. + /// + /// - [value]: The reference value. + /// - Returns: The rate as a proportion (current value / reference value). + /// If the reference value is `0` or the current value is `null`, returns `0`. + num rate(num value) { + if (value == 0 || _value == null) { + return 0; + } + return _value! / value; + } + + /// Calculates the variance of the recorded values. + /// + /// Returns `null` if fewer than 2 values have been recorded. + num? get variance { + if (_count < 2) return null; + return (_sumOfSquares - (_total * _total) / _count) / _count; + } + + /// Calculates the standard deviation of the recorded values. + /// + /// Returns `null` if fewer than 2 values have been recorded. + num? get standardDeviation { + final varianceValue = variance; + return varianceValue != null ? sqrt(varianceValue) : null; + } + + /// Returns the mode of the recorded values. + /// + /// The mode is the value that appears most frequently. + /// If there are multiple values with the same highest frequency, returns the first one. + /// Returns `null` if no values have been recorded. + num? get mode { + if (_frequencyMap.isEmpty) return null; + + num? modeValue; + int maxFrequency = 0; + + _frequencyMap.forEach((value, frequency) { + if (frequency > maxFrequency) { + modeValue = value; + maxFrequency = frequency; + } + }); + + return modeValue; + } +} diff --git a/lib/src/sim_property.dart b/lib/src/sim_property.dart new file mode 100644 index 0000000..5836afc --- /dev/null +++ b/lib/src/sim_property.dart @@ -0,0 +1,18 @@ +/// A base class for simulation properties. +/// +/// This class provides a common interface for properties used in discrete event simulations, +/// such as counters, numeric metrics, or other tracked values. +abstract class SimProperty { + /// The name of this property (optional). + /// + /// Useful for identifying the property in logs or reports. + final String name; + + /// Creates a new [SimProperty] instance. + /// + /// Optionally, provide a [name] to identify the property. + SimProperty({this.name = ''}); + + /// Resets the property to its initial state. + void reset(); +} diff --git a/lib/src/sim_result.dart b/lib/src/sim_result.dart index d04b176..e78853e 100644 --- a/lib/src/sim_result.dart +++ b/lib/src/sim_result.dart @@ -1,14 +1,20 @@ import 'dart:collection'; import 'package:simdart/src/internal/sim_result_interface.dart'; +import 'package:simdart/src/sim_counter.dart'; +import 'package:simdart/src/sim_num.dart'; import 'package:simdart/src/simulation_track.dart'; class SimResult implements SimResultInterface { SimResult( {required this.duration, required this.startTime, - required List? tracks}) - : tracks = tracks != null ? UnmodifiableListView(tracks) : null; + required List? tracks, + required Map numProperties, + required Map counterProperties}) + : tracks = tracks != null ? UnmodifiableListView(tracks) : null, + numProperties = UnmodifiableMapView(numProperties), + counterProperties = UnmodifiableMapView(counterProperties); @override final int duration; @@ -17,4 +23,7 @@ class SimResult implements SimResultInterface { final int startTime; final List? tracks; + + final Map numProperties; + final Map counterProperties; } diff --git a/lib/src/simdart.dart b/lib/src/simdart.dart index 78ce43f..ff2ad5b 100644 --- a/lib/src/simdart.dart +++ b/lib/src/simdart.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; import 'dart:math'; +import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; import 'package:simdart/src/internal/event_action.dart'; @@ -9,11 +10,12 @@ import 'package:simdart/src/internal/event_scheduler_interface.dart'; import 'package:simdart/src/internal/now_interface.dart'; import 'package:simdart/src/internal/repeat_event_action.dart'; import 'package:simdart/src/internal/resource.dart'; -import 'package:simdart/src/internal/time_action.dart'; -import 'package:simdart/src/internal/time_loop.dart'; import 'package:simdart/src/internal/sim_configuration_interface.dart'; +import 'package:simdart/src/internal/time_action.dart'; import 'package:simdart/src/interval.dart'; import 'package:simdart/src/resource_configurator.dart'; +import 'package:simdart/src/sim_counter.dart'; +import 'package:simdart/src/sim_num.dart'; import 'package:simdart/src/sim_result.dart'; import 'package:simdart/src/simulation_track.dart'; import 'package:simdart/src/start_time_handling.dart'; @@ -43,28 +45,16 @@ class SimDart /// /// - [executionPriority]: Defines the priority of task execution in the simulation. SimDart( - {StartTimeHandling startTimeHandling = StartTimeHandling.throwErrorIfPast, - int? now, + {this.startTimeHandling = StartTimeHandling.throwErrorIfPast, + int now = 0, this.secondarySortByName = false, this.includeTracks = false, - this.executionPriorityCounter = 0, + this.executionPriority = 0, int? seed}) - : random = Random(seed) { - _loop = TimeLoop( - now: now, - includeTracks: includeTracks, - beforeRun: _beforeRun, - executionPriorityCounter: executionPriorityCounter, - startTimeHandling: startTimeHandling); - } - - late final TimeLoop _loop; + : random = Random(seed), + _now = now; - @override - final int executionPriorityCounter; - - @override - final bool includeTracks; + bool _hasRun = false; /// Determines whether events with the same start time are sorted by their event name. /// @@ -75,6 +65,8 @@ class SimDart final bool secondarySortByName; final Map _resources = {}; + final Map _numProperties = {}; + final Map _counterProperties = {}; /// Holds the configurations for the resources in the simulator. /// @@ -93,6 +85,44 @@ class SimDart /// to await the opportunity to be processed once the resource is released. final Queue _waitingForResource = Queue(); + /// Queue that holds the [TimeAction] instances to be executed at their respective times. + final PriorityQueue _actions = PriorityQueue( + (a, b) { + final primaryComparison = a.start.compareTo(b.start); + if (primaryComparison != 0) { + return primaryComparison; + } + return a.secondaryCompareTo(b); + }, + ); + + @override + final StartTimeHandling startTimeHandling; + + @override + final int executionPriority; + + int _executionCount = 0; + + @override + final bool includeTracks; + + int? _startTime; + + int _duration = 0; + + bool _nextEventScheduled = false; + + late int? _until; + + @override + int get now => _now; + late int _now; + + List? _tracks; + + Completer? _terminator; + void _beforeRun() { for (ResourceConfiguration rc in ResourcesConfiguratorHelper.iterable(configurator: resources)) { @@ -107,7 +137,35 @@ class SimDart /// - [until]: The time at which execution should stop. Execution will include events /// scheduled at this time (inclusive). If null, execution will continue indefinitely. Future run({int? until}) async { - return _loop.run(until: until); + if (_hasRun) { + throw StateError('The simulation has already been run.'); + } + + _hasRun = true; + if (until != null && _now > until) { + throw ArgumentError('`now` must be less than or equal to `until`.'); + } + _until = until; + + if (_terminator != null) { + return _buildResult(); + } + if (_actions.isEmpty) { + _duration = 0; + _startTime = 0; + return _buildResult(); + } + _duration = 0; + _startTime = null; + + _beforeRun(); + + _terminator = Completer(); + _scheduleNextEvent(); + await _terminator?.future; + _duration = _now - (_startTime ?? 0); + _terminator = null; + return _buildResult(); } @override @@ -184,7 +242,7 @@ class SimDart start ??= now; if (interval != null && rejectedEventPolicy != null) { - _loop.addAction(RepeatEventAction( + _addAction(RepeatEventAction( sim: this, rejectedEventPolicy: rejectedEventPolicy, start: start, @@ -193,7 +251,7 @@ class SimDart resourceId: resourceId, interval: interval)); } else { - _loop.addAction(EventAction( + _addAction(EventAction( sim: this, start: start, eventName: name, @@ -204,11 +262,63 @@ class SimDart } } - @override - int get now => _loop.now; + SimResult _buildResult() { + return SimResult( + startTime: _startTime ?? 0, + duration: _duration, + tracks: _tracks, + numProperties: _numProperties, + counterProperties: _counterProperties); + } - @override - StartTimeHandling get startTimeHandling => _loop.startTimeHandling; + void _scheduleNextEvent() { + assert(!_nextEventScheduled, 'Multiple schedules for the next event.'); + _nextEventScheduled = true; + if (executionPriority == 0 || _executionCount < executionPriority) { + _executionCount++; + Future.microtask(_consumeFirstEvent); + } else { + _executionCount = 0; + Future.delayed(Duration.zero, _consumeFirstEvent); + } + } + + void _addAction(TimeAction action) { + _actions.add(action); + } + + void _addTrack(SimulationTrack track) { + _tracks ??= []; + _tracks!.add(track); + } + + Future _consumeFirstEvent() async { + _nextEventScheduled = false; + if (_actions.isEmpty) { + _terminator?.complete(); + return; + } + + TimeAction action = _actions.removeFirst(); + + // Advance the simulation time to the action's start time. + if (action.start > now) { + _now = action.start; + if (_until != null && now > _until!) { + _startTime ??= now; + _terminator?.complete(); + return; + } + } else if (action.start < now) { + action.start = now; + } + + _startTime ??= now; + + action.execute(); + + _scheduleNextEvent(); + } } /// Defines the behavior of the interval after a newly created event has been rejected. @@ -248,12 +358,12 @@ class SimDartHelper { /// Adds an [TimeAction] to the loop. static void addAction({required SimDart sim, required TimeAction action}) { - sim._loop.addAction(action); + sim._addAction(action); } static void restoreWaitingEventsForResource({required SimDart sim}) { while (sim._waitingForResource.isNotEmpty) { - sim._loop.addAction(sim._waitingForResource.removeFirst()); + sim._addAction(sim._waitingForResource.removeFirst()); } } @@ -275,10 +385,19 @@ class SimDartHelper { for (Resource resource in sim._resources.values) { resourceUsage[resource.id] = resource.queue.length; } - sim._loop.addTrack(SimulationTrack( + sim._addTrack(SimulationTrack( status: status, name: eventName, time: sim.now, resourceUsage: resourceUsage)); } + + static SimCounter counter({required SimDart sim, required String name}) { + return sim._counterProperties + .putIfAbsent(name, () => SimCounter(name: name)); + } + + static SimNum num({required SimDart sim, required String name}) { + return sim._numProperties.putIfAbsent(name, () => SimNum(name: name)); + } } diff --git a/test/process_test.dart b/test/process_test.dart index a919150..160b165 100644 --- a/test/process_test.dart +++ b/test/process_test.dart @@ -1,5 +1,4 @@ import 'package:simdart/simdart.dart'; -import 'package:simdart/src/sim_result.dart'; import 'package:test/test.dart'; import 'track_tester.dart'; diff --git a/test/property_test.dart b/test/property_test.dart new file mode 100644 index 0000000..cf8448c --- /dev/null +++ b/test/property_test.dart @@ -0,0 +1,105 @@ +import 'package:simdart/src/sim_counter.dart'; +import 'package:simdart/src/sim_num.dart'; +import 'package:test/test.dart'; + +void main() { + group('SimCounter', () { + late SimCounter counter; + + setUp(() { + counter = SimCounter(name: 'Test Counter'); + }); + + test('Initial count should be 0', () { + expect(counter.value, 0); + }); + + test('Increment should increase count by 1', () { + counter.inc(); + expect(counter.value, 1); + }); + + test('Increment by value should increase count by specified amount', () { + counter.incBy(5); + expect(counter.value, 5); + }); + + test('Increment by negative value should not change count', () { + counter.incBy(-3); + expect(counter.value, 0); + }); + + test('Reset should set count to 0', () { + counter.incBy(10); + counter.reset(); + expect(counter.value, 0); + }); + }); + group('SimNum', () { + late SimNum metric; + + setUp(() { + metric = SimNum(name: 'Test Metric'); + }); + + test('Initial value should be null', () { + expect(metric.value, isNull); + }); + + test('Setting value should update current value', () { + metric.value = 10.0; + expect(metric.value, 10.0); + }); + + test('Setting null value should not update min, max, or average', () { + metric.value = 10.0; + metric.value = null; + expect(metric.min, 10.0); + expect(metric.max, 10.0); + expect(metric.average, 10.0); + }); + + test('Min and max should track smallest and largest values', () { + metric.value = 10.0; + metric.value = 5.0; + metric.value = 15.0; + expect(metric.min, 5.0); + expect(metric.max, 15.0); + }); + + test('Average should calculate correctly', () { + metric.value = 10.0; + metric.value = 20.0; + metric.value = 30.0; + expect(metric.average, 20.0); + }); + + test('Variance should calculate correctly', () { + metric.value = 10.0; + metric.value = 20.0; + metric.value = 30.0; + expect(metric.variance, closeTo(66.666, 0.001)); + }); + + test('Standard deviation should calculate correctly', () { + metric.value = 10.0; + metric.value = 20.0; + metric.value = 30.0; + expect(metric.standardDeviation, closeTo(8.164, 0.001)); + }); + + test('Rate', () { + metric.value = 10.0; + expect(0.1, metric.rate(100)); + }); + + test('Reset should clear all values', () { + metric.value = 10.0; + metric.reset(); + expect(metric.value, isNull); + expect(metric.min, isNull); + expect(metric.max, isNull); + expect(metric.average, isNull); + }); + }); +} diff --git a/test/repeat_process_test.dart b/test/repeat_process_test.dart index 226e53c..74d4c74 100644 --- a/test/repeat_process_test.dart +++ b/test/repeat_process_test.dart @@ -1,5 +1,4 @@ import 'package:simdart/simdart.dart'; -import 'package:simdart/src/sim_result.dart'; import 'package:test/test.dart'; import 'track_tester.dart'; diff --git a/test/resource_test.dart b/test/resource_test.dart index e4889da..cd8338a 100644 --- a/test/resource_test.dart +++ b/test/resource_test.dart @@ -1,5 +1,4 @@ import 'package:simdart/simdart.dart'; -import 'package:simdart/src/sim_result.dart'; import 'package:test/test.dart'; import 'track_tester.dart'; diff --git a/test/time_loop_test.dart b/test/time_loop_test.dart index f841ba1..eac4b47 100644 --- a/test/time_loop_test.dart +++ b/test/time_loop_test.dart @@ -1,6 +1,6 @@ import 'package:simdart/simdart.dart'; import 'package:simdart/src/internal/time_action.dart'; -import 'package:simdart/src/internal/time_loop.dart'; +import 'package:simdart/src/simdart.dart'; import 'package:test/test.dart'; class TestAction extends TimeAction { @@ -18,23 +18,27 @@ class TestAction extends TimeAction { void main() { group('TimeLoop', () { test('Loop', () async { - TimeLoop loop = TimeLoop( + SimDart sim = SimDart( includeTracks: true, - now: null, - executionPriorityCounter: 0, - beforeRun: () {}, + executionPriority: 0, startTimeHandling: StartTimeHandling.throwErrorIfPast); List names = []; - loop.addAction(TestAction(start: 0, name: 'A', names: names)); - loop.addAction(TestAction(start: 1, name: 'B', names: names)); - loop.addAction(TestAction(start: 10, name: 'C', names: names)); - loop.addAction(TestAction(start: 5, name: 'D', names: names)); - loop.addAction(TestAction(start: 2, name: 'E', names: names)); - loop.addAction(TestAction(start: 9, name: 'F', names: names)); - - await loop.run(); + SimDartHelper.addAction( + sim: sim, action: TestAction(start: 0, name: 'A', names: names)); + SimDartHelper.addAction( + sim: sim, action: TestAction(start: 1, name: 'B', names: names)); + SimDartHelper.addAction( + sim: sim, action: TestAction(start: 10, name: 'C', names: names)); + SimDartHelper.addAction( + sim: sim, action: TestAction(start: 5, name: 'D', names: names)); + SimDartHelper.addAction( + sim: sim, action: TestAction(start: 2, name: 'E', names: names)); + SimDartHelper.addAction( + sim: sim, action: TestAction(start: 9, name: 'F', names: names)); + + await sim.run(); expect(names, ['A', 'B', 'E', 'D', 'F', 'C']); }); diff --git a/test/track_tester.dart b/test/track_tester.dart index 7ec07ca..77e33b0 100644 --- a/test/track_tester.dart +++ b/test/track_tester.dart @@ -1,5 +1,4 @@ import 'package:simdart/simdart.dart'; -import 'package:simdart/src/sim_result.dart'; import 'package:test/expect.dart'; class TrackTester { diff --git a/test/wait_test.dart b/test/wait_test.dart index c3e7278..37c9573 100644 --- a/test/wait_test.dart +++ b/test/wait_test.dart @@ -1,5 +1,4 @@ import 'package:simdart/simdart.dart'; -import 'package:simdart/src/sim_result.dart'; import 'package:test/test.dart'; import 'track_tester.dart'; From c38e56b421fb6e3785b57dcc3711a2b9ffec852b Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Wed, 12 Feb 2025 19:37:53 -0300 Subject: [PATCH 07/10] Renaming EventContext to SimContext. #2 --- example/example.dart | 2 +- lib/simdart.dart | 2 +- lib/src/event.dart | 4 ++-- lib/src/internal/event_action.dart | 4 ++-- lib/src/internal/resource.dart | 16 ++++++++-------- lib/src/{event_context.dart => sim_context.dart} | 7 +++---- test/process_test.dart | 2 +- test/resource_test.dart | 4 ++-- test/wait_test.dart | 2 +- 9 files changed, 21 insertions(+), 22 deletions(-) rename lib/src/{event_context.dart => sim_context.dart} (83%) diff --git a/example/example.dart b/example/example.dart index 11ee7aa..63afe48 100644 --- a/example/example.dart +++ b/example/example.dart @@ -12,7 +12,7 @@ void main() async { print('duration: ${result.duration}'); } -void _a(EventContext context) async { +void _a(SimContext context) async { await context.wait(2); context.process(event: _a, delay: 2, name: 'A'); } diff --git a/lib/simdart.dart b/lib/simdart.dart index 68e6f2f..1d5cd48 100644 --- a/lib/simdart.dart +++ b/lib/simdart.dart @@ -1,5 +1,5 @@ export 'src/event.dart'; -export 'src/event_context.dart'; +export 'src/sim_context.dart'; export 'src/interval.dart'; export 'src/observable.dart'; export 'src/resource_configurator.dart' hide ResourcesConfiguratorHelper; diff --git a/lib/src/event.dart b/lib/src/event.dart index df8a9de..707fa78 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -3,5 +3,5 @@ import 'package:simdart/simdart.dart'; /// The event to be executed. /// /// A function that represents an event in the simulation. It receives an -/// [EventContext] that provides data about the event's execution state and context. -typedef Event = void Function(EventContext context); +/// [SimContext] that provides data about the event's execution state and context. +typedef Event = void Function(SimContext context); diff --git a/lib/src/internal/event_action.dart b/lib/src/internal/event_action.dart index 9d83024..05f1fc5 100644 --- a/lib/src/internal/event_action.dart +++ b/lib/src/internal/event_action.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; -import 'package:simdart/src/event_context.dart'; +import 'package:simdart/src/sim_context.dart'; import 'package:simdart/src/internal/resource.dart'; import 'package:simdart/src/internal/time_action.dart'; import 'package:simdart/src/interval.dart'; @@ -13,7 +13,7 @@ import 'package:simdart/src/simdart.dart'; import 'package:simdart/src/simulation_track.dart'; @internal -class EventAction extends TimeAction implements EventContext { +class EventAction extends TimeAction implements SimContext { EventAction( {required SimDart sim, required super.start, diff --git a/lib/src/internal/resource.dart b/lib/src/internal/resource.dart index 014db3b..3e21cda 100644 --- a/lib/src/internal/resource.dart +++ b/lib/src/internal/resource.dart @@ -1,20 +1,20 @@ import 'package:meta/meta.dart'; -import 'package:simdart/src/event_context.dart'; +import 'package:simdart/src/sim_context.dart'; @internal abstract class Resource { final String id; final int capacity; - final List queue = []; - final bool Function(EventContext context)? acquisitionRule; + final List queue = []; + final bool Function(SimContext context)? acquisitionRule; - final List waiting = []; + final List waiting = []; Resource({required this.id, this.capacity = 1, this.acquisitionRule}); - bool acquire(EventContext event); + bool acquire(SimContext event); - void release(EventContext event); + void release(SimContext event); bool isAvailable(); } @@ -24,7 +24,7 @@ class LimitedResource extends Resource { LimitedResource({required super.id, super.capacity, super.acquisitionRule}); @override - bool acquire(EventContext event) { + bool acquire(SimContext event) { if (acquisitionRule != null && !acquisitionRule!(event)) { // waiting.add(event); return false; @@ -39,7 +39,7 @@ class LimitedResource extends Resource { } @override - void release(EventContext event) { + void release(SimContext event) { queue.remove(event); } diff --git a/lib/src/event_context.dart b/lib/src/sim_context.dart similarity index 83% rename from lib/src/event_context.dart rename to lib/src/sim_context.dart index ed5a65e..3a6a5db 100644 --- a/lib/src/event_context.dart +++ b/lib/src/sim_context.dart @@ -4,11 +4,10 @@ import 'dart:math'; import 'package:simdart/simdart.dart'; import 'package:simdart/src/internal/event_scheduler_interface.dart'; -/// Represents the context of an event in the simulation. +/// Represents the context of the simulation. /// -/// Encapsulates the information and state of an event being executed -/// within the simulation. -abstract interface class EventContext implements EventSchedulerInterface { +/// Encapsulates the information and state of the simulation. +abstract interface class SimContext implements EventSchedulerInterface { /// Pauses the execution of the event for the specified [delay] in simulation time. /// /// The event is re-added to the simulation's event queue and will resume after diff --git a/test/process_test.dart b/test/process_test.dart index 160b165..ef2854f 100644 --- a/test/process_test.dart +++ b/test/process_test.dart @@ -3,7 +3,7 @@ import 'package:test/test.dart'; import 'track_tester.dart'; -Future emptyEvent(EventContext context) async {} +Future emptyEvent(SimContext context) async {} void main() { group('Process', () { diff --git a/test/resource_test.dart b/test/resource_test.dart index cd8338a..6c6be02 100644 --- a/test/resource_test.dart +++ b/test/resource_test.dart @@ -77,11 +77,11 @@ void main() { SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); sim.resources.limited(id: 'r', capacity: 2); - eventA(EventContext context) async { + eventA(SimContext context) async { await context.wait(10); } - eventB(EventContext context) async {} + eventB(SimContext context) async {} sim.resources.limited(id: 'resource', capacity: 2); diff --git a/test/wait_test.dart b/test/wait_test.dart index 37c9573..9da8bb9 100644 --- a/test/wait_test.dart +++ b/test/wait_test.dart @@ -3,7 +3,7 @@ import 'package:test/test.dart'; import 'track_tester.dart'; -Future emptyEvent(EventContext context) async {} +Future emptyEvent(SimContext context) async {} void main() { group('Wait', () { From 64e96b463db29759d3198cd0fb5669b11f5ba56c Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Wed, 12 Feb 2025 20:07:34 -0300 Subject: [PATCH 08/10] Allow creating new resources during simulation. #2 --- CHANGELOG.md | 1 + README.md | 1 - lib/simdart.dart | 2 +- lib/src/internal/event_action.dart | 4 ++ .../internal/event_scheduler_interface.dart | 3 +- lib/src/resource_configurator.dart | 43 ------------------- lib/src/resources.dart | 31 +++++++++++++ lib/src/sim_context.dart | 6 ++- lib/src/simdart.dart | 29 +++++-------- 9 files changed, 54 insertions(+), 66 deletions(-) delete mode 100644 lib/src/resource_configurator.dart create mode 100644 lib/src/resources.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 72412ad..2224b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Setting to determine how often `Future.delayed` is used instead of `Future.microtask` during event execution to allow GUI refresh. * Adding numeric properties and counter. +* Allow creating new resources during simulation. ## 0.1.0 diff --git a/README.md b/README.md index d6a679c..9359e64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![](https://img.shields.io/pub/v/simdart.svg)](https://pub.dev/packages/simdart) [![](https://img.shields.io/badge/%F0%9F%91%8D%20and%20%E2%AD%90-are%20free-yellow)](#) -[![](https://img.shields.io/badge/Under%20Development-blue)](#) ![](https://simdart.github.io/simdart-assets/simdart-text-128h.png) diff --git a/lib/simdart.dart b/lib/simdart.dart index 1d5cd48..6aa73c3 100644 --- a/lib/simdart.dart +++ b/lib/simdart.dart @@ -2,7 +2,7 @@ export 'src/event.dart'; export 'src/sim_context.dart'; export 'src/interval.dart'; export 'src/observable.dart'; -export 'src/resource_configurator.dart' hide ResourcesConfiguratorHelper; +export 'src/resources.dart' hide ResourcesHelper; export 'src/simdart.dart' hide SimDartHelper; export 'src/simulation_track.dart'; export 'src/start_time_handling.dart'; diff --git a/lib/src/internal/event_action.dart b/lib/src/internal/event_action.dart index 05f1fc5..3359f1a 100644 --- a/lib/src/internal/event_action.dart +++ b/lib/src/internal/event_action.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; +import 'package:simdart/src/resources.dart'; import 'package:simdart/src/sim_context.dart'; import 'package:simdart/src/internal/resource.dart'; import 'package:simdart/src/internal/time_action.dart'; @@ -175,4 +176,7 @@ class EventAction extends TimeAction implements SimContext { SimNum num(String name) { return SimDartHelper.num(sim: _sim, name: name); } + + @override + Resources get resources => _sim.resources; } diff --git a/lib/src/internal/event_scheduler_interface.dart b/lib/src/internal/event_scheduler_interface.dart index ef5a644..6a14d5a 100644 --- a/lib/src/internal/event_scheduler_interface.dart +++ b/lib/src/internal/event_scheduler_interface.dart @@ -1,11 +1,10 @@ import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; -import 'package:simdart/src/internal/now_interface.dart'; import 'package:simdart/src/interval.dart'; import 'package:simdart/src/simdart.dart'; @internal -abstract interface class EventSchedulerInterface implements NowInterface { +abstract interface class EventSchedulerInterface { /// Schedules a new event to occur repeatedly based on the specified interval configuration. /// /// [event] is the function that represents the action to be executed when the event occurs. diff --git a/lib/src/resource_configurator.dart b/lib/src/resource_configurator.dart deleted file mode 100644 index a948676..0000000 --- a/lib/src/resource_configurator.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:meta/meta.dart'; - -/// This class is responsible for configuring the resources -/// available in the simulator. It allows adding resource configurations, but once -/// the simulator starts running, no new configurations can be added. -/// -/// ## Note -/// After the simulation starts running, no new configurations can be -/// added. Configurations need to be defined before the simulation starts. -class ResourcesConfigurator { - final List _configurations = []; - - /// Configures a resource with limited capacity. - /// - /// This method adds a resource configuration with the specified capacity. - /// The resource will be configured as limited, meaning it will have a maximum - /// capacity defined by the [capacity] parameter. - /// - /// - [id]: The unique identifier of the resource (required). - /// - [capacity]: The maximum capacity of the resource. The default value is 1. - void limited({required String id, int capacity = 1}) { - _configurations - .add(LimitedResourceConfiguration(id: id, capacity: capacity)); - } -} - -abstract class ResourceConfiguration { - ResourceConfiguration({required this.id, required this.capacity}); - - final String id; - final int capacity; -} - -class LimitedResourceConfiguration extends ResourceConfiguration { - LimitedResourceConfiguration({required super.id, required super.capacity}); -} - -@internal -class ResourcesConfiguratorHelper { - static Iterable iterable( - {required ResourcesConfigurator configurator}) => - configurator._configurations; -} diff --git a/lib/src/resources.dart b/lib/src/resources.dart new file mode 100644 index 0000000..1e3df57 --- /dev/null +++ b/lib/src/resources.dart @@ -0,0 +1,31 @@ +import 'package:meta/meta.dart'; +import 'package:simdart/src/internal/resource.dart'; +import 'package:simdart/src/simdart.dart'; + +/// This class is responsible for creating the resources +/// available in the simulator. +class Resources { + Resources._(SimDart sim) : _sim = sim; + + final SimDart _sim; + + /// Creates a resource with limited capacity. + /// + /// This method adds a resource with the specified capacity. + /// The resource will be configured as limited, meaning it will have a maximum + /// capacity defined by the [capacity] parameter. + /// + /// - [id]: The unique identifier of the resource (required). + /// - [capacity]: The maximum capacity of the resource. The default value is 1. + void limited({required String id, int capacity = 1}) { + SimDartHelper.addResource( + sim: _sim, + resourceId: id, + create: () => LimitedResource(id: id, capacity: capacity)); + } +} + +@internal +class ResourcesHelper { + static Resources build(SimDart sim) => Resources._(sim); +} diff --git a/lib/src/sim_context.dart b/lib/src/sim_context.dart index 3a6a5db..3401276 100644 --- a/lib/src/sim_context.dart +++ b/lib/src/sim_context.dart @@ -3,11 +3,13 @@ import 'dart:math'; import 'package:simdart/simdart.dart'; import 'package:simdart/src/internal/event_scheduler_interface.dart'; +import 'package:simdart/src/internal/now_interface.dart'; /// Represents the context of the simulation. /// /// Encapsulates the information and state of the simulation. -abstract interface class SimContext implements EventSchedulerInterface { +abstract interface class SimContext + implements EventSchedulerInterface, NowInterface { /// Pauses the execution of the event for the specified [delay] in simulation time. /// /// The event is re-added to the simulation's event queue and will resume after @@ -32,4 +34,6 @@ abstract interface class SimContext implements EventSchedulerInterface { /// - [name]: The name of the numeric metric. This is used to identify the metric in logs or reports. /// - Returns: A new instance of [SimNum]. SimNum num(String name); + + Resources get resources; } diff --git a/lib/src/simdart.dart b/lib/src/simdart.dart index ff2ad5b..0ac2c5e 100644 --- a/lib/src/simdart.dart +++ b/lib/src/simdart.dart @@ -13,7 +13,7 @@ import 'package:simdart/src/internal/resource.dart'; import 'package:simdart/src/internal/sim_configuration_interface.dart'; import 'package:simdart/src/internal/time_action.dart'; import 'package:simdart/src/interval.dart'; -import 'package:simdart/src/resource_configurator.dart'; +import 'package:simdart/src/resources.dart'; import 'package:simdart/src/sim_counter.dart'; import 'package:simdart/src/sim_num.dart'; import 'package:simdart/src/sim_result.dart'; @@ -64,15 +64,12 @@ class SimDart /// remains undefined for events with identical start times. final bool secondarySortByName; - final Map _resources = {}; final Map _numProperties = {}; final Map _counterProperties = {}; - /// Holds the configurations for the resources in the simulator. - /// - /// Once the simulation begins, no new resource configurations can be added to - /// this list. - final ResourcesConfigurator resources = ResourcesConfigurator(); + /// Holds the resources in the simulator. + final Map _resources = {}; + late final Resources resources = ResourcesHelper.build(this); /// The instance of the random number generator used across the simulation. /// It is initialized once and reused to improve performance, avoiding the need to @@ -123,15 +120,6 @@ class SimDart Completer? _terminator; - void _beforeRun() { - for (ResourceConfiguration rc - in ResourcesConfiguratorHelper.iterable(configurator: resources)) { - if (rc is LimitedResourceConfiguration) { - _resources[rc.id] = LimitedResource(id: rc.id, capacity: rc.capacity); - } - } - } - /// Runs the simulation, processing events in chronological order. /// /// - [until]: The time at which execution should stop. Execution will include events @@ -158,8 +146,6 @@ class SimDart _duration = 0; _startTime = null; - _beforeRun(); - _terminator = Completer(); _scheduleNextEvent(); await _terminator?.future; @@ -377,6 +363,13 @@ class SimDartHelper { return sim._resources[resourceId]; } + static void addResource( + {required SimDart sim, + required String resourceId, + required Resource Function() create}) { + sim._resources.putIfAbsent(resourceId, create); + } + static void addSimulationTrack( {required SimDart sim, required String eventName, From d7b17450016684252702d1f39a0ad9243f975bff Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Wed, 12 Feb 2025 20:25:29 -0300 Subject: [PATCH 09/10] Removing SimResult interface. #2 --- lib/src/internal/sim_result_interface.dart | 20 -------------------- lib/src/sim_result.dart | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 24 deletions(-) delete mode 100644 lib/src/internal/sim_result_interface.dart diff --git a/lib/src/internal/sim_result_interface.dart b/lib/src/internal/sim_result_interface.dart deleted file mode 100644 index 2561f35..0000000 --- a/lib/src/internal/sim_result_interface.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:meta/meta.dart'; - -/// Represents the simulation result. -@internal -abstract interface class SimResultInterface { - /// The time, in simulated time units, when the simulation started. - /// This is the moment at which the first event is scheduled to be processed. - /// - /// For example, if the first process is scheduled to occur at time 10, - /// then the simulation start time would be 10. This value helps track when - /// the simulation officially begins its execution in terms of the simulation time. - int get startTime; - - /// The duration, in simulated time units, that the simulation took to execute. - /// - /// This value represents the total time elapsed during the processing of the simulation, - /// from the start to the completion of all event handling, in terms of the simulated environment. - /// It is used to track how much time has passed in the simulation model, not real-world time. - int get duration; -} diff --git a/lib/src/sim_result.dart b/lib/src/sim_result.dart index e78853e..7a5a547 100644 --- a/lib/src/sim_result.dart +++ b/lib/src/sim_result.dart @@ -1,11 +1,11 @@ import 'dart:collection'; -import 'package:simdart/src/internal/sim_result_interface.dart'; import 'package:simdart/src/sim_counter.dart'; import 'package:simdart/src/sim_num.dart'; import 'package:simdart/src/simulation_track.dart'; -class SimResult implements SimResultInterface { +/// Represents the simulation result. +class SimResult { SimResult( {required this.duration, required this.startTime, @@ -16,10 +16,19 @@ class SimResult implements SimResultInterface { numProperties = UnmodifiableMapView(numProperties), counterProperties = UnmodifiableMapView(counterProperties); - @override + /// The duration, in simulated time units, that the simulation took to execute. + /// + /// This value represents the total time elapsed during the processing of the simulation, + /// from the start to the completion of all event handling, in terms of the simulated environment. + /// It is used to track how much time has passed in the simulation model, not real-world time. final int duration; - @override + /// The time, in simulated time units, when the simulation started. + /// This is the moment at which the first event is scheduled to be processed. + /// + /// For example, if the first process is scheduled to occur at time 10, + /// then the simulation start time would be 10. This value helps track when + /// the simulation officially begins its execution in terms of the simulation time. final int startTime; final List? tracks; From 1df8984885375bc4b5520691e8ac2d05a627ef03 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Leite de Andrade Date: Sat, 22 Feb 2025 16:21:10 -0300 Subject: [PATCH 10/10] Add acquire and release resource methods * Renaming methods. #11 * Simplifying the API. Event receiving SimDart instance. * Adding acquire and release methods for resources. #11 * Removing unused import. * Fixing unrecovered events after waiting for resource. #11 * Fixing async methods. #11 * Updating README. #11 * Updating CHANGELOG. #11 * Formatting. #11 * Fixing hidden member name. #11 --- CHANGELOG.md | 1 + README.md | 72 ++--- example/example.dart | 10 +- lib/simdart.dart | 5 +- lib/src/event.dart | 7 +- lib/src/internal/completer_action.dart | 15 ++ lib/src/internal/event_action.dart | 227 ++++++++-------- .../internal/event_scheduler_interface.dart | 51 ---- lib/src/internal/now_interface.dart | 7 - lib/src/internal/repeat_event_action.dart | 57 ++-- lib/src/internal/resource.dart | 22 +- lib/src/internal/resources_context_impl.dart | 34 +++ lib/src/internal/resources_impl.dart | 28 ++ .../internal/sim_configuration_interface.dart | 24 -- lib/src/internal/simdart_interface.dart | 59 +++++ lib/src/internal/time_action.dart | 11 +- lib/src/resources.dart | 28 +- lib/src/resources_context.dart | 22 ++ lib/src/sim_context.dart | 33 +-- lib/src/simdart.dart | 247 +++++++----------- lib/src/simulation_track.dart | 12 +- test/process_test.dart | 30 +-- test/repeat_process_test.dart | 109 +++++--- test/resource_test.dart | 133 ++++++---- test/time_loop_test.dart | 26 +- test/track_tester.dart | 10 +- test/wait_test.dart | 106 +++++--- 27 files changed, 742 insertions(+), 644 deletions(-) create mode 100644 lib/src/internal/completer_action.dart delete mode 100644 lib/src/internal/event_scheduler_interface.dart delete mode 100644 lib/src/internal/now_interface.dart create mode 100644 lib/src/internal/resources_context_impl.dart create mode 100644 lib/src/internal/resources_impl.dart delete mode 100644 lib/src/internal/sim_configuration_interface.dart create mode 100644 lib/src/internal/simdart_interface.dart create mode 100644 lib/src/resources_context.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2224b0a..b5e56bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Setting to determine how often `Future.delayed` is used instead of `Future.microtask` during event execution to allow GUI refresh. * Adding numeric properties and counter. * Allow creating new resources during simulation. +* Methods for acquiring and releasing resources within events. ## 0.1.0 diff --git a/README.md b/README.md index 9359e64..c9cfebb 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ A collection of different interval types used to control event timing in simulat import 'package:simdart/simdart.dart'; void main() async { - final SimDart sim = SimDart(onTrack: (track) => print(track)); + final SimDart sim = SimDart(); sim.process(event: _a, name: 'A'); sim.process(event: _b, start: 5, name: 'B'); @@ -87,28 +87,34 @@ void main() async { await sim.run(); } -void _a(EventContext context) async { +Future _a(SimContext context) async { + print('[${context.now}][${context.eventName}] start'); await context.wait(10); context.process(event: _c, delay: 1, name: 'C'); + print('[${context.now}][${context.eventName}] end'); } -void _b(EventContext context) async { +Future _b(SimContext context) async { + print('[${context.now}][${context.eventName}] start'); await context.wait(1); + print('[${context.now}][${context.eventName}] end'); } -void _c(EventContext context) async { +Future _c(SimContext context) async { + print('[${context.now}][${context.eventName}] start'); await context.wait(10); + print('[${context.now}][${context.eventName}] end'); } ``` Output: ``` -[0][A][executed] -[5][B][executed] -[6][B][resumed] -[10][A][resumed] -[11][C][executed] -[21][C][resumed] +[0][A] start +[5][B] start +[6][B] end +[10][A] end +[11][C] start +[21][C] end ``` ### Resource capacity @@ -117,35 +123,35 @@ Output: import 'package:simdart/simdart.dart'; void main() async { - final SimDart sim = SimDart(onTrack: (track) => print(track)); + final SimDart sim = SimDart(); - sim.resources.limited(id: 'resource', capacity: 2); + sim.resources.limited(id: 'resource', capacity: 1); - sim.process(event: _a, name: 'A1', resourceId: 'resource'); - sim.process(event: _a, name: 'A2', start: 1, resourceId: 'resource'); - sim.process(event: _a, name: 'A3', start: 2, resourceId: 'resource'); - sim.process(event: _b, name: 'B', start: 3); + sim.process(event: _eventResource, name: 'A'); + sim.process(event: _eventResource, name: 'B'); await sim.run(); } -void _a(EventContext context) async { +Future _eventResource(SimContext context) async { + print('[${context.now}][${context.eventName}] acquiring resource...'); + await context.resources.acquire('resource'); + print('[${context.now}][${context.eventName}] resource acquired'); await context.wait(10); + print('[${context.now}][${context.eventName}] releasing resource...'); + context.resources.release('resource'); } -void _b(EventContext context) async {} ``` Output: ``` -[0][A1][executed] -[1][A2][executed] -[2][A3][rejected] -[3][B][executed] -[10][A1][resumed] -[10][A3][executed] -[11][A2][resumed] -[20][A3][resumed] +[0][A] acquiring resource... +[0][A] resource acquired +[0][B] acquiring resource... +[10][A] releasing resource... +[10][B] resource acquired +[20][B] releasing resource... ``` ### Repeating process @@ -154,23 +160,25 @@ Output: import 'package:simdart/simdart.dart'; void main() async { - final SimDart sim = SimDart(onTrack: (track) => print(track)); + final SimDart sim = SimDart(); sim.repeatProcess( event: _a, start: 1, - name: 'A', + name: (start) => 'A$start', interval: Interval.fixed(fixedInterval: 2, untilTime: 5)); await sim.run(); } -void _a(EventContext context) async {} +Future _a(SimContext context) async { + print('[${context.now}][${context.eventName}]'); +} ``` Output: ``` -[1][A][executed] -[3][A][executed] -[5][A][executed] +[1][A1] +[3][A3] +[5][A5] ``` \ No newline at end of file diff --git a/example/example.dart b/example/example.dart index 63afe48..194861d 100644 --- a/example/example.dart +++ b/example/example.dart @@ -3,16 +3,18 @@ import 'package:simdart/simdart.dart'; void main() async { final SimDart sim = SimDart(includeTracks: true); - sim.process(event: _a, name: 'A'); + sim.process(event: _eventA, name: 'A'); - SimResult result = await sim.run(until: 10); + SimResult result = await sim.run(); result.tracks?.forEach((track) => print(track)); print('startTime: ${result.startTime}'); print('duration: ${result.duration}'); } -void _a(SimContext context) async { +Future _eventA(SimContext context) async { await context.wait(2); - context.process(event: _a, delay: 2, name: 'A'); + context.process(event: _eventB, delay: 2, name: 'B'); } + +Future _eventB(SimContext context) async {} diff --git a/lib/simdart.dart b/lib/simdart.dart index 6aa73c3..6a96cce 100644 --- a/lib/simdart.dart +++ b/lib/simdart.dart @@ -1,8 +1,8 @@ export 'src/event.dart'; -export 'src/sim_context.dart'; export 'src/interval.dart'; export 'src/observable.dart'; -export 'src/resources.dart' hide ResourcesHelper; +export 'src/resources_context.dart'; +export 'src/resources.dart'; export 'src/simdart.dart' hide SimDartHelper; export 'src/simulation_track.dart'; export 'src/start_time_handling.dart'; @@ -10,3 +10,4 @@ export 'src/sim_result.dart'; export 'src/sim_num.dart'; export 'src/sim_property.dart'; export 'src/sim_counter.dart'; +export 'src/sim_context.dart'; diff --git a/lib/src/event.dart b/lib/src/event.dart index 707fa78..424d370 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,7 +1,6 @@ -import 'package:simdart/simdart.dart'; +import 'package:simdart/src/sim_context.dart'; /// The event to be executed. /// -/// A function that represents an event in the simulation. It receives an -/// [SimContext] that provides data about the event's execution state and context. -typedef Event = void Function(SimContext context); +/// A function that represents an event in the simulation. +typedef Event = Future Function(SimContext context); diff --git a/lib/src/internal/completer_action.dart b/lib/src/internal/completer_action.dart new file mode 100644 index 0000000..2656717 --- /dev/null +++ b/lib/src/internal/completer_action.dart @@ -0,0 +1,15 @@ +import 'package:meta/meta.dart'; +import 'package:simdart/src/internal/time_action.dart'; + +@internal +class CompleterAction extends TimeAction { + CompleterAction( + {required super.start, required this.complete, required super.order}); + + final Function complete; + + @override + void execute() { + complete.call(); + } +} diff --git a/lib/src/internal/event_action.dart b/lib/src/internal/event_action.dart index 3359f1a..0d18f2c 100644 --- a/lib/src/internal/event_action.dart +++ b/lib/src/internal/event_action.dart @@ -1,13 +1,14 @@ import 'dart:async'; -import 'dart:math'; import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; -import 'package:simdart/src/resources.dart'; -import 'package:simdart/src/sim_context.dart'; +import 'package:simdart/src/internal/completer_action.dart'; import 'package:simdart/src/internal/resource.dart'; +import 'package:simdart/src/internal/resources_context_impl.dart'; import 'package:simdart/src/internal/time_action.dart'; import 'package:simdart/src/interval.dart'; +import 'package:simdart/src/resources_context.dart'; +import 'package:simdart/src/sim_context.dart'; import 'package:simdart/src/sim_counter.dart'; import 'package:simdart/src/sim_num.dart'; import 'package:simdart/src/simdart.dart'; @@ -16,135 +17,136 @@ import 'package:simdart/src/simulation_track.dart'; @internal class EventAction extends TimeAction implements SimContext { EventAction( - {required SimDart sim, + {required this.sim, required super.start, required String? eventName, - required this.event, - required this.resourceId, - required this.onReject, - required this.secondarySortByName}) - : _sim = sim, - _eventName = eventName; + required this.event}) + : _eventName = eventName; /// The name of the event. final String? _eventName; + @override String get eventName => _eventName ?? hashCode.toString(); /// The event to be executed. final Event event; - final Function? onReject; - - final SimDart _sim; + final SimDart sim; @override - Random get random => _sim.random; - - /// The resource id required by the event. - final String? resourceId; - - bool _resourceAcquired = false; - - final bool secondarySortByName; - - /// Internal handler for resuming a waiting event. - void Function()? _resume; + late final ResourcesContext resources = ResourcesContextImpl(sim, this); @override - int secondaryCompareTo(TimeAction action) { - if (secondarySortByName && action is EventAction) { - return eventName.compareTo(action.eventName); - } - return 0; - } + int get now => sim.now; - bool get _canRun => resourceId == null || _resourceAcquired; + /// Internal handler for resuming a waiting event. + EventCompleter? _eventCompleter; @override void execute() { - final Function()? resume = _resume; - - if (resume != null) { - if (_sim.includeTracks) { - SimDartHelper.addSimulationTrack( - sim: _sim, eventName: eventName, status: Status.resumed); - } - // Resume the event if it is waiting, otherwise execute its action. - resume.call(); - return; + if (_eventCompleter != null) { + throw StateError('This event is yielding'); } - Resource? resource = - SimDartHelper.getResource(sim: _sim, resourceId: resourceId); - if (resource != null) { - _resourceAcquired = resource.acquire(this); + if (sim.includeTracks) { + SimDartHelper.addSimulationTrack( + sim: sim, eventName: eventName, status: Status.called); } - if (_sim.includeTracks) { - Status status = Status.executed; - if (!_canRun) { - status = Status.rejected; + _runEvent().then((_) { + if (_eventCompleter != null) { + SimDartHelper.error( + sim: sim, + msg: + "Next event is being scheduled, but the current one is still paused waiting for continuation. Did you forget to use 'await'?"); + return; } - SimDartHelper.addSimulationTrack( - sim: _sim, eventName: eventName, status: status); - } + SimDartHelper.scheduleNextAction(sim: sim); + }); + } - if (_canRun) { - _runEvent().then((_) { - if (_resourceAcquired) { - if (resource != null) { - resource.release(this); - _resourceAcquired = false; - } - // Event released some resource, others events need retry. - SimDartHelper.restoreWaitingEventsForResource(sim: _sim); - } - }); - } else { - onReject?.call(); - SimDartHelper.queueOnWaitingForResource(sim: _sim, action: this); - } + Future _runEvent() async { + await event(this); } @override Future wait(int delay) async { - if (_resume != null) { + if (_eventCompleter != null) { + SimDartHelper.error( + sim: sim, + msg: "The event is already waiting. Did you forget to use 'await'?"); return; } - start = _sim.now + delay; - // Adds it back to the loop to be resumed in the future. - SimDartHelper.addAction(sim: _sim, action: this); - - final Completer resume = Completer(); - _resume = () { - resume.complete(); - _resume = null; - }; - await resume.future; + if (sim.includeTracks) { + SimDartHelper.addSimulationTrack( + sim: sim, eventName: eventName, status: Status.yielded); + } + _eventCompleter = EventCompleter(event: this); + + // Schedule a complete to resume this event in the future. + SimDartHelper.addAction( + sim: sim, + action: CompleterAction( + start: sim.now + delay, + complete: _eventCompleter!.complete, + order: order)); + SimDartHelper.scheduleNextAction(sim: sim); + + await _eventCompleter!.future; + _eventCompleter = null; } - Future _runEvent() async { - return event(this); + Future acquireResource(String id) async { + if (_eventCompleter != null) { + SimDartHelper.error( + sim: sim, + msg: + "This event should be waiting for the resource to be released. Did you forget to use 'await'?"); + return; + } + Resource? resource = SimDartHelper.getResource(sim: sim, resourceId: id); + if (resource != null) { + bool acquired = resource.acquire(this); + if (!acquired) { + if (sim.includeTracks) { + SimDartHelper.addSimulationTrack( + sim: sim, eventName: eventName, status: Status.yielded); + } + _eventCompleter = EventCompleter(event: this); + resource.waiting.add(this); + SimDartHelper.scheduleNextAction(sim: sim); + await _eventCompleter!.future; + _eventCompleter = null; + return await acquireResource(id); + } + } } - @override - int get now => _sim.now; + void releaseResource(String id) { + Resource? resource = SimDartHelper.getResource(sim: sim, resourceId: id); + if (resource != null) { + if (resource.release(sim, this)) { + if (resource.waiting.isNotEmpty) { + //resource.waiting.removeAt(0).call(); + EventAction other = resource.waiting.removeAt(0); + // Schedule a complete to resume this event in the future. + SimDartHelper.addAction( + sim: sim, + action: CompleterAction( + start: sim.now, + complete: other._eventCompleter!.complete, + order: other.order)); + SimDartHelper.scheduleNextAction(sim: sim); + } + } + } + } @override - void process( - {required Event event, - String? resourceId, - String? name, - int? start, - int? delay}) { - _sim.process( - event: event, - resourceId: resourceId, - name: name, - start: start, - delay: delay); + void process({required Event event, String? name, int? start, int? delay}) { + sim.process(event: event, name: name, start: start, delay: delay); } @override @@ -153,30 +155,43 @@ class EventAction extends TimeAction implements SimContext { int? start, int? delay, required Interval interval, - RejectedEventPolicy rejectedEventPolicy = - RejectedEventPolicy.keepRepeating, - String? resourceId, - String? name}) { - _sim.repeatProcess( + StopCondition? stopCondition, + String Function(int start)? name}) { + sim.repeatProcess( event: event, - interval: interval, start: start, delay: delay, - rejectedEventPolicy: rejectedEventPolicy, - resourceId: resourceId, + interval: interval, + stopCondition: stopCondition, name: name); } @override SimCounter counter(String name) { - return SimDartHelper.counter(sim: _sim, name: name); + return sim.counter(name); } @override SimNum num(String name) { - return SimDartHelper.num(sim: _sim, name: name); + return sim.num(name); } +} - @override - Resources get resources => _sim.resources; +class EventCompleter { + EventCompleter({required this.event}); + + final Completer _completer = Completer(); + + final EventAction event; + + Future get future => _completer.future; + + void complete() { + if (event.sim.includeTracks) { + SimDartHelper.addSimulationTrack( + sim: event.sim, eventName: event.eventName, status: Status.resumed); + } + _completer.complete(); + event._eventCompleter = null; + } } diff --git a/lib/src/internal/event_scheduler_interface.dart b/lib/src/internal/event_scheduler_interface.dart deleted file mode 100644 index 6a14d5a..0000000 --- a/lib/src/internal/event_scheduler_interface.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:simdart/src/event.dart'; -import 'package:simdart/src/interval.dart'; -import 'package:simdart/src/simdart.dart'; - -@internal -abstract interface class EventSchedulerInterface { - /// Schedules a new event to occur repeatedly based on the specified interval configuration. - /// - /// [event] is the function that represents the action to be executed when the event occurs. - /// [start] is the absolute time at which the event should occur. If null, the event will - /// occur at the [now] simulation time. - /// [delay] is the number of time units after the [now] when the event has been scheduled. - /// It cannot be provided if [start] is specified. - /// [interval] defines the timing configuration for the event, including its start time and - /// the interval between repetitions. The specific details of the interval behavior depend - /// on the implementation of the [Interval]. - /// [resourceId] is an optional parameter that specifies the ID of the resource required by the event. - /// [name] is an optional identifier for the event. - /// [rejectedEventPolicy] defines the behavior of the interval after a newly created event has been rejected. - /// - /// Throws an [ArgumentError] if the provided interval configuration is invalid, such as - /// containing negative or inconsistent timing values. - void repeatProcess( - {required Event event, - int? start, - int? delay, - required Interval interval, - RejectedEventPolicy rejectedEventPolicy = - RejectedEventPolicy.keepRepeating, - String? resourceId, - String? name}); - - /// Schedules a new event to occur at a specific simulation time or after a delay. - /// - /// [event] is the function that represents the action to be executed when the event occurs. - /// [start] is the absolute time at which the event should occur. If null, the event will - /// occur at the [now] simulation time. - /// [delay] is the number of time units after the [now] when the event has been scheduled. - /// It cannot be provided if [start] is specified. - /// [resourceId] is an optional parameter that specifies the ID of the resource required by the event. - /// [name] is an optional identifier for the event. - /// - /// Throws an [ArgumentError] if both [start] and [delay] are provided or if [delay] is negative. - void process( - {required Event event, - String? resourceId, - String? name, - int? start, - int? delay}); -} diff --git a/lib/src/internal/now_interface.dart b/lib/src/internal/now_interface.dart deleted file mode 100644 index b8ed185..0000000 --- a/lib/src/internal/now_interface.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:meta/meta.dart'; - -@internal -abstract interface class NowInterface { - /// Gets the current simulation time. - int get now; -} diff --git a/lib/src/internal/repeat_event_action.dart b/lib/src/internal/repeat_event_action.dart index bec1e8c..91a3ee7 100644 --- a/lib/src/internal/repeat_event_action.dart +++ b/lib/src/internal/repeat_event_action.dart @@ -12,60 +12,37 @@ class RepeatEventAction extends TimeAction { required this.eventName, required this.event, required this.interval, - required this.resourceId, - required this.rejectedEventPolicy}); + required this.stopCondition}); /// The name of the event. - final String? eventName; + final String Function(int start)? eventName; - /// Defines the behavior of the interval after a newly created event has been rejected. - final RejectedEventPolicy rejectedEventPolicy; + final StopCondition? stopCondition; /// The event to be executed. final Event event; - /// The resource id required by the event. - final String? resourceId; - final Interval interval; final SimDart sim; - bool _discard = false; - @override void execute() { - if (_discard) { - return; - } //TODO Run directly without adding to the loop? - SimDartHelper.process( - sim: sim, - event: event, - start: null, - delay: null, - name: eventName, - resourceId: resourceId, - onReject: rejectedEventPolicy == RejectedEventPolicy.stopRepeating - ? _removeFromLoop - : null, - interval: null, - rejectedEventPolicy: null); - int? start = interval.nextStart(sim); - if (start != null) { - //TODO avoid start = now? - this.start = start; - SimDartHelper.addAction(sim: sim, action: this); + sim.process( + event: event, name: eventName != null ? eventName!(sim.now) : null); + bool repeat = true; + if (stopCondition != null) { + repeat = stopCondition!(sim); } - } - - void _removeFromLoop() { - _discard = true; - } - - @override - int secondaryCompareTo(TimeAction action) { - // Gain priority over event actions - return -1; + if (repeat) { + int? start = interval.nextStart(sim); + if (start != null) { + //TODO avoid start = now? + this.start = start; + SimDartHelper.addAction(sim: sim, action: this); + } + } + SimDartHelper.scheduleNextAction(sim: sim); } } diff --git a/lib/src/internal/resource.dart b/lib/src/internal/resource.dart index 3e21cda..3d9ad17 100644 --- a/lib/src/internal/resource.dart +++ b/lib/src/internal/resource.dart @@ -1,20 +1,22 @@ import 'package:meta/meta.dart'; -import 'package:simdart/src/sim_context.dart'; +import 'package:simdart/src/internal/event_action.dart'; +import 'package:simdart/src/simdart.dart'; @internal abstract class Resource { final String id; final int capacity; - final List queue = []; - final bool Function(SimContext context)? acquisitionRule; + final List queue = []; + final bool Function(EventAction event)? acquisitionRule; - final List waiting = []; + /// A queue that holds completer to resume events waiting for a resource to become available. + final List waiting = []; Resource({required this.id, this.capacity = 1, this.acquisitionRule}); - bool acquire(SimContext event); + bool acquire(EventAction event); - void release(SimContext event); + bool release(SimDart sim, EventAction event); bool isAvailable(); } @@ -24,9 +26,8 @@ class LimitedResource extends Resource { LimitedResource({required super.id, super.capacity, super.acquisitionRule}); @override - bool acquire(SimContext event) { + bool acquire(EventAction event) { if (acquisitionRule != null && !acquisitionRule!(event)) { - // waiting.add(event); return false; } if (isAvailable()) { @@ -34,13 +35,12 @@ class LimitedResource extends Resource { return true; } - // waiting.add(event); return false; } @override - void release(SimContext event) { - queue.remove(event); + bool release(SimDart sim, EventAction event) { + return queue.remove(event); } @override diff --git a/lib/src/internal/resources_context_impl.dart b/lib/src/internal/resources_context_impl.dart new file mode 100644 index 0000000..a92cf4b --- /dev/null +++ b/lib/src/internal/resources_context_impl.dart @@ -0,0 +1,34 @@ +import 'package:meta/meta.dart'; +import 'package:simdart/src/internal/event_action.dart'; +import 'package:simdart/src/internal/resource.dart'; +import 'package:simdart/src/resources_context.dart'; +import 'package:simdart/src/simdart.dart'; + +@internal +class ResourcesContextImpl extends ResourcesContext { + ResourcesContextImpl(super.sim, EventAction event) + : _sim = sim, + _event = event; + + final SimDart _sim; + final EventAction _event; + + @override + void release(String id) { + _event.releaseResource(id); + } + + @override + bool tryAcquire(String id) { + Resource? resource = SimDartHelper.getResource(sim: _sim, resourceId: id); + if (resource != null) { + return resource.acquire(_event); + } + return false; + } + + @override + Future acquire(String id) async { + return await _event.acquireResource(id); + } +} diff --git a/lib/src/internal/resources_impl.dart b/lib/src/internal/resources_impl.dart new file mode 100644 index 0000000..045584c --- /dev/null +++ b/lib/src/internal/resources_impl.dart @@ -0,0 +1,28 @@ +import 'package:meta/meta.dart'; +import 'package:simdart/src/internal/resource.dart'; +import 'package:simdart/src/resources.dart'; +import 'package:simdart/src/simdart.dart'; + +@internal +class ResourcesImpl implements Resources { + ResourcesImpl(SimDart sim) : _sim = sim; + + final SimDart _sim; + + @override + void limited({required String id, int capacity = 1}) { + SimDartHelper.addResource( + sim: _sim, + resourceId: id, + create: () => LimitedResource(id: id, capacity: capacity)); + } + + @override + bool isAvailable(String id) { + Resource? resource = SimDartHelper.getResource(sim: _sim, resourceId: id); + if (resource != null) { + return resource.isAvailable(); + } + return false; + } +} diff --git a/lib/src/internal/sim_configuration_interface.dart b/lib/src/internal/sim_configuration_interface.dart deleted file mode 100644 index dd5d311..0000000 --- a/lib/src/internal/sim_configuration_interface.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:simdart/src/start_time_handling.dart'; - -@internal -abstract interface class SimConfigurationInterface { - /// Specifies how the simulation handles start times in the past. - StartTimeHandling get startTimeHandling; - - /// Determines whether simulation tracks should be included in the simulation result. - /// - /// When set to `true`, the simulation will collect and return a list of [SimulationTrack] - /// objects as part of its result. If set to `false`, the tracks will not be collected, - /// and the list will be `null`. - /// - /// Default: `false` - bool get includeTracks; - - /// Determines how often `Future.delayed` is used instead of `Future.microtask` during events execution. - /// - /// - `0`: Always uses `microtask`. - /// - `1`: Alternates between `microtask` and `Future.delayed`. - /// - `N > 1`: Executes `N` events with `microtask` before using `Future.delayed`. - int get executionPriority; -} diff --git a/lib/src/internal/simdart_interface.dart b/lib/src/internal/simdart_interface.dart new file mode 100644 index 0000000..673d8ad --- /dev/null +++ b/lib/src/internal/simdart_interface.dart @@ -0,0 +1,59 @@ +import 'package:meta/meta.dart'; +import 'package:simdart/src/event.dart'; +import 'package:simdart/src/interval.dart'; +import 'package:simdart/src/sim_counter.dart'; +import 'package:simdart/src/sim_num.dart'; +import 'package:simdart/src/simdart.dart'; + +@internal +abstract interface class SimDartInterface { + /// Gets the current simulation time. + int get now; + + /// Schedules a new event to occur repeatedly based on the specified interval configuration. + /// + /// - [event]: The event to repeat. + /// - [start]: The time at which the first event should be executed. If null, the event will + /// occur at the [now] simulation time. + /// - [delay]: The delay before starting the repetition. + /// - [interval]: The interval between event executions. + /// - [stopCondition]: A function that determines whether to stop the repetition. + /// If provided, it will be called before each subsequent event execution. + /// If it returns `true`, the repetition stops. + /// The first event is always executed, regardless of the stop condition. + /// - [name] is an optional identifier for the event. + /// + /// Throws an [ArgumentError] if the provided interval configuration is invalid, such as + /// containing negative or inconsistent timing values. + void repeatProcess( + {required Event event, + int? start, + int? delay, + required Interval interval, + StopCondition? stopCondition, + String Function(int start)? name}); + + /// Schedules a new event to occur at a specific simulation time or after a delay. + /// + /// [event] is the function that represents the action to be executed when the event occurs. + /// [start] is the absolute time at which the event should occur. If null, the event will + /// occur at the [now] simulation time. + /// [delay] is the number of time units after the [now] when the event has been scheduled. + /// It cannot be provided if [start] is specified. + /// [name] is an optional identifier for the event. + /// + /// Throws an [ArgumentError] if both [start] and [delay] are provided or if [delay] is negative. + void process({required Event event, String? name, int? start, int? delay}); + + /// Creates a new [SimCounter] instance with the given name. + /// + /// - [name]: The name of the counter. This is used to identify the counter in logs or reports. + /// - Returns: A new instance of [SimCounter]. + SimCounter counter(String name); + + /// Creates a new [SimNum] instance with the given name. + /// + /// - [name]: The name of the numeric metric. This is used to identify the metric in logs or reports. + /// - Returns: A new instance of [SimNum]. + SimNum num(String name); +} diff --git a/lib/src/internal/time_action.dart b/lib/src/internal/time_action.dart index 9b81792..e8583a8 100644 --- a/lib/src/internal/time_action.dart +++ b/lib/src/internal/time_action.dart @@ -3,14 +3,15 @@ import 'package:meta/meta.dart'; /// Represents any action to be executed at a specific time in the temporal loop of the algorithm. @internal abstract class TimeAction { - TimeAction({required this.start}); + static int _globalOrder = 0; + + TimeAction({required this.start, int? order}) + : order = order ?? _globalOrder++; /// The scheduled start time. int start; - void execute(); + final int order; - int secondaryCompareTo(TimeAction action) { - return 0; - } + void execute(); } diff --git a/lib/src/resources.dart b/lib/src/resources.dart index 1e3df57..ad486e7 100644 --- a/lib/src/resources.dart +++ b/lib/src/resources.dart @@ -1,14 +1,4 @@ -import 'package:meta/meta.dart'; -import 'package:simdart/src/internal/resource.dart'; -import 'package:simdart/src/simdart.dart'; - -/// This class is responsible for creating the resources -/// available in the simulator. -class Resources { - Resources._(SimDart sim) : _sim = sim; - - final SimDart _sim; - +abstract class Resources { /// Creates a resource with limited capacity. /// /// This method adds a resource with the specified capacity. @@ -17,15 +7,11 @@ class Resources { /// /// - [id]: The unique identifier of the resource (required). /// - [capacity]: The maximum capacity of the resource. The default value is 1. - void limited({required String id, int capacity = 1}) { - SimDartHelper.addResource( - sim: _sim, - resourceId: id, - create: () => LimitedResource(id: id, capacity: capacity)); - } -} + void limited({required String id, int capacity = 1}); -@internal -class ResourcesHelper { - static Resources build(SimDart sim) => Resources._(sim); + /// Checks if a resource is available. + /// + /// - [id]: The id of the resource to check. + /// - Returns: `true` if the resource is available, `false` otherwise. + bool isAvailable(String id); } diff --git a/lib/src/resources_context.dart b/lib/src/resources_context.dart new file mode 100644 index 0000000..39de0b3 --- /dev/null +++ b/lib/src/resources_context.dart @@ -0,0 +1,22 @@ +import 'package:simdart/src/internal/resources_impl.dart'; + +abstract class ResourcesContext extends ResourcesImpl { + ResourcesContext(super.sim); + + /// Releases a previously acquired resource. + /// + /// - [id]: The id of the resource to release. + void release(String id); + + /// Tries to acquire a resource immediately. + /// + /// - [id]: The id of the resource to acquire. + /// - Returns: `true` if the resource was acquired, `false` otherwise. + bool tryAcquire(String id); + + /// Acquires a resource, waiting if necessary until it becomes available. + /// + /// - [id]: The id of the resource to acquire. + /// - Returns: A [Future] that completes when the resource is acquired. + Future acquire(String id); +} diff --git a/lib/src/sim_context.dart b/lib/src/sim_context.dart index 3401276..dbde1e9 100644 --- a/lib/src/sim_context.dart +++ b/lib/src/sim_context.dart @@ -1,15 +1,7 @@ -import 'dart:async'; -import 'dart:math'; +import 'package:simdart/src/internal/simdart_interface.dart'; +import 'package:simdart/src/resources_context.dart'; -import 'package:simdart/simdart.dart'; -import 'package:simdart/src/internal/event_scheduler_interface.dart'; -import 'package:simdart/src/internal/now_interface.dart'; - -/// Represents the context of the simulation. -/// -/// Encapsulates the information and state of the simulation. -abstract interface class SimContext - implements EventSchedulerInterface, NowInterface { +abstract interface class SimContext implements SimDartInterface { /// Pauses the execution of the event for the specified [delay] in simulation time. /// /// The event is re-added to the simulation's event queue and will resume after @@ -18,22 +10,7 @@ abstract interface class SimContext /// Throws an [ArgumentError] if the delay is negative. Future wait(int delay); - /// The instance of the random number generator used across the simulation. - /// It is initialized once and reused to improve performance, avoiding the need to - /// instantiate a new `Random` object for each event. - Random get random; - - /// Creates a new [SimCounter] instance with the given name. - /// - /// - [name]: The name of the counter. This is used to identify the counter in logs or reports. - /// - Returns: A new instance of [SimCounter]. - SimCounter counter(String name); - - /// Creates a new [SimNum] instance with the given name. - /// - /// - [name]: The name of the numeric metric. This is used to identify the metric in logs or reports. - /// - Returns: A new instance of [SimNum]. - SimNum num(String name); + String get eventName; - Resources get resources; + ResourcesContext get resources; } diff --git a/lib/src/simdart.dart b/lib/src/simdart.dart index 0ac2c5e..4748f1b 100644 --- a/lib/src/simdart.dart +++ b/lib/src/simdart.dart @@ -1,16 +1,14 @@ import 'dart:async'; -import 'dart:collection'; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:simdart/src/event.dart'; import 'package:simdart/src/internal/event_action.dart'; -import 'package:simdart/src/internal/event_scheduler_interface.dart'; -import 'package:simdart/src/internal/now_interface.dart'; import 'package:simdart/src/internal/repeat_event_action.dart'; import 'package:simdart/src/internal/resource.dart'; -import 'package:simdart/src/internal/sim_configuration_interface.dart'; +import 'package:simdart/src/internal/resources_impl.dart'; +import 'package:simdart/src/internal/simdart_interface.dart'; import 'package:simdart/src/internal/time_action.dart'; import 'package:simdart/src/interval.dart'; import 'package:simdart/src/resources.dart'; @@ -21,11 +19,7 @@ import 'package:simdart/src/simulation_track.dart'; import 'package:simdart/src/start_time_handling.dart'; /// Represents a discrete-event simulation engine. -class SimDart - implements - SimConfigurationInterface, - EventSchedulerInterface, - NowInterface { +class SimDart implements SimDartInterface { /// Creates a simulation instance. /// /// - [now]: The starting time of the simulation. Defaults to `0` if null. @@ -47,7 +41,6 @@ class SimDart SimDart( {this.startTimeHandling = StartTimeHandling.throwErrorIfPast, int now = 0, - this.secondarySortByName = false, this.includeTracks = false, this.executionPriority = 0, int? seed}) @@ -56,59 +49,56 @@ class SimDart bool _hasRun = false; - /// Determines whether events with the same start time are sorted by their event name. - /// - /// The primary sorting criterion is always the simulated start time (`start`). If - /// two events have the same start time, the order between them will be decided by - /// their event name when [secondarySortByName] is set to true. If false, the order - /// remains undefined for events with identical start times. - final bool secondarySortByName; - final Map _numProperties = {}; final Map _counterProperties = {}; /// Holds the resources in the simulator. final Map _resources = {}; - late final Resources resources = ResourcesHelper.build(this); + + late final Resources resources = ResourcesImpl(this); /// The instance of the random number generator used across the simulation. /// It is initialized once and reused to improve performance, avoiding the need to /// instantiate a new `Random` object for each event. late final Random random; - /// A queue that holds event actions that are waiting for a resource to become available. - /// - /// These events were initially denied the resource and are placed in this queue - /// to await the opportunity to be processed once the resource is released. - final Queue _waitingForResource = Queue(); - /// Queue that holds the [TimeAction] instances to be executed at their respective times. final PriorityQueue _actions = PriorityQueue( (a, b) { - final primaryComparison = a.start.compareTo(b.start); - if (primaryComparison != 0) { - return primaryComparison; + final int c = a.start.compareTo(b.start); + if (c != 0) { + return c; } - return a.secondaryCompareTo(b); + return a.order.compareTo(b.order); }, ); - @override + /// Specifies how the simulation handles start times in the past. final StartTimeHandling startTimeHandling; - @override + /// Determines how often `Future.delayed` is used instead of `Future.microtask` during events execution. + /// + /// - `0`: Always uses `microtask`. + /// - `1`: Alternates between `microtask` and `Future.delayed`. + /// - `N > 1`: Executes `N` events with `microtask` before using `Future.delayed`. final int executionPriority; int _executionCount = 0; - @override + /// Determines whether simulation tracks should be included in the simulation result. + /// + /// When set to `true`, the simulation will collect and return a list of [SimulationTrack] + /// objects as part of its result. If set to `false`, the tracks will not be collected, + /// and the list will be `null`. + /// + /// Default: `false` final bool includeTracks; int? _startTime; int _duration = 0; - bool _nextEventScheduled = false; + bool _nextActionScheduled = false; late int? _until; @@ -120,6 +110,8 @@ class SimDart Completer? _terminator; + bool _error = false; + /// Runs the simulation, processing events in chronological order. /// /// - [until]: The time at which execution should stop. Execution will include events @@ -147,61 +139,49 @@ class SimDart _startTime = null; _terminator = Completer(); - _scheduleNextEvent(); + _scheduleNextAction(); await _terminator?.future; _duration = _now - (_startTime ?? 0); _terminator = null; return _buildResult(); } + @override + SimCounter counter(String name) { + return _counterProperties.putIfAbsent(name, () => SimCounter(name: name)); + } + + @override + SimNum num(String name) { + return _numProperties.putIfAbsent(name, () => SimNum(name: name)); + } + @override void repeatProcess( {required Event event, int? start, int? delay, required Interval interval, - RejectedEventPolicy rejectedEventPolicy = - RejectedEventPolicy.keepRepeating, - String? resourceId, - String? name}) { - _process( - event: event, + StopCondition? stopCondition, + String Function(int start)? name}) { + start = _calculateEventStart(start: start, delay: delay); + _addAction(RepeatEventAction( + sim: this, start: start, - delay: delay, - name: name, - resourceId: resourceId, - onReject: null, + eventName: name, + event: event, interval: interval, - rejectedEventPolicy: rejectedEventPolicy); + stopCondition: stopCondition)); } @override - void process( - {required Event event, - String? resourceId, - String? name, - int? start, - int? delay}) { - _process( - event: event, - start: start, - delay: delay, - name: name, - resourceId: resourceId, - onReject: null, - interval: null, - rejectedEventPolicy: null); + void process({required Event event, String? name, int? start, int? delay}) { + start = _calculateEventStart(start: start, delay: delay); + _addAction( + EventAction(sim: this, start: start, eventName: name, event: event)); } - void _process( - {required Event event, - required int? start, - required int? delay, - required String? name, - required String? resourceId, - required Function? onReject, - required Interval? interval, - required RejectedEventPolicy? rejectedEventPolicy}) { + int _calculateEventStart({required int? start, required int? delay}) { if (start != null && delay != null) { throw ArgumentError( 'Both start and delay cannot be provided at the same time.'); @@ -227,25 +207,7 @@ class SimDart start ??= now; - if (interval != null && rejectedEventPolicy != null) { - _addAction(RepeatEventAction( - sim: this, - rejectedEventPolicy: rejectedEventPolicy, - start: start, - eventName: name, - event: event, - resourceId: resourceId, - interval: interval)); - } else { - _addAction(EventAction( - sim: this, - start: start, - eventName: name, - event: event, - resourceId: resourceId, - onReject: onReject, - secondarySortByName: secondarySortByName)); - } + return start; } SimResult _buildResult() { @@ -257,29 +219,47 @@ class SimDart counterProperties: _counterProperties); } - void _scheduleNextEvent() { - assert(!_nextEventScheduled, 'Multiple schedules for the next event.'); - _nextEventScheduled = true; - if (executionPriority == 0 || _executionCount < executionPriority) { - _executionCount++; - Future.microtask(_consumeFirstEvent); - } else { - _executionCount = 0; - Future.delayed(Duration.zero, _consumeFirstEvent); + void _scheduleNextAction() { + if (_error) { + return; + } + if (!_nextActionScheduled) { + _nextActionScheduled = true; + if (executionPriority == 0 || _executionCount < executionPriority) { + _executionCount++; + Future.microtask(_consumeNextAction); + } else { + _executionCount = 0; + Future.delayed(Duration.zero, _consumeNextAction); + } } } void _addAction(TimeAction action) { + if (_error) { + return; + } _actions.add(action); } - void _addTrack(SimulationTrack track) { + void _addTrack({required String eventName, required Status status}) { + Map resourceUsage = {}; + for (Resource resource in _resources.values) { + resourceUsage[resource.id] = resource.queue.length; + } _tracks ??= []; - _tracks!.add(track); + _tracks!.add(SimulationTrack( + status: status, + name: eventName, + time: now, + resourceUsage: resourceUsage)); } - Future _consumeFirstEvent() async { - _nextEventScheduled = false; + Future _consumeNextAction() async { + if (_error) { + return; + } + _nextActionScheduled = false; if (_actions.isEmpty) { _terminator?.complete(); return; @@ -302,62 +282,21 @@ class SimDart _startTime ??= now; action.execute(); - - _scheduleNextEvent(); } } -/// Defines the behavior of the interval after a newly created event has been rejected. -enum RejectedEventPolicy { - /// Continues the repetition of the event at the specified intervals, even after the event was rejected. - keepRepeating, - - /// Stops the repetition of the event entirely after it has been rejected. - stopRepeating -} +typedef StopCondition = bool Function(SimDart sim); /// A helper class to access private members of the [SimDart] class. /// /// This class is marked as internal and should only be used within the library. @internal class SimDartHelper { - static void process( - {required SimDart sim, - required Event event, - required int? start, - required int? delay, - required String? name, - required String? resourceId, - required Function? onReject, - required Interval? interval, - required RejectedEventPolicy? rejectedEventPolicy}) { - sim._process( - event: event, - start: start, - delay: delay, - name: name, - resourceId: resourceId, - onReject: onReject, - interval: interval, - rejectedEventPolicy: rejectedEventPolicy); - } - /// Adds an [TimeAction] to the loop. static void addAction({required SimDart sim, required TimeAction action}) { sim._addAction(action); } - static void restoreWaitingEventsForResource({required SimDart sim}) { - while (sim._waitingForResource.isNotEmpty) { - sim._addAction(sim._waitingForResource.removeFirst()); - } - } - - static void queueOnWaitingForResource( - {required SimDart sim, required EventAction action}) { - sim._waitingForResource.add(action); - } - static Resource? getResource( {required SimDart sim, required String? resourceId}) { return sim._resources[resourceId]; @@ -374,23 +313,17 @@ class SimDartHelper { {required SimDart sim, required String eventName, required Status status}) { - Map resourceUsage = {}; - for (Resource resource in sim._resources.values) { - resourceUsage[resource.id] = resource.queue.length; - } - sim._addTrack(SimulationTrack( - status: status, - name: eventName, - time: sim.now, - resourceUsage: resourceUsage)); + sim._addTrack(eventName: eventName, status: status); } - static SimCounter counter({required SimDart sim, required String name}) { - return sim._counterProperties - .putIfAbsent(name, () => SimCounter(name: name)); + static void scheduleNextAction({required SimDart sim}) { + sim._scheduleNextAction(); } - static SimNum num({required SimDart sim, required String name}) { - return sim._numProperties.putIfAbsent(name, () => SimNum(name: name)); + static void error({required SimDart sim, required String msg}) { + sim._error = true; + sim._actions.clear(); + sim._terminator?.completeError(StateError(msg)); + sim._terminator = null; } } diff --git a/lib/src/simulation_track.dart b/lib/src/simulation_track.dart index a2e3a10..dda751c 100644 --- a/lib/src/simulation_track.dart +++ b/lib/src/simulation_track.dart @@ -26,7 +26,7 @@ class SimulationTrack { /// Constructor for creating a [SimulationTrack] instance. /// - /// [status] is the event status (e.g., [Status.executed]). + /// [status] is the event status (e.g., [Status.called]). /// [name] is the name of the event being processed, can be null. /// [time] is the simulation time when the event occurred. SimulationTrack( @@ -47,17 +47,13 @@ class SimulationTrack { /// This enumeration is used to track and distinguish different event status /// during the lifecycle of the simulation. enum Status { - /// The event was executed for the first time. - executed, + /// The event was called for the first time. + called, /// The event was resumed after being paused. resumed, - /// The event was scheduled internally, typically by [EventScheduler]. - scheduled, - - /// The resource was rejected for the event. - rejected; + yielded; /// Returns the string representation of the status. @override diff --git a/test/process_test.dart b/test/process_test.dart index ef2854f..d487137 100644 --- a/test/process_test.dart +++ b/test/process_test.dart @@ -8,34 +8,34 @@ Future emptyEvent(SimContext context) async {} void main() { group('Process', () { test('start', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.process(event: emptyEvent, name: 'a'); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); - tt.test(["[0][a][executed]"]); + tt.test(["[0][a][called]"]); - sim = SimDart(includeTracks: true, secondarySortByName: true); + sim = SimDart(includeTracks: true); sim.process(event: emptyEvent, start: 10, name: 'a'); result = await sim.run(); tt = TrackTester(result); - tt.test(["[10][a][executed]"]); + tt.test(["[10][a][called]"]); }); test('delay 1', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.process(event: emptyEvent, delay: 0, name: 'a'); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); - tt.test(["[0][a][executed]"]); + tt.test(["[0][a][called]"]); }); test('delay 2', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.process(event: emptyEvent, delay: 10, name: 'a'); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); - tt.test(["[10][a][executed]"]); + tt.test(["[10][a][called]"]); }); test('delay 3', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.process( event: (context) async { context.process(event: emptyEvent, delay: 10, name: 'b'); @@ -44,10 +44,10 @@ void main() { name: 'a'); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); - tt.test(["[5][a][executed]", "[15][b][executed]"]); + tt.test(["[5][a][called]", "[15][b][called]"]); }); test('delay 4', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.process( event: (context) async { context.process(event: emptyEvent, delay: 10, name: 'b'); @@ -63,10 +63,10 @@ void main() { SimResult result = await sim.run(); TrackTester tt = TrackTester(result); tt.test([ - "[0][a][executed]", - "[2][c][executed]", - "[4][d][executed]", - "[10][b][executed]" + "[0][a][called]", + "[2][c][called]", + "[4][d][called]", + "[10][b][called]" ]); }); }); diff --git a/test/repeat_process_test.dart b/test/repeat_process_test.dart index 74d4c74..1b53ade 100644 --- a/test/repeat_process_test.dart +++ b/test/repeat_process_test.dart @@ -6,94 +6,129 @@ import 'track_tester.dart'; void main() { group('Repeat process', () { test('Simple', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.repeatProcess( - event: (context) {}, - name: 'A', + event: (context) async {}, + name: (start) => 'A$start', interval: Interval.fixed(fixedInterval: 1, untilTime: 2)); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); - tt.test(["[0][A][executed]", "[1][A][executed]", "[2][A][executed]"]); + tt.test(['[0][A0][called]', '[1][A1][called]', '[2][A2][called]']); }); test('Wait', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.repeatProcess( event: (context) async { await context.wait(1); }, - name: 'A', + name: (start) => 'A$start', interval: Interval.fixed(fixedInterval: 1, untilTime: 2)); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); tt.test([ - "[0][A][executed]", - "[1][A][resumed]", - "[1][A][executed]", - "[2][A][resumed]", - "[2][A][executed]", - "[3][A][resumed]" + '[0][A0][called]', + '[0][A0][yielded]', + '[1][A0][resumed]', + '[1][A1][called]', + '[1][A1][yielded]', + '[2][A1][resumed]', + '[2][A2][called]', + '[2][A2][yielded]', + '[3][A2][resumed]' ]); }); - test('Resource - keep', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + test('Resource - acquire and wait', () async { + SimDart sim = SimDart(includeTracks: true); + + sim.resources.limited(id: 'r'); + + sim.process( + event: (context) async { + await context.resources.acquire('r'); + await context.wait(10); + context.resources.release('r'); + }, + name: 'A'); + sim.process( + event: (context) async { + await context.resources.acquire('r'); + context.resources.release('r'); + }, + name: 'B'); + + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + '[0][A][called]', + '[0][A][yielded]', + '[0][B][called]', + '[0][B][yielded]', + '[10][A][resumed]', + '[10][B][resumed]' + ]); + }); + + test('Resource', () async { + SimDart sim = SimDart(includeTracks: true); sim.resources.limited(id: 'r'); sim.repeatProcess( event: (context) async { + await context.resources.acquire('r'); await context.wait(10); + context.resources.release('r'); }, - name: 'A', - resourceId: 'r', + name: (start) => 'A$start', interval: Interval.fixed(fixedInterval: 1, untilTime: 2)); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); tt.test([ - "[0][A][executed]", - "[1][A][rejected]", - "[2][A][rejected]", - "[10][A][resumed]", - "[10][A][executed]", - "[10][A][rejected]", - "[20][A][resumed]", - "[20][A][executed]", - "[30][A][resumed]" + '[0][A0][called]', + '[0][A0][yielded]', + '[1][A1][called]', + '[1][A1][yielded]', + '[2][A2][called]', + '[2][A2][yielded]', + '[10][A0][resumed]', + '[10][A1][resumed]', + '[10][A1][yielded]', + '[20][A1][resumed]', + '[20][A2][resumed]', + '[20][A2][yielded]', + '[30][A2][resumed]' ]); }); test('Resource - stop', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.resources.limited(id: 'r'); sim.repeatProcess( event: (context) async { + await context.resources.acquire('r'); await context.wait(2); + context.resources.release('r'); }, - name: 'A', - resourceId: 'r', - interval: Interval.fixed(fixedInterval: 1, untilTime: 50), - rejectedEventPolicy: RejectedEventPolicy.stopRepeating); + name: (start) => 'A$start', + stopCondition: (s) => !s.resources.isAvailable('r'), + interval: Interval.fixed(fixedInterval: 1, untilTime: 50)); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); - tt.test([ - "[0][A][executed]", - "[1][A][rejected]", - "[2][A][resumed]", - "[2][A][executed]", - "[4][A][resumed]" - ]); + tt.test(['[0][A0][called]', '[0][A0][yielded]', '[2][A0][resumed]']); }); }); } diff --git a/test/resource_test.dart b/test/resource_test.dart index 6c6be02..4d08724 100644 --- a/test/resource_test.dart +++ b/test/resource_test.dart @@ -6,103 +6,140 @@ import 'track_tester.dart'; void main() { group('Resource', () { test('Capacity 1', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.resources.limited(id: 'r'); fA(context) async { + await context.resources.acquire('r'); await context.wait(1); + context.resources.release('r'); } fB(context) async { + await context.resources.acquire('r'); await context.wait(1); + context.resources.release('r'); } fC(context) async { + await context.resources.acquire('r'); await context.wait(1); + context.resources.release('r'); } - sim.process(event: fA, name: 'A', resourceId: 'r'); - sim.process(event: fB, name: 'B', resourceId: 'r'); - sim.process(event: fC, name: 'C', resourceId: 'r'); + sim.process(event: fA, name: 'A'); + sim.process(event: fB, name: 'B'); + sim.process(event: fC, name: 'C'); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); tt.test([ - "[0][A][executed]", - "[0][B][rejected]", - "[0][C][rejected]", - "[1][A][resumed]", - "[1][B][executed]", - "[1][C][rejected]", - "[2][B][resumed]", - "[2][C][executed]", - "[3][C][resumed]" + '[0][A][called]', + '[0][A][yielded]', + '[0][B][called]', + '[0][B][yielded]', + '[0][C][called]', + '[0][C][yielded]', + '[1][A][resumed]', + '[1][B][resumed]', + '[1][B][yielded]', + '[2][B][resumed]', + '[2][C][resumed]', + '[2][C][yielded]', + '[3][C][resumed]' ]); }); test('Capacity 2', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.resources.limited(id: 'r', capacity: 2); - fA(context) async { - await context.wait(1); - } - - fB(context) async { + event(context) async { + await context.resources.acquire('r'); await context.wait(1); + context.resources.release('r'); } - fC(context) async { - await context.wait(1); - } - - sim.process(event: fA, name: 'A', resourceId: 'r'); - sim.process(event: fB, name: 'B', resourceId: 'r'); - sim.process(event: fC, name: 'C', resourceId: 'r'); + sim.process(event: event, name: 'A'); + sim.process(event: event, name: 'B'); + sim.process(event: event, name: 'C'); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); tt.test([ - "[0][A][executed]", - "[0][B][executed]", - "[0][C][rejected]", - "[1][A][resumed]", - "[1][C][executed]", - "[1][B][resumed]", - "[2][C][resumed]" + '[0][A][called]', + '[0][A][yielded]', + '[0][B][called]', + '[0][B][yielded]', + '[0][C][called]', + '[0][C][yielded]', + '[1][A][resumed]', + '[1][B][resumed]', + '[1][C][resumed]', + '[1][C][yielded]', + '[2][C][resumed]' ]); }); test('Avoid unnecessary re-executing', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + SimDart sim = SimDart(includeTracks: true); sim.resources.limited(id: 'r', capacity: 2); - eventA(SimContext context) async { + eventResource(SimContext context) async { + await context.resources.acquire('resource'); await context.wait(10); + context.resources.release('resource'); } - eventB(SimContext context) async {} + event(SimContext context) async {} sim.resources.limited(id: 'resource', capacity: 2); - sim.process(event: eventA, name: 'A1', resourceId: 'resource'); - sim.process(event: eventA, name: 'A2', start: 1, resourceId: 'resource'); - sim.process(event: eventA, name: 'A3', start: 2, resourceId: 'resource'); - sim.process(event: eventB, name: 'B', start: 3); + sim.process(event: eventResource, name: 'A'); + sim.process(event: eventResource, name: 'B', start: 1); + sim.process(event: eventResource, name: 'C', start: 2); + sim.process(event: event, name: 'D', start: 3); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); tt.test([ - "[0][A1][executed]", - "[1][A2][executed]", - "[2][A3][rejected]", - "[3][B][executed]", - "[10][A1][resumed]", - "[10][A3][executed]", - "[11][A2][resumed]", - "[20][A3][resumed]" + '[0][A][called]', + '[0][A][yielded]', + '[1][B][called]', + '[1][B][yielded]', + '[2][C][called]', + '[2][C][yielded]', + '[3][D][called]', + '[10][A][resumed]', + '[10][C][resumed]', + '[10][C][yielded]', + '[11][B][resumed]', + '[20][C][resumed]' ]); }); + test('without await', () async { + expect( + () async { + SimDart sim = SimDart(includeTracks: true); + sim.resources.limited(id: 'r', capacity: 1); + sim.process( + event: (context) async { + context.resources.acquire('r'); // acquired + context.resources.acquire('r'); // should await + context.resources.acquire('r'); // error + }, + name: 'a'); + sim.process(event: (context) async {}); + await sim.run(); + }, + throwsA( + predicate((e) => + e is StateError && + e.message.contains( + "This event should be waiting for the resource to be released. Did you forget to use 'await'?")), + ), + ); + }); }); } diff --git a/test/time_loop_test.dart b/test/time_loop_test.dart index eac4b47..dd0c20a 100644 --- a/test/time_loop_test.dart +++ b/test/time_loop_test.dart @@ -4,14 +4,20 @@ import 'package:simdart/src/simdart.dart'; import 'package:test/test.dart'; class TestAction extends TimeAction { - TestAction({required super.start, required this.names, required this.name}); + TestAction( + {required this.sim, + required super.start, + required this.names, + required this.name}); + final SimDart sim; final String name; final List names; @override void execute() { names.add(name); + SimDartHelper.scheduleNextAction(sim: sim); } } @@ -26,17 +32,23 @@ void main() { List names = []; SimDartHelper.addAction( - sim: sim, action: TestAction(start: 0, name: 'A', names: names)); + sim: sim, + action: TestAction(sim: sim, start: 0, name: 'A', names: names)); SimDartHelper.addAction( - sim: sim, action: TestAction(start: 1, name: 'B', names: names)); + sim: sim, + action: TestAction(sim: sim, start: 1, name: 'B', names: names)); SimDartHelper.addAction( - sim: sim, action: TestAction(start: 10, name: 'C', names: names)); + sim: sim, + action: TestAction(sim: sim, start: 10, name: 'C', names: names)); SimDartHelper.addAction( - sim: sim, action: TestAction(start: 5, name: 'D', names: names)); + sim: sim, + action: TestAction(sim: sim, start: 5, name: 'D', names: names)); SimDartHelper.addAction( - sim: sim, action: TestAction(start: 2, name: 'E', names: names)); + sim: sim, + action: TestAction(sim: sim, start: 2, name: 'E', names: names)); SimDartHelper.addAction( - sim: sim, action: TestAction(start: 9, name: 'F', names: names)); + sim: sim, + action: TestAction(sim: sim, start: 9, name: 'F', names: names)); await sim.run(); diff --git a/test/track_tester.dart b/test/track_tester.dart index 77e33b0..aa1abed 100644 --- a/test/track_tester.dart +++ b/test/track_tester.dart @@ -9,10 +9,12 @@ class TrackTester { int get length => result.tracks != null ? result.tracks!.length : 0; void test(List tracks) { - expect(tracks.length, result.tracks?.length); - for (int index = 0; index < tracks.length; index++) { - SimulationTrack? track = result.tracks?[index]; - expect(tracks[index], track?.toString()); + List list = []; + if (result.tracks != null) { + for (SimulationTrack track in result.tracks!) { + list.add(track.toString()); + } } + expect(list, tracks); } } diff --git a/test/wait_test.dart b/test/wait_test.dart index 9da8bb9..46bbf9f 100644 --- a/test/wait_test.dart +++ b/test/wait_test.dart @@ -7,47 +7,39 @@ Future emptyEvent(SimContext context) async {} void main() { group('Wait', () { - test('with await', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + test('now', () async { + late int now1, now2; + SimDart sim = SimDart(); sim.process( + start: 1, event: (context) async { + now1 = context.now; await context.wait(10); - }, - name: 'a'); - sim.process(event: emptyEvent, start: 5, name: 'b'); + now2 = context.now; + }); - SimResult result = await sim.run(); - - TrackTester tt = TrackTester(result); - tt.test(["[0][a][executed]", "[5][b][executed]", "[10][a][resumed]"]); + await sim.run(); + expect(now1, 1); + expect(now2, 11); }); - test('with await 2', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + test('simple', () async { + SimDart sim = SimDart(includeTracks: true); sim.process( event: (context) async { await context.wait(10); - sim.process(event: emptyEvent, delay: 1, name: 'c'); }, - start: 0, name: 'a'); - sim.process(event: emptyEvent, delay: 5, name: 'b'); SimResult result = await sim.run(); TrackTester tt = TrackTester(result); - tt.test([ - "[0][a][executed]", - "[5][b][executed]", - "[10][a][resumed]", - "[11][c][executed]" - ]); + tt.test(['[0][a][called]', '[0][a][yielded]', '[10][a][resumed]']); }); - - test('without await', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + test('with await', () async { + SimDart sim = SimDart(includeTracks: true); sim.process( event: (context) async { - context.wait(10); + await context.wait(10); }, name: 'a'); sim.process(event: emptyEvent, start: 5, name: 'b'); @@ -55,14 +47,18 @@ void main() { SimResult result = await sim.run(); TrackTester tt = TrackTester(result); - tt.test(["[0][a][executed]", "[5][b][executed]", "[10][a][resumed]"]); + tt.test([ + '[0][a][called]', + '[0][a][yielded]', + '[5][b][called]', + '[10][a][resumed]' + ]); }); - - test('without await 2', () async { - SimDart sim = SimDart(includeTracks: true, secondarySortByName: true); + test('with await 2', () async { + SimDart sim = SimDart(includeTracks: true); sim.process( event: (context) async { - context.wait(10); + await context.wait(10); sim.process(event: emptyEvent, delay: 1, name: 'c'); }, start: 0, @@ -73,11 +69,55 @@ void main() { TrackTester tt = TrackTester(result); tt.test([ - "[0][a][executed]", - "[1][c][executed]", - "[5][b][executed]", - "[10][a][resumed]" + '[0][a][called]', + '[0][a][yielded]', + '[5][b][called]', + '[10][a][resumed]', + '[11][c][called]' ]); }); + + test('wait without await', () async { + expect( + () async { + SimDart sim = SimDart(includeTracks: true); + sim.process( + event: (context) async { + context.wait(10); + }, + name: 'a'); + sim.process(event: emptyEvent, start: 5, name: 'b'); + + await sim.run(); + }, + throwsA( + predicate((e) => + e is StateError && + e.message.contains( + "Next event is being scheduled, but the current one is still paused waiting for continuation. Did you forget to use 'await'?")), + ), + ); + }); + + test('multiple wait without await', () async { + expect( + () async { + SimDart sim = SimDart(includeTracks: true); + sim.process( + event: (context) async { + context.wait(10); + context.wait(10); + }, + name: 'a'); + await sim.run(); + }, + throwsA( + predicate((e) => + e is StateError && + e.message.contains( + "The event is already waiting. Did you forget to use 'await'?")), + ), + ); + }); }); }