diff --git a/CHANGELOG.md b/CHANGELOG.md index 41575e3..b5e56bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 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. +* Allow creating new resources during simulation. +* Methods for acquiring and releasing resources within events. + ## 0.1.0 * Initial release diff --git a/README.md b/README.md index f5d9e3a..c9cfebb 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) @@ -80,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'); @@ -88,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.sim.process(event: _c, delay: 1, name: 'C'); + 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 @@ -118,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 @@ -155,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 52169d6..194861d 100644 --- a/example/example.dart +++ b/example/example.dart @@ -1,14 +1,20 @@ import 'package:simdart/simdart.dart'; void main() async { - final SimDart sim = SimDart(onTrack: (track) => print(track)); + final SimDart sim = SimDart(includeTracks: true); - sim.process(event: _a, name: 'A'); + sim.process(event: _eventA, name: 'A'); - 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(EventContext context) async { +Future _eventA(SimContext context) async { await context.wait(2); - context.sim.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 beb761e..6a96cce 100644 --- a/lib/simdart.dart +++ b/lib/simdart.dart @@ -1,8 +1,13 @@ -export 'src/start_time_handling.dart'; -export 'src/simdart.dart' hide SimDartHelper; export 'src/event.dart'; -export 'src/simulation_track.dart'; export 'src/interval.dart'; export 'src/observable.dart'; -export 'src/resource_configurator.dart' hide ResourcesConfiguratorHelper; -export 'src/execution_priority.dart'; +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'; +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 fa3cb59..424d370 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,26 +1,6 @@ -import 'dart:async'; - -import 'package:simdart/src/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 -/// [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. -mixin EventContext { - /// The simulation instance managing this event. - SimDart get sim; - - /// 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); -} +/// A function that represents an event in the simulation. +typedef Event = Future Function(SimContext context); 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/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 a30f20c..0d18f2c 100644 --- a/lib/src/internal/event_action.dart +++ b/lib/src/internal/event_action.dart @@ -2,126 +2,196 @@ 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/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'; import 'package:simdart/src/simulation_track.dart'; @internal -class EventAction extends TimeAction with EventContext { +class EventAction extends TimeAction implements SimContext { EventAction( {required this.sim, required super.start, required String? eventName, - required this.event, - required this.resourceId, - required this.onTrack, - required this.onReject, - required this.secondarySortByName}) + required this.event}) : _eventName = eventName; /// The name of the event. 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; + @override + String get eventName => _eventName ?? hashCode.toString(); /// The event to be executed. final Event event; - final Function? onReject; - - @override final SimDart sim; - /// The resource id required by the event. - final String? resourceId; - - bool _resourceAcquired = false; + @override + late final ResourcesContext resources = ResourcesContextImpl(sim, this); - final bool secondarySortByName; + @override + int get now => sim.now; /// Internal handler for resuming a waiting event. - void Function()? _resume; + EventCompleter? _eventCompleter; @override - int secondaryCompareTo(TimeAction action) { - if (secondarySortByName && action is EventAction) { - return eventName.compareTo(action.eventName); + void execute() { + if (_eventCompleter != null) { + throw StateError('This event is yielding'); + } + + if (sim.includeTracks) { + SimDartHelper.addSimulationTrack( + sim: sim, eventName: eventName, status: Status.called); } - return 0; + + _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.scheduleNextAction(sim: sim); + }); } - bool get _canRun => resourceId == null || _resourceAcquired; + Future _runEvent() async { + await event(this); + } @override - void execute() { - final Function()? resume = _resume; - - if (resume != null) { - if (onTrack != null) { - onTrack!(SimDartHelper.buildSimulationTrack( - sim: sim, eventName: eventName, status: Status.resumed)); - } - // Resume the event if it is waiting, otherwise execute its action. - resume.call(); + Future wait(int delay) async { + if (_eventCompleter != null) { + SimDartHelper.error( + sim: sim, + msg: "The event is already waiting. Did you forget to use 'await'?"); return; } - 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.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; + } - if (onTrack != null) { - Status status = Status.executed; - if (!_canRun) { - status = Status.rejected; + 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); } - onTrack!(SimDartHelper.buildSimulationTrack( - sim: sim, eventName: eventName, status: status)); } + } - 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); + 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); } - }); - } else { - onReject?.call(); - SimDartHelper.queueOnWaitingForResource(sim: sim, action: this); + } } } @override - Future wait(int delay) async { - if (_resume != null) { - return; - } + void process({required Event event, String? name, int? start, int? delay}) { + sim.process(event: event, name: name, start: start, delay: delay); + } - start = sim.now + delay; - // Adds it back to the loop to be resumed in the future. - SimDartHelper.addAction(sim: sim, action: this); + @override + void repeatProcess( + {required Event event, + int? start, + int? delay, + required Interval interval, + StopCondition? stopCondition, + String Function(int start)? name}) { + sim.repeatProcess( + event: event, + start: start, + delay: delay, + interval: interval, + stopCondition: stopCondition, + name: name); + } - final Completer resume = Completer(); - _resume = () { - resume.complete(); - _resume = null; - }; - await resume.future; + @override + SimCounter counter(String name) { + return sim.counter(name); } - Future _runEvent() async { - return event(this); + @override + SimNum num(String name) { + return sim.num(name); + } +} + +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/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 b268525..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/event.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(EventContext 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(EventContext event); + bool acquire(EventAction event); - void release(EventContext 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(EventContext 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(EventContext 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/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/internal/time_loop.dart b/lib/src/internal/time_loop.dart deleted file mode 100644 index 6ed9578..0000000 --- a/lib/src/internal/time_loop.dart +++ /dev/null @@ -1,135 +0,0 @@ -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/time_action.dart'; -import 'package:simdart/src/internal/time_loop_mixin.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 { - TimeLoop( - {required int? now, - required this.beforeRun, - required this.executionPriority, - required this.startTimeHandling}) { - _now = now ?? 0; - _priorityScheduler = executionPriority == ExecutionPriority.high - ? _highPrioritySchedule - : _lowPrioritySchedule; - } - - @override - final StartTimeHandling startTimeHandling; - - @override - final ExecutionPriority executionPriority; - - final Function beforeRun; - - /// 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; - int? _startTime; - - @override - int? get duration => _duration; - int? _duration; - - late final Function _priorityScheduler; - - bool _nextEventScheduled = false; - - late int? _until; - - @override - int get now => _now; - late int _now; - - 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; - } - if (_actions.isEmpty) { - _duration = 0; - _startTime = 0; - return; - } - _duration = null; - _startTime = null; - - beforeRun(); - - _terminator = Completer(); - _scheduleNextEvent(); - await _terminator?.future; - _duration = _now - (startTime ?? 0); - _terminator = null; - } - - 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); - } - - void addAction(TimeAction action) { - _actions.add(action); - } - - 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/internal/time_loop_mixin.dart b/lib/src/internal/time_loop_mixin.dart deleted file mode 100644 index a2d1fc7..0000000 --- a/lib/src/internal/time_loop_mixin.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:simdart/src/execution_priority.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 { - /// 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; - - /// 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. - /// - /// 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/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..ad486e7 --- /dev/null +++ b/lib/src/resources.dart @@ -0,0 +1,17 @@ +abstract class Resources { + /// 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}); + + /// 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 new file mode 100644 index 0000000..dbde1e9 --- /dev/null +++ b/lib/src/sim_context.dart @@ -0,0 +1,16 @@ +import 'package:simdart/src/internal/simdart_interface.dart'; +import 'package:simdart/src/resources_context.dart'; + +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 + /// the specified delay has passed. + /// + /// Throws an [ArgumentError] if the delay is negative. + Future wait(int delay); + + String get eventName; + + ResourcesContext get resources; +} 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 new file mode 100644 index 0000000..7a5a547 --- /dev/null +++ b/lib/src/sim_result.dart @@ -0,0 +1,38 @@ +import 'dart:collection'; + +import 'package:simdart/src/sim_counter.dart'; +import 'package:simdart/src/sim_num.dart'; +import 'package:simdart/src/simulation_track.dart'; + +/// Represents the simulation result. +class SimResult { + SimResult( + {required this.duration, + required this.startTime, + required List? tracks, + required Map numProperties, + required Map counterProperties}) + : tracks = tracks != null ? UnmodifiableListView(tracks) : null, + numProperties = UnmodifiableMapView(numProperties), + counterProperties = UnmodifiableMapView(counterProperties); + + /// 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; + + /// 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; + + final Map numProperties; + final Map counterProperties; +} diff --git a/lib/src/simdart.dart b/lib/src/simdart.dart index 24cd4d4..4748f1b 100644 --- a/lib/src/simdart.dart +++ b/lib/src/simdart.dart @@ -1,23 +1,25 @@ 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/execution_priority.dart'; import 'package:simdart/src/internal/event_action.dart'; import 'package:simdart/src/internal/repeat_event_action.dart'; +import 'package:simdart/src/internal/resource.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/internal/time_loop.dart'; -import 'package:simdart/src/internal/time_loop_mixin.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/resources.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'; /// Represents a discrete-event simulation engine. -class SimDart with TimeLoopMixin { +class SimDart implements SimDartInterface { /// Creates a simulation instance. /// /// - [now]: The starting time of the simulation. Defaults to `0` if null. @@ -26,8 +28,7 @@ class SimDart with TimeLoopMixin { /// - [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 @@ -38,145 +39,149 @@ class SimDart with TimeLoopMixin { /// /// - [executionPriority]: Defines the priority of task execution in the simulation. SimDart( - {StartTimeHandling startTimeHandling = StartTimeHandling.throwErrorIfPast, - OnTrack? onTrack, - int? now, - this.secondarySortByName = false, - ExecutionPriority executionPriority = ExecutionPriority.high, + {this.startTimeHandling = StartTimeHandling.throwErrorIfPast, + int now = 0, + this.includeTracks = false, + this.executionPriority = 0, int? seed}) - : _onTrack = onTrack, - random = Random(seed) { - _loop = TimeLoop( - now: now, - beforeRun: _beforeRun, - executionPriority: executionPriority, - startTimeHandling: startTimeHandling); - } + : random = Random(seed), + _now = now; - late final TimeLoop _loop; + 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 = {}; - final Map _resources = {}; + /// Holds the resources in the simulator. + final Map _resources = {}; - /// 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(); + 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 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; + /// Queue that holds the [TimeAction] instances to be executed at their respective times. + final PriorityQueue _actions = PriorityQueue( + (a, b) { + final int c = a.start.compareTo(b.start); + if (c != 0) { + return c; + } + return a.order.compareTo(b.order); + }, + ); + + /// Specifies how the simulation handles start times in the past. + final StartTimeHandling startTimeHandling; - /// A queue that holds event actions that are waiting for a resource to become available. + /// Determines how often `Future.delayed` is used instead of `Future.microtask` during events execution. /// - /// 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(); - - void _beforeRun() { - for (ResourceConfiguration rc - in ResourcesConfiguratorHelper.iterable(configurator: resources)) { - if (rc is LimitedResourceConfiguration) { - _resources[rc.id] = LimitedResource(id: rc.id, capacity: rc.capacity); - } - } - } + /// - `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; + + /// 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 _nextActionScheduled = false; + + late int? _until; + + @override + int get now => _now; + late int _now; + + List? _tracks; + + 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 /// scheduled at this time (inclusive). If null, execution will continue indefinitely. - Future run({int? until}) async { - return _loop.run(until: until); + Future run({int? until}) async { + 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; + + _terminator = Completer(); + _scheduleNextAction(); + await _terminator?.future; + _duration = _now - (_startTime ?? 0); + _terminator = null; + return _buildResult(); } - /// 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 + 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)); } - /// 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}) { - _process( - event: event, - start: start, - delay: delay, - name: name, - resourceId: resourceId, - onReject: null, - interval: null, - rejectedEventPolicy: null); + @override + 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.'); @@ -202,96 +207,94 @@ class SimDart with TimeLoopMixin { start ??= now; - if (interval != null && rejectedEventPolicy != null) { - _loop.addAction(RepeatEventAction( - sim: this, - rejectedEventPolicy: rejectedEventPolicy, - start: start, - eventName: name, - event: event, - resourceId: resourceId, - interval: interval)); - } else { - _loop.addAction(EventAction( - sim: this, - onTrack: _onTrack, - start: start, - eventName: name, - event: event, - resourceId: resourceId, - onReject: onReject, - secondarySortByName: secondarySortByName)); - } + return start; } - @override - int? get duration => _loop.duration; + SimResult _buildResult() { + return SimResult( + startTime: _startTime ?? 0, + duration: _duration, + tracks: _tracks, + numProperties: _numProperties, + counterProperties: _counterProperties); + } - @override - ExecutionPriority get executionPriority => _loop.executionPriority; + 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); + } + } + } - @override - int get now => _loop.now; + void _addAction(TimeAction action) { + if (_error) { + return; + } + _actions.add(action); + } - @override - int? get startTime => _loop.startTime; + 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(SimulationTrack( + status: status, + name: eventName, + time: now, + resourceUsage: resourceUsage)); + } - @override - StartTimeHandling get startTimeHandling => _loop.startTimeHandling; -} + Future _consumeNextAction() async { + if (_error) { + return; + } + _nextActionScheduled = false; + if (_actions.isEmpty) { + _terminator?.complete(); + return; + } + + TimeAction action = _actions.removeFirst(); -/// A function signature for tracking the progress of a simulation. -typedef OnTrack = void Function(SimulationTrack track); + // 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; + } -/// 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, + _startTime ??= now; - /// Stops the repetition of the event entirely after it has been rejected. - stopRepeating + action.execute(); + } } +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._loop.addAction(action); - } - - static void restoreWaitingEventsForResource({required SimDart sim}) { - while (sim._waitingForResource.isNotEmpty) { - sim._loop.addAction(sim._waitingForResource.removeFirst()); - } - } - - static void queueOnWaitingForResource( - {required SimDart sim, required EventAction action}) { - sim._waitingForResource.add(action); + sim._addAction(action); } static Resource? getResource( @@ -299,18 +302,28 @@ class SimDartHelper { return sim._resources[resourceId]; } - static SimulationTrack buildSimulationTrack( + 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, required Status status}) { - Map resourceUsage = {}; - for (Resource resource in sim._resources.values) { - resourceUsage[resource.id] = resource.queue.length; - } - return SimulationTrack( - status: status, - name: eventName, - time: sim.now, - resourceUsage: resourceUsage); + sim._addTrack(eventName: eventName, status: status); + } + + static void scheduleNextAction({required SimDart sim}) { + sim._scheduleNextAction(); + } + + 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 e375ac9..d487137 100644 --- a/test/process_test.dart +++ b/test/process_test.dart @@ -1,70 +1,73 @@ import 'package:simdart/simdart.dart'; import 'package:test/test.dart'; -import 'test_helper.dart'; +import 'track_tester.dart'; + +Future emptyEvent(SimContext 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); + sim.process(event: emptyEvent, name: 'a'); + SimResult result = await sim.run(); + TrackTester tt = TrackTester(result); + tt.test(["[0][a][called]"]); - 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); + sim.process(event: emptyEvent, start: 10, name: 'a'); + result = await sim.run(); + tt = TrackTester(result); + tt.test(["[10][a][called]"]); }); - 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); + sim.process(event: emptyEvent, delay: 0, name: 'a'); + SimResult result = await sim.run(); + TrackTester tt = TrackTester(result); + tt.test(["[0][a][called]"]); + }); + test('delay 2', () async { + 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][called]"]); + }); + test('delay 3', () async { + SimDart sim = SimDart(includeTracks: true); + sim.process( event: (context) async { - context.sim - .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][called]", "[15][b][called]"]); + }); + test('delay 4', () async { + SimDart sim = SimDart(includeTracks: true); + sim.process( event: (context) async { - context.sim - .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.sim - .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][called]", + "[2][c][called]", + "[4][d][called]", + "[10][b][called]" + ]); }); }); } 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 e2bb108..1b53ade 100644 --- a/test/repeat_process_test.dart +++ b/test/repeat_process_test.dart @@ -1,92 +1,134 @@ import 'package:simdart/simdart.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); - helper.sim.repeatProcess( - event: (context) {}, - name: 'A', + sim.repeatProcess( + event: (context) async {}, + name: (start) => 'A$start', 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][A0][called]', '[1][A1][called]', '[2][A2][called]']); }); test('Wait', () async { - TestHelper helper = TestHelper(); + SimDart sim = SimDart(includeTracks: true); - helper.sim.repeatProcess( + sim.repeatProcess( event: (context) async { await context.wait(1); }, - name: 'A', + name: (start) => 'A$start', 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][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 { - TestHelper helper = TestHelper(); + test('Resource - acquire and wait', () async { + SimDart sim = SimDart(includeTracks: true); - helper.sim.resources.limited(id: 'r'); + sim.resources.limited(id: 'r'); - helper.sim.repeatProcess( + 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: 'A', - resourceId: '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: (start) => 'A$start', 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][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 { - TestHelper helper = TestHelper(); + SimDart sim = SimDart(includeTracks: true); - helper.sim.resources.limited(id: 'r'); + sim.resources.limited(id: 'r'); - helper.sim.repeatProcess( + 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); - - 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); + 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][A0][called]', '[0][A0][yielded]', '[2][A0][resumed]']); }); }); } diff --git a/test/resource_test.dart b/test/resource_test.dart index 31c961b..4d08724 100644 --- a/test/resource_test.dart +++ b/test/resource_test.dart @@ -1,101 +1,145 @@ import 'package:simdart/simdart.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); + 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'); } - 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'); + 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][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 { - TestHelper helper = TestHelper(); - helper.sim.resources.limited(id: 'r', capacity: 2); + 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); - } - - 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: 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][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 { - TestHelper helper = TestHelper(); - helper.sim.resources.limited(id: 'r', capacity: 2); + SimDart sim = SimDart(includeTracks: true); + sim.resources.limited(id: 'r', capacity: 2); - eventA(EventContext context) async { + eventResource(SimContext context) async { + await context.resources.acquire('resource'); await context.wait(10); + context.resources.release('resource'); } - eventB(EventContext context) async {} - - SimDart sim = helper.sim; + 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); - - 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); + 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][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/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..dd0c20a 100644 --- a/test/time_loop_test.dart +++ b/test/time_loop_test.dart @@ -1,39 +1,56 @@ 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 { - 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); } } void main() { group('TimeLoop', () { test('Loop', () async { - TimeLoop loop = TimeLoop( - now: null, - executionPriority: ExecutionPriority.high, - beforeRun: () {}, + SimDart sim = SimDart( + includeTracks: true, + 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(sim: sim, start: 0, name: 'A', names: names)); + SimDartHelper.addAction( + sim: sim, + action: TestAction(sim: sim, start: 1, name: 'B', names: names)); + SimDartHelper.addAction( + sim: sim, + action: TestAction(sim: sim, start: 10, name: 'C', names: names)); + SimDartHelper.addAction( + sim: sim, + action: TestAction(sim: sim, start: 5, name: 'D', names: names)); + SimDartHelper.addAction( + sim: sim, + action: TestAction(sim: sim, start: 2, name: 'E', names: names)); + SimDartHelper.addAction( + sim: sim, + action: TestAction(sim: sim, 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 new file mode 100644 index 0000000..aa1abed --- /dev/null +++ b/test/track_tester.dart @@ -0,0 +1,20 @@ +import 'package:simdart/simdart.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) { + 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 04fed2b..46bbf9f 100644 --- a/test/wait_test.dart +++ b/test/wait_test.dart @@ -1,72 +1,123 @@ import 'package:simdart/simdart.dart'; import 'package:test/test.dart'; -import 'test_helper.dart'; +import 'track_tester.dart'; + +Future emptyEvent(SimContext context) async {} void main() { group('Wait', () { - test('with await', () async { - TestHelper helper = TestHelper(); - helper.sim.process( + 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'); - 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); + now2 = context.now; + }); - helper = TestHelper(); - helper.sim.process( + await sim.run(); + expect(now1, 1); + expect(now2, 11); + }); + test('simple', () async { + SimDart sim = SimDart(includeTracks: true); + sim.process( event: (context) async { await context.wait(10); - helper.sim - .process(event: TestHelper.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); - }); - test('without await', () async { - TestHelper helper = TestHelper(); - helper.sim.process( + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test(['[0][a][called]', '[0][a][yielded]', '[10][a][resumed]']); + }); + test('with await', () async { + SimDart sim = SimDart(includeTracks: true); + sim.process( event: (context) async { - context.wait(10); + 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'); - helper = TestHelper(); - helper.sim.process( + SimResult result = await sim.run(); + + TrackTester tt = TrackTester(result); + tt.test([ + '[0][a][called]', + '[0][a][yielded]', + '[5][b][called]', + '[10][a][resumed]' + ]); + }); + test('with await 2', () async { + SimDart sim = SimDart(includeTracks: true); + sim.process( event: (context) async { - context.wait(10); - helper.sim - .process(event: TestHelper.emptyEvent, delay: 1, name: 'c'); + await context.wait(10); + 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][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'?")), + ), + ); }); }); }