476 lines
21 KiB
ReStructuredText
476 lines
21 KiB
ReStructuredText
.. include:: replace.txt
|
||
.. highlight:: cpp
|
||
|
||
|
||
.. heading hierarchy:
|
||
------------- Chapter
|
||
************* Section (#.#)
|
||
============= Subsection (#.#.#)
|
||
############# Paragraph (no number)
|
||
|
||
Events and Simulator
|
||
--------------------
|
||
|
||
|ns3| is a discrete-event network simulator. Conceptually, the simulator
|
||
keeps track of a number of events that are scheduled to execute at a
|
||
specified simulation time. The job of the simulator is to execute the
|
||
events in sequential time order. Once the completion of an event occurs,
|
||
the simulator will move to the next event (or will exit if there are no
|
||
more events in the event queue). If, for example, an event scheduled
|
||
for simulation time "100 seconds" is executed, and the next event is not
|
||
scheduled until "200 seconds", the simulator will immediately jump from
|
||
100 seconds to 200 seconds (of simulation time) to execute the next event.
|
||
This is what is meant by "discrete-event" simulator.
|
||
|
||
To make this all happen, the simulator needs a few things:
|
||
|
||
1) a simulator object that can access an event queue where events are
|
||
stored and that can manage the execution of events
|
||
2) a scheduler responsible for inserting and removing events from the queue
|
||
3) a way to represent simulation time
|
||
4) the events themselves
|
||
|
||
This chapter of the manual describes these fundamental objects
|
||
(simulator, scheduler, time, event) and how they are used.
|
||
|
||
Event
|
||
*****
|
||
|
||
An event represents something that changes the simulation status, i.e.,
|
||
between two events the simulation status does not change, and the event
|
||
will likely change it (it could also not change anything).
|
||
|
||
Note that another way to understand an event is to consider it as a delayed
|
||
function call. With the due differences, a discrete event simulation is not
|
||
much different from a "normal" program where the functions are not called
|
||
immediately, but are marked with a "time", and the time is used to decide
|
||
the order of the functions execution.
|
||
|
||
The time, of course, is a simulated time, and is quite different from the
|
||
"real" time. Depending on the simulation complexity the simulated time
|
||
can advance faster or slower then the "real" time, but like a "real" time
|
||
can only go forward.
|
||
|
||
An example of an event is the reception of a packet, or the expiration
|
||
of a timer.
|
||
|
||
An event is represented by:
|
||
|
||
* The time at which the event will happen
|
||
* A pointer to the function that will "handle" the event,
|
||
* The parameters of the function that will handle the event (if any),
|
||
* Other internal structures.
|
||
|
||
An event is scheduled through a call to ``Simulator::Schedule``, and once
|
||
scheduled, it can be canceled or removed.
|
||
Removal implies removal from the scheduler data structure, while cancel
|
||
keeps them in the data structure but sets a boolean flag that suppresses
|
||
calling the bound event function at the scheduled time. When an event is
|
||
scheduled by the Simulator, an ``EventId`` is returned. The client may use
|
||
this event ID to later cancel or remove the event; see the example program
|
||
``src/core/examples/sample-simulator.{cc,py}`` for example usage.
|
||
Cancelling an event is typically less computationally expensive than
|
||
removing it, but cancelled events consumes more memory in the scheduler
|
||
data structure, which might impact its performances.
|
||
|
||
Events are stored by the simulator in a scheduler data
|
||
structure. Events are handled in increasing order of
|
||
simulator time, and in the case of two events with the same
|
||
scheduled time, the event with the lowest unique ID (a
|
||
monotonically increasing counter) will be handled first.
|
||
In other words tied events are handled in FIFO order.
|
||
|
||
Note that concurrent events (events that happen at the very same time)
|
||
are unlikely in a real system - not to say impossible. In |ns3|
|
||
concurrent events are common for a number of reasons, one of them
|
||
being the time representation. While developing a model this must
|
||
be carefully taken into account.
|
||
|
||
During the event execution, the simulation time will not advance, i.e., each
|
||
event is executed in zero time. This is a common assumption in
|
||
discrete event simulations, and holds when the computational complexity of
|
||
the operations executed in the event is negligible.
|
||
When this assumption does not hold, it is necessary to schedule a second event
|
||
to mimic the end of the computationally intensive task.
|
||
|
||
As an example, suppose to have a device that receives a packet and has to
|
||
perform a complex analysis on it (e.g., an image processing task). The
|
||
sequence of events will be:
|
||
|
||
* T(t) - Packet reception and processing, save the result somewhere, and
|
||
schedule an event in (t+d) marking the end of the data processing.
|
||
* T(t+d) - Retrieve the data, and do other stuff based them.
|
||
|
||
So, even if the data processing actually did return a result in the
|
||
execution of the first event, the data is considered valid only after
|
||
the second event.
|
||
|
||
The image below can be useful to clarify the idea.
|
||
|
||
.. image:: figures/time-consuming-event-handling.png
|
||
|
||
|
||
Simulator
|
||
*********
|
||
|
||
The Simulator class is the public entry point to access event scheduling
|
||
facilities. Once a couple of events have been scheduled to start the
|
||
simulation, the user can start to execute them by entering the simulator
|
||
main loop (call ``Simulator::Run``). Once the main loop starts running, it
|
||
will sequentially execute all scheduled events in order from oldest to
|
||
most recent until there are either no more events left in the event
|
||
queue or Simulator::Stop has been called.
|
||
|
||
To schedule events for execution by the simulator main loop, the
|
||
Simulator class provides the Simulator::Schedule* family of functions.
|
||
|
||
1) Handling event handlers with different signatures
|
||
|
||
These functions are declared and implemented as C++ templates to handle
|
||
automatically the wide variety of C++ event handler signatures used in
|
||
the wild. For example, to schedule an event to execute 10 seconds in the
|
||
future, and invoke a C++ method or function with specific arguments, you
|
||
might write this:
|
||
|
||
::
|
||
|
||
void handler(int arg0, int arg1)
|
||
{
|
||
std::cout << "handler called with argument arg0=" << arg0 << " and
|
||
arg1=" << arg1 << std::endl;
|
||
}
|
||
|
||
Simulator::Schedule(Seconds(10), &handler, 10, 5);
|
||
|
||
Which will output:
|
||
|
||
.. sourcecode:: text
|
||
|
||
handler called with argument arg0=10 and arg1=5
|
||
|
||
Of course, these C++ templates can also handle transparently member
|
||
methods on C++ objects:
|
||
|
||
*To be completed: member method example*
|
||
|
||
Notes:
|
||
|
||
* the |ns3| Schedule methods recognize automatically functions and
|
||
methods only if they take less than 5 arguments. If you need them to
|
||
support more arguments, please, file a bug report.
|
||
* Readers familiar with the term 'fully-bound functors' will recognize
|
||
the Simulator::Schedule methods as a way to automatically construct such
|
||
objects.
|
||
|
||
2) Common scheduling operations
|
||
|
||
The Simulator API was designed to make it really simple to schedule most
|
||
events. It provides three variants to do so (ordered from most commonly
|
||
used to least commonly used):
|
||
|
||
* Schedule methods which allow you to schedule an event in the future
|
||
by providing the delay between the current simulation time and the
|
||
expiration date of the target event.
|
||
* ScheduleNow methods which allow you to schedule an event for the
|
||
current simulation time: they will execute _after_ the current event is
|
||
finished executing but _before_ the simulation time is changed for the
|
||
next event.
|
||
* ScheduleDestroy methods which allow you to hook in the shutdown
|
||
process of the Simulator to cleanup simulation resources: every
|
||
'destroy' event is executed when the user calls the Simulator::Destroy
|
||
method.
|
||
|
||
3) Maintaining the simulation context
|
||
|
||
There are two basic ways to schedule events, with and without *context*.
|
||
What does this mean?
|
||
|
||
::
|
||
|
||
Simulator::Schedule(Time const &time, MEM mem_ptr, OBJ obj);
|
||
|
||
vs.
|
||
|
||
::
|
||
|
||
Simulator::ScheduleWithContext(uint32_t context, Time const &time, MEM mem_ptr, OBJ obj);
|
||
|
||
Readers who invest time and effort in developing or using a non-trivial
|
||
simulation model will know the value of the |ns3| logging framework to
|
||
debug simple and complex simulations alike. One of the important
|
||
features that is provided by this logging framework is the automatic
|
||
display of the network node id associated with the 'currently' running
|
||
event.
|
||
|
||
The node id of the currently executing network node is in fact tracked
|
||
by the Simulator class. It can be accessed with the
|
||
Simulator::GetContext method which returns the 'context' (a 32-bit
|
||
integer) associated and stored in the currently-executing event. In some
|
||
rare cases, when an event is not associated with a specific network
|
||
node, its 'context' is set to 0xffffffff.
|
||
|
||
To associate a context to each event, the Schedule, and ScheduleNow
|
||
methods automatically reuse the context of the currently-executing event
|
||
as the context of the event scheduled for execution later.
|
||
|
||
In some cases, most notably when simulating the transmission of a packet
|
||
from a node to another, this behavior is undesirable since the expected
|
||
context of the reception event is that of the receiving node, not the
|
||
sending node. To avoid this problem, the Simulator class provides a
|
||
specific schedule method: ScheduleWithContext which allows one to
|
||
provide explicitly the node id of the receiving node associated with
|
||
the receive event.
|
||
|
||
*XXX: code example*
|
||
|
||
In some very rare cases, developers might need to modify or understand
|
||
how the context (node id) of the first event is set to that of its
|
||
associated node. This is accomplished by the NodeList class: whenever a
|
||
new node is created, the NodeList class uses ScheduleWithContext to
|
||
schedule a 'initialize' event for this node. The 'initialize' event thus executes
|
||
with a context set to that of the node id and can use the normal variety
|
||
of Schedule methods. It invokes the Node::Initialize method which propagates
|
||
the 'initialize' event by calling the DoInitialize method for each object
|
||
associated with the node. The DoInitialize method overridden in some of these
|
||
objects (most notably in the Application base class) will schedule some
|
||
events (most notably Application::StartApplication) which will in turn
|
||
scheduling traffic generation events which will in turn schedule
|
||
network-level events.
|
||
|
||
Notes:
|
||
|
||
* Users need to be careful to propagate DoInitialize methods across objects
|
||
by calling Initialize explicitly on their member objects
|
||
* The context id associated with each ScheduleWithContext method has
|
||
other uses beyond logging: it is used by an experimental branch of |ns3|
|
||
to perform parallel simulation on multicore systems using
|
||
multithreading.
|
||
|
||
The Simulator::* functions do not know what the context is: they
|
||
merely make sure that whatever context you specify with
|
||
ScheduleWithContext is available when the corresponding event executes
|
||
with ::GetContext.
|
||
|
||
It is up to the models implemented on top of Simulator::* to interpret
|
||
the context value. In |ns3|, the network models interpret the context
|
||
as the node id of the node which generated an event. This is why it is
|
||
important to call ScheduleWithContext in ns3::Channel subclasses
|
||
because we are generating an event from node i to node j and we want
|
||
to make sure that the event which will run on node j has the right
|
||
context.
|
||
|
||
Available Simulator Engines
|
||
===========================
|
||
|
||
|ns3| supplies two different types of basic simulator engine to manage
|
||
event execution. These are derived from the abstract base class `SimulatorImpl`:
|
||
|
||
* `DefaultSimulatorImpl` This is a classic sequential discrete event
|
||
simulator engine which uses a single thread of execution. This engine
|
||
executes events as fast as possible.
|
||
* `DistributedSimulatorImpl` This is a classic YAWNS distributed ("parallel")
|
||
simulator engine. By labeling and instantiating your model components
|
||
appropriately this engine will execute the model in parallel across many
|
||
compute processes, yet in a time-synchronized way, as if the model had
|
||
executed sequentially. The two advantages are to execute models faster
|
||
and to execute models too large to fit in one compute node. This engine also
|
||
attempts to execute as fast as possible.
|
||
* `NullMessageSimulatorImpl` This implements a variant of the Chandy-
|
||
Misra-Bryant (CMB) null message algorithm for parallel simulation.
|
||
Like `DistributedSimulatorImpl` this requires appropriate labeling and
|
||
instantiation of model components. This engine attempts to execute
|
||
events as fast as possible.
|
||
|
||
You can choose which simulator engine to use by setting a global variable,
|
||
for example::
|
||
|
||
GlobalValue::Bind("SimulatorImplementationType",
|
||
StringValue("ns3::DistributedSimulatorImpl"));
|
||
|
||
or by using a command line argument
|
||
|
||
.. sourcecode:: console
|
||
|
||
$ ./ns3 run "... --SimulatorImplementationType=ns3::DistributedSimulatorImpl"
|
||
|
||
In addition to the basic simulator engines there is a general facility used
|
||
to build "adapters" which provide small behavior modifications to one of
|
||
the core `SimulatorImpl` engines. The adapter base class is
|
||
`SimulatorAdapter`, itself derived from `SimulatorImpl`. `SimulatorAdapter`
|
||
uses the `PIMPL (pointer to implementation) <https://en.cppreference.com/w/cpp/language/pimpl>`_
|
||
idiom to forward all calls to the configured base simulator engine.
|
||
This makes it easy to provide small customizations
|
||
just by overriding the specific Simulator calls needed, and allowing
|
||
`SimulatorAdapter` to handle the rest.
|
||
|
||
There are few places where adapters are used currently:
|
||
|
||
* `RealtimeSimulatorImpl` This adapter attempts to execute in real time
|
||
by pacing the wall clock evolution. This pacing is "best effort",
|
||
meaning actual event execution may not occur exactly in sync, but
|
||
close to it. This engine is normally only used with the
|
||
`DefaultSimulatorImpl`, but it can be used to keep a distributed
|
||
simulation synchronized with real time. See the :doc:`realtime` chapter.
|
||
* `VisualSimulatorImpl` This adapter starts a live visualization of the
|
||
running simulation, showing the network graph and each packet traversing
|
||
the links.
|
||
* `LocalTimeSimulatorImpl` This adapter enables attaching noisy local clocks
|
||
to `Nodes`, then scheduling events with respect to the local noisy clock,
|
||
instead of relative to the true simulator time.
|
||
|
||
In addition to the PIMPL idiom of `SimulatorAdapter` there is a special
|
||
per-event customization hook::
|
||
|
||
SimulatorImpl::PreEventHook( const EventId & id)
|
||
|
||
One can use this to perform any housekeeping actions before the next event
|
||
actually executes.
|
||
|
||
The distinction between a core engine and an adapter is the following: there
|
||
can only ever be one core engine running, while there can be several adapters
|
||
chained up each providing a variation on the base engine execution.
|
||
For example one can use noisy local clocks with the real time adapter.
|
||
|
||
A single adapter can be added on top of the `DefaultSimulatorImpl` by the same
|
||
two methods above: binding the `"SimulatorImplementationType"` global value or
|
||
using the command line argument. To chain multiple adapters a different
|
||
approach must be used; see the `SimulatorAdapter::AddAdapter()`
|
||
API documentation.
|
||
|
||
The simulator engine type can be set once, but must be set before the
|
||
first call to the `Simulator()` API. In practice, since some models have
|
||
to schedule their start up events when they are constructed, this means
|
||
generally you should set the engine type before instantiating any other
|
||
model components.
|
||
|
||
The engine type can be changed after `Simulator::Destroy()` but before
|
||
any additional calls to the Simulator API, for instance when executing
|
||
multiple runs in a single |ns3| invocation.
|
||
|
||
|
||
Time
|
||
****
|
||
|
||
|ns3| internally represents simulation times and durations as
|
||
64-bit signed integers (with the sign bit used for negative durations).
|
||
The time values are interpreted with respect to a "resolution" unit in the
|
||
customary SI units: fs, ps, ns, us, ms, s, min, h, d, y.
|
||
The unit defines the minimum Time value.
|
||
It can be changed once before any calls to `Simulator::Run()`.
|
||
It is not stored with the 64-bit time value itself.
|
||
|
||
Times can be constructed from all standard numeric types
|
||
(using the configured default unit)
|
||
or with explicit units (as in `Time MicroSeconds (uint64_t value)`).
|
||
Times can be compared, tested for sign or equality to zero, rounded to
|
||
a given unit, converted to standard numeric types in specific units.
|
||
All basic arithmetic operations are supported
|
||
(addition, subtraction, multiplication or division
|
||
by a scalar (numeric value)). Times can be written to/read from IO streams.
|
||
In the case of writing it is easy to choose the output unit, different
|
||
from the resolution unit.
|
||
|
||
Here are examples of common usage:
|
||
|
||
.. sourcecode:: cpp
|
||
|
||
Time t1 = MilliSeconds(1500); // 1.5 s = 1500 ms
|
||
Time t2 = MicroSeconds(500); // 500 microseconds
|
||
Time t3 = t1 + t2; // arithmetic
|
||
Time t4 = t3 * 2; // multiplication
|
||
|
||
if (t4 > Seconds(3))
|
||
{
|
||
std::cout << "t4 is greater than 3 seconds\n";
|
||
}
|
||
|
||
double ms = t4.GetMilliSeconds(); // convert to double in ms
|
||
|
||
std::cout << t4.As(Time::MS) << "\n"; // stream with specific unit
|
||
|
||
.. note::
|
||
|
||
When using floating-point values with constructors like ``Seconds(1.5)``,
|
||
users should be aware that values smaller than the current resolution
|
||
will be rounded. For example, if the resolution is set to nanoseconds,
|
||
values like ``Seconds(1e-10)`` will be rounded to zero.
|
||
Consider inspecting ``Time::SetResolution()`` and using methods like
|
||
``GetNanoSeconds()`` to understand how sub-resolution values behave in practice.
|
||
|
||
When calling ``Time::SetResolution()``, there is a trade-off between
|
||
the precision of time measurements and the maximum simulation time
|
||
span that can be represented. Finer resolutions (like femtoseconds)
|
||
allow for more precise timing but reduce the maximum representable
|
||
duration.
|
||
|
||
.. list-table::
|
||
:header-rows: 1
|
||
:widths: 25 25 50
|
||
|
||
* - Resolution Unit
|
||
- Smallest Step
|
||
- Approximate Max Time Span
|
||
* - Seconds
|
||
- 1 s
|
||
- ~2.9 × 10¹¹ years
|
||
* - Milliseconds
|
||
- 1 ms
|
||
- ~2.9 × 10⁸ years
|
||
* - Microseconds
|
||
- 1 µs
|
||
- ~2.9 × 10⁵ years
|
||
* - Nanoseconds
|
||
- 1 ns
|
||
- ~292 years
|
||
* - Picoseconds
|
||
- 1 ps
|
||
- ~107 days
|
||
* - Femtoseconds
|
||
- 1 fs
|
||
- ~2.6 hours
|
||
|
||
Scheduler
|
||
*********
|
||
|
||
The main job of the `Scheduler` classes is to maintain the priority queue of
|
||
future events. The scheduler can be set with a global variable,
|
||
similar to choosing the `SimulatorImpl`::
|
||
|
||
GlobalValue::Bind("SchedulerType",
|
||
StringValue("ns3::DistributedSimulatorImpl"));
|
||
|
||
The scheduler can be changed at any time via `Simulator::SetScheduler()`.
|
||
The default scheduler is `MapScheduler` which uses a `std::map<>` to
|
||
store events in time order.
|
||
|
||
Because event distributions vary by model there is no one
|
||
best strategy for the priority queue, so |ns3| has several options with
|
||
differing tradeoffs. The example `utils/bench-scheduler.c` can be used
|
||
to test the performance for a user-supplied event distribution.
|
||
For modest execution times (less than an hour, say) the choice of priority
|
||
queue is usually not significant; configuring the build type to optimized
|
||
is much more important in reducing execution times.
|
||
|
||
The available scheduler types, and a summary of their time and space
|
||
complexity on `Insert()` and `RemoveNext()`, are listed in the
|
||
following table. See the individual Scheduler API pages for details on the
|
||
complexity of the other API calls.
|
||
|
||
+------------------------+-------------------------------------+-------------+--------------+----------+--------------+
|
||
| Scheduler Type | Complexity |
|
||
+------------------------+-------------------------------------+-------------+--------------+----------+--------------+
|
||
| | | Time | Space |
|
||
| `SchedulerImpl` Type | Method +-------------+--------------+----------+--------------+
|
||
| | | Insert() | RemoveNext() | Overhead | Per Event |
|
||
+========================+=====================================+=============+==============+==========+==============+
|
||
| CalendarScheduler | `<std::list> []` | Constant | Constant | 24 bytes | 16 bytes |
|
||
+------------------------+-------------------------------------+-------------+--------------+----------+--------------+
|
||
| HeapScheduler | Heap on `std::vector` | Logarithmic | Logarithmic | 24 bytes | 0 |
|
||
+------------------------+-------------------------------------+-------------+--------------+----------+--------------+
|
||
| ListScheduler | `std::list` | Linear | Constant | 24 bytes | 16 bytes |
|
||
+------------------------+-------------------------------------+-------------+--------------+----------+--------------+
|
||
| MapScheduler | `st::map` | Logarithmic | Constant | 40 bytes | 32 bytes |
|
||
+------------------------+-------------------------------------+-------------+--------------+----------+--------------+
|
||
| PriorityQueueScheduler | `std::priority_queue<,std::vector>` | Logarithmic | Logarithms | 24 bytes | 0 |
|
||
+------------------------+-------------------------------------+-------------+--------------+----------+--------------+
|