Ns 3 Manual
Ns 3 Manual
Release ns-3-dev
ns-3 project
CONTENTS
1 2
Organization Random Variables 2.1 Quick Overview . . . . . . . . . . . 2.2 Background . . . . . . . . . . . . . . 2.3 Seeding and independent replications 2.4 Class RandomVariable . . . . . . . . 2.5 Base class public API . . . . . . . . 2.6 Types of RandomVariables . . . . . . 2.7 Semantics of RandomVariable objects 2.8 Using other PRNG . . . . . . . . . . 2.9 More advanced usage . . . . . . . . 2.10 Publishing your results . . . . . . . . 2.11 Summary . . . . . . . . . . . . . . .
3 5 5 5 6 7 7 7 8 8 9 9 9 11 11 12 15 15 15 16 18 19 21 21 21 23 28 29 31 35 37 39 39
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
Callbacks 3.1 Callbacks Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Callbacks Background . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Object model 4.1 Object-oriented behavior . . . . . . 4.2 Object base classes . . . . . . . . . 4.3 Memory management and class Ptr 4.4 Object factories . . . . . . . . . . . 4.5 Downcasting . . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
Attributes 5.1 Object Overview . . . . . . . . . . . . . . . 5.2 Smart pointers . . . . . . . . . . . . . . . . 5.3 Attribute Overview . . . . . . . . . . . . . . 5.4 Extending attributes . . . . . . . . . . . . . 5.5 Adding new class type to the attribute system 5.6 CongStore . . . . . . . . . . . . . . . . . . Object names Logging
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
6 7 8
Overview . . . . . . . . . . . . Using the Tracing API . . . . . Using Trace Helpers . . . . . . Tracing implementation details
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
40 43 43 54 55 55 55 56 57 59 59 59 60 62 63 67 67 67 68 68 70 71 71 71 73 75 75 75 78 88
10 Helpers 11 Making Plots using the Gnuplot Class 11.1 Creating Plots Using the Gnuplot Class . . . . . . 11.2 An Example Program that Uses the Gnuplot Class 11.3 An Example 2-Dimensional Plot . . . . . . . . . . 11.4 An Example 2-Dimensional Plot with Error Bars . 11.5 An Example 3-Dimensional Plot . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
12 Using Python to Run ns-3 12.1 Introduction . . . . . . . . . . . . . . . . . . . . . . 12.2 An Example Python Script that Runs ns-3 . . . . . . . 12.3 Running Python Scripts . . . . . . . . . . . . . . . . 12.4 Caveats . . . . . . . . . . . . . . . . . . . . . . . . . 12.5 Working with Python Bindings . . . . . . . . . . . . 12.6 Instructions for Handling New Files or Changed APIs 12.7 Monolithic Python Bindings . . . . . . . . . . . . . . 12.8 Modular Python Bindings . . . . . . . . . . . . . . . 12.9 More Information for Developers . . . . . . . . . . . 13 Tests 13.1 13.2 13.3 13.4
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
14 Support 14.1 Creating a new ns-3 model . . . . . . . . . . 14.2 Adding a New Module to ns-3 . . . . . . . . 14.3 Enabling Subsets of ns-3 Modules . . . . . . 14.4 Enabling/disabling ns-3 Tests and Examples 14.5 Troubleshooting . . . . . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
ii
This is the ns-3 Manual. Primary documentation for the ns-3 project is available in ve forms: ns-3 Doxygen: Documentation of the public APIs of the simulator Tutorial, Manual (this document), and Model Library for the latest release and development tree ns-3 wiki This document is written in reStructuredText for Sphinx and is maintained in the doc/manual directory of ns-3s source code.
CONTENTS
CONTENTS
CHAPTER
ONE
ORGANIZATION
This chapter describes the overall ns-3 software organization and the corresponding organization of this manual. ns-3 is a discrete-event network simulator in which the simulation core and models are implemented in C++. ns-3 is built as a library which may be statically or dynamically linked to a C++ main program that denes the simulation topology and starts the simulator. ns-3 also exports nearly all of its API to Python, allowing Python programs to import an ns3 module in much the same way as the ns-3 library is linked by executables in C++.
Figure 1.1: Software organization of ns-3 The source code for ns-3 is mostly organized in the src directory and can be described by the diagram in Software organization of ns-3. We will work our way from the bottom up; in general, modules only have dependencies on modules beneath them in the gure. We rst describe the core of the simulator; those components that are common across all protocol, hardware, and environmental models. The simulation core is implemented in src/core. Packets are fundamental objects in a network simulator and are implemented in src/network. These two simulation modules by themselves are intended to comprise a generic simulation core that can be used by different kinds of networks, not just Internet-based networks. The above modules of ns-3 are independent of specic network and device models, which are covered in subsequent parts of this manual. In addition to the above ns-3 core, we introduce, also in the initial portion of the manual, two other modules that supplement the core C++-based API. ns-3 programs may access all of the API directly or may make use of a so-called helper API that provides convenient wrappers or encapsulation of low-level API calls. The fact that ns-3 programs can be written to two APIs (or a combination thereof) is a fundamental aspect of the simulator. We also describe how Python is supported in ns-3 before moving onto specic models of relevance to network simulation.
The remainder of the manual is focused on documenting the models and supporting capabilities. The next part focuses on two fundamental objects in ns-3: the Node and NetDevice. Two special NetDevice types are designed to support network emulation use cases, and emulation is described next. The following chapter is devoted to Internetrelated models, including the sockets API used by Internet applications. The next chapter covers applications, and the following chapter describes additional support for simulation, such as animators and statistics. The project maintains a separate manual devoted to testing and validation of ns-3 code (see the ns-3 Testing and Validation manual).
Chapter 1. Organization
CHAPTER
TWO
RANDOM VARIABLES
ns-3 contains a built-in pseudo-random number generator (PRNG). It is important for serious users of the simulator to understand the functionality, conguration, and usage of this PRNG, and to decide whether it is sufcient for his or her research use.
2.2 Background
Simulations use a lot of random numbers; one study found that most network simulations spend as much as 50% of the CPU generating random numbers. Simulation users need to be concerned with the quality of the (pseudo) random numbers and the independence between different streams of random numbers. Users need to be concerned with a few issues, such as: the seeding of the random number generator and whether a simulation outcome is deterministic or not, how to acquire different streams of random numbers that are independent from one another, and how long it takes for streams to cycle
We will introduce a few terms here: a RNG provides a long sequence of (pseudo) random numbers. The length of this sequence is called the cycle length or period, after which the RNG will repeat itself. This sequence can be partitioned into disjoint streams. A stream of a RNG is a contiguous subset or block of the RNG sequence. For instance, if the RNG period is of length N, and two streams are provided from this RNG, then the rst stream might use the rst N/2 values and the second stream might produce the second N/2 values. An important property here is that the two streams are uncorrelated. Likewise, each stream can be partitioned disjointedly to a number of uncorrelated substreams. The underlying RNG hopefully produces a pseudo-random sequence of numbers with a very long cycle length, and partitions this into streams and substreams in an efcient manner. ns-3 uses the same underlying random number generator as does ns-2: the MRG32k3a generator from Pierre LEcuyer. A detailed description can be found in http://www.iro.umontreal.ca/~lecuyer/myftp/papers/streams00.pdf. The MRG32k3a generator provides 1.8x1019 independent streams of random numbers, each of which consists of 2.3x1015 substreams. Each substream has a period (i.e., the number of random numbers before overlap) of 7.6x1022 . The period of the entire generator is 3.1x1057 . Class ns3::RandomVariable is the public interface to this underlying random number generator. When users create new RandomVariables (such as ns3::UniformVariable, ns3::ExponentialVariable, etc.), they create an object that uses one of the distinct, independent streams of the random number generator. Therefore, each object of type ns3::RandomVariable has, conceptually, its own virtual RNG. Furthermore, each ns3::RandomVariable can be congured to use one of the set of substreams drawn from the main stream. An alternate implementation would be to allow each RandomVariable to have its own (differently seeded) RNG. However, we cannot guarantee as strongly that the different sequences would be uncorrelated in such a case; hence, we prefer to use a single RNG and streams and substreams from it.
Which is better, setting a new seed or advancing the substream state? There is no guarantee that the streams produced by two random seeds will not overlap. The only way to guarantee that two streams do not overlap is to use the substream capability provided by the RNG implementation. Therefore, use the substream capability to produce multiple independent runs of the same simulation. In other words, the more statistically rigorous way to congure multiple independent replications is to use a xed seed and to advance the run number. This implementation allows for a maximum of 2.3x1015 independent replications using the substreams. For ease of use, it is not necessary to control the seed and run number from within the program; the user can set the NS_GLOBAL_VALUE environment variable as follows:
Another way to control this is by passing a command-line argument; since this is an ns-3 GlobalValue instance, it is equivalently done such as follows:
./waf --command-template="%s --RngRun=3" --run program-name
The above command-line variants make it easy to run lots of different runs from a shell script by just passing a different RngRun index.
We have already described the seeding conguration above. Different RandomVariable subclasses may have additional API.
class SequentialVariable class ExponentialVariable class ParetoVariable class WeibullVariable class NormalVariable class EmpiricalVariable class IntEmpiricalVariable class DeterministicVariable class LogNormalVariable class TriangularVariable class GammaVariable class ErlangVariable class ZipfVariable
Here, the ns-3 user can change the default random variable for this delay model (which is a UniformVariable ranging from 0 to 1) through the attribute system.
2.11 Summary
Lets review what things you should do when creating a simulation. Decide whether you are running with a xed seed or random seed; a xed seed is the default, Decide how you are going to manage independent replications, if applicable, Convince yourself that you are not drawing more random values than the cycle length, if you are running a very long simulation, and When you publish, follow the guidelines above about documenting your use of the random number generator.
10
CHAPTER
THREE
CALLBACKS
Some new users to ns-3 are unfamiliar with an extensively used programming idiom used throughout the code: the ns-3 callback. This chapter provides some motivation on the callback, guidance on how to use it, and details on its implementation.
This certainly works, but it has the drawback that it introduces a dependency on A and B to know about the other at compile time (this makes it harder to have independent compilation units in the simulator) and is not generalized; if in a later usage scenario, B needs to talk to a completely different C object, the source code for B needs to be changed to add a c_instance and so forth. It is easy to see that this is a brute force mechanism of communication that can lead to programming cruft in the models.
11
This is not to say that objects should not know about one another if there is a hard dependency between them, but that often the model can be made more exible if its interactions are less constrained at compile time. This is not an abstract problem for network simulation research, but rather it has been a source of problems in previous simulators, when researchers want to extend or modify the system to do different things (as they are apt to do in research). Consider, for example, a user who wants to add an IPsec security protocol sublayer between TCP and IP:
-----------| TCP | -----------| ----------| IP | --------------------| TCP | ----------| ----------| IPsec | ----------| ----------| IP | -----------
becomes ->
If the simulator has made assumptions, and hard coded into the code, that IP always talks to a transport protocol above, the user may be forced to hack the system to get the desired interconnections. This is clearly not an optimal way to design a generic simulator.
What you get from this is a variable named simply pfi that is initialized to the value 0. If you want to initialize this pointer to something meaningful, you have to have a function with a matching signature. In this case:
int MyFunction (int arg) {}
If you have this target, you can initialize the variable to point to your function like:
pfi = MyFunction;
You can then call MyFunction indirectly using the more suggestive form of the call:
int result = (*pfi) (1234);
This is suggestive since it looks like you are dereferencing the function pointer just like you would dereference any pointer. Typically, however, people take advantage of the fact that the compiler knows what is going on and will just use a shorter form:
12
Chapter 3. Callbacks
Notice that the function pointer obeys value semantics, so you can pass it around like any other value. Typically, when you use an asynchronous interface you will pass some entity like this to a function which will perform an action and call back to let you know it completed. It calls back by following the indirection and executing the provided function. In C++ you have the added complexity of objects. The analogy with the PFI above means you have a pointer to a member function returning an int (PMI) instead of the pointer to function returning an int (PFI). The declaration of the variable providing the indirection looks only slightly different:
int (MyClass::*pmi) (int arg) = 0;
This declares a variable named pmi just as the previous example declared a variable named pfi. Since the will be to call a method of an instance of a particular class, one must declare that method in a class:
class MyClass { public: int MyMethod (int arg); };
Given this class declaration, one would then initialize that variable like this:
pmi = &MyClass::MyMethod;
This assigns the address of the code implementing the method to the variable, completing the indirection. In order to call a method, the code needs a this pointer. This, in turn, means there must be an object of MyClass to refer to. A simplistic example of this is just calling a method indirectly (think virtual function):
int (MyClass::*pmi) (int arg) = 0; pmi = &MyClass::MyMethod; MyClass myClass; (myClass.*pmi) (1234); // Declare a PMI // Point at the implementation code // Need an instance of the class // Call the method with an object ptr
Just like in the C example, you can use this in an asynchronous call to another module which will call back using a method and an object pointer. The straightforward extension one might consider is to pass a pointer to the object and the PMI variable. The module would just do:
(*objectPtr.*pmi) (1234);
to execute the callback on the desired object. One might ask at this time, whats the point? The called module will have to understand the concrete type of the calling object in order to properly make the callback. Why not just accept this, pass the correctly typed object pointer and do object->Method(1234) in the code instead of the callback? This is precisely the problem described above. What is needed is a way to decouple the calling function from the called class completely. This requirement led to the development of the Functor. A functor is the outgrowth of something invented in the 1960s called a closure. It is basically just a packaged-up function call, possibly with some state. A functor has two parts, a specic part and a generic part, related through inheritance. The calling code (the code that executes the callback) will execute a generic overloaded operator () of a generic functor to cause the callback to be called. The called code (the code that wants to be called back) will have to provide a specialized implementation of the operator () that performs the class-specic work that caused the close-coupling problem above. With the specic functor and its overloaded operator () created, the called code then gives the specialized code to the module that will execute the callback (the calling code).
13
The calling code will take a generic functor as a parameter, so an implicit cast is done in the function call to convert the specic functor to a generic functor. This means that the calling module just needs to understand the generic functor type. It is decoupled from the calling code completely. The information one needs to make a specic functor is the object pointer and the pointer-to-method address. The essence of what needs to happen is that the system declares a generic part of the functor:
template <typename T> class Functor { public: virtual int operator() (T arg) = 0; };
The caller denes a specic part of the functor that really is just there to implement the specic operator() method:
template <typename T, typename ARG> class SpecificFunctor : public Functor<ARG> { public: SpecificFunctor(T* p, int (T::*_pmi)(ARG arg)) { m_p = p; m_pmi = _pmi; } virtual int operator() (ARG arg) { (*m_p.*m_pmi)(arg); } private: int (T::*m_pmi)(ARG arg); T* m_p; };
14
Chapter 3. Callbacks
CHAPTER
FOUR
OBJECT MODEL
ns-3 is fundamentally a C++ object system. Objects can be declared and instantiated as usual, per C++ rules. ns-3 also adds some features to traditional C++ objects, as described below, to provide greater functionality and features. This manual chapter is intended to introduce the reader to the ns-3 object model. This section describes the C++ class design for ns-3 objects. In brief, several design patterns in use include classic object-oriented design (polymorphic interfaces and implementations), separation of interface and implementation, the non-virtual public interface design pattern, an object aggregation facility, and reference counting for memory management. Those familiar with component models such as COM or Bonobo will recognize elements of the design in the ns-3 object aggregation model, although the ns-3 design is not strictly in accordance with either.
15
It is not required that ns-3 objects inherit from these class, but those that do get special properties. Classes deriving from class Object get the following properties. the ns-3 type and attribute system (see Attributes) an object aggregation system a smart-pointer reference counting system (class Ptr) Classes that derive from class ObjectBase get the rst two properties above, but do not get smart pointers. Classes that derive from class SimpleRefCount: get only the smart-pointer reference counting system. In practice, class Object is the variant of the three above that the ns-3 developer will most commonly encounter.
16
Please do not create such objects using operator new; create them using CreateObject() instead. For objects deriving from class SimpleRefCount, or other objects that support usage of the smart pointer class, a templated helper function is available and recommended to be used::
Ptr<B> b = Create<B> ();
This is simply a wrapper around operator new that correctly handles the reference counting system. In summary, use Create<B> if B is not an object but just uses reference counting (e.g. Packet), and use CreateObject<B> if B derives from ns3::Object.
4.3.3 Aggregation
The ns-3 object aggregation system is motivated in strong part by a recognition that a common use case for ns-2 has been the use of inheritance and polymorphism to extend protocol models. For instance, specialized versions of TCP such as RenoTcpAgent derive from (and override functions from) class TcpAgent. However, two problems that have arisen in the ns-2 model are downcasts and weak base class. Downcasting refers to the procedure of using a base class pointer to an object and querying it at run time to nd out type information, used to explicitly cast the pointer to a subclass pointer so that the subclass API can be used. Weak base class refers to the problems that arise when a class cannot be effectively reused (derived from) because it lacks necessary functionality, leading the developer to have to modify the base class and causing proliferation of base class API calls, some of which may not be semantically correct for all subclasses. ns-3 is using a version of the query interface design pattern to avoid these problems. This design is based on elements of the Component Object Model and GNOME Bonobo although full binary-level compatibility of replaceable components is not supported and we have tried to simplify the syntax and impact on model developers.
17
Note that the Ipv4 protocols are created using CreateObject(). Then, they are aggregated to the node. In this manner, the Node base class does not need to be edited to allow users with a base class Node pointer to access the Ipv4 interface; users may ask the node for a pointer to its Ipv4 interface at runtime. How the user asks the node is described in the next subsection. Note that it is a programming error to aggregate more than one object of the same type to an ns3::Object. So, for instance, aggregation is not an option for storing all of the active sockets of a node.
If the node in fact does not have an Ipv4 object aggregated to it, then the method will return null. Therefore, it is good practice to check the return value from such a function call. If successful, the user can now use the Ptr to the Ipv4 object that was previously aggregated to the node. Another example of how one might use aggregation is to add optional models to objects. For instance, an existing Node object may have an Energy Model object aggregated to it at run time (without modifying and recompiling the node class). An existing model (such as a wireless net device) can then later GetObject for the energy model and act appropriately if the interface has been either built in to the underlying Node object or aggregated to it at run time. However, other nodes need not know anything about energy models. We hope that this mode of programming will require much less need for developers to modify the base classes.
The rst method allows one to use the ns-3 TypeId system to specify the type of objects created. The second allows one to set attributes on the objects to be created, and the third allows one to create the objects themselves. For example:
ObjectFactory factory; // Make this factory create objects of type FriisPropagationLossModel factory.SetTypeId ("ns3::FriisPropagationLossModel") // Make this factory object change a default value of an attribute, for // subsequently created objects factory.Set ("SystemLoss", DoubleValue (2.0)); // Create one such object Ptr<Object> object = factory.Create (); factory.Set ("SystemLoss", DoubleValue (3.0)); // Create another object with a different SystemLoss Ptr<Object> object = factory.Create ();
18
4.5 Downcasting
A question that has arisen several times is, If I have a base class pointer (Ptr) to an object and I want the derived class pointer, should I downcast (via C++ dynamic cast) to get the derived pointer, or should I use the object aggregation system to GetObject<> () to nd a Ptr to the interface to the subclass API? The answer to this is that in many situations, both techniques will work. ns-3 provides a templated function for making the syntax of Object dynamic casting much more user friendly::
template <typename T1, typename T2> Ptr<T1> DynamicCast (Ptr<T2> const&p) { return Ptr<T1> (dynamic_cast<T1 *> (PeekPointer (p))); }
DynamicCast works when the programmer has a base type pointer and is testing against a subclass pointer. GetObject works when looking for different objects aggregated, but also works with subclasses, in the same way as DynamicCast. If unsure, the programmer should use GetObject, as it works in all cases. If the programmer knows the class hierarchy of the object under consideration, it is more direct to just use DynamicCast.
4.5. Downcasting
19
20
CHAPTER
FIVE
ATTRIBUTES
In ns-3 simulations, there are two main aspects to conguration: the simulation topology and how objects are connected the values used by the models instantiated in the topology This chapter focuses on the second item above: how the many values in use in ns-3 are organized, documented, and modiable by ns-3 users. The ns-3 attribute system is also the underpinning of how traces and statistics are gathered in the simulator. Before delving into details of the attribute value system, it will help to review some basic properties of class ns3::Object.
21
5.2.1 CreateObject
As we discussed above in Memory management and class Ptr, at the lowest-level API, objects of type ns3::Object are not instantiated using operator new as usual but instead by a templated function called CreateObject(). A typical way to create such an object is as follows::
Ptr<WifiNetDevice> nd = CreateObject<WifiNetDevice> ();
Objects that derive from ns3::Object must be allocated on the heap using CreateObject(). Those deriving from ns3::ObjectBase, such as ns-3 helper functions and packet headers and trailers, can be allocated on the stack. In some scripts, you may not see a lot of CreateObject() calls in the code; this is because there are some helper objects in effect that are doing the CreateObject()s for you.
5.2.2 TypeId
ns-3 classes that derive from class ns3::Object can include a metadata class called TypeId that records metainformation about the class, for use in the object aggregation and component manager systems: a unique string identifying the class the base class of the subclass, within the metadata system the set of accessible constructors in the subclass
22
Chapter 5. Attributes
MakeObjectVectorChecker<NetDevice> ()) .AddAttribute ("ApplicationList", "The list of applications associated to this Node.", ObjectVectorValue (), MakeObjectVectorAccessor (&Node::m_applications), MakeObjectVectorChecker<Application> ()) .AddAttribute ("Id", "The id (unique integer) of this Node.", TypeId::ATTR_GET, // allow only getting it. UintegerValue (0), MakeUintegerAccessor (&Node::m_id), MakeUintegerChecker<uint32_t> ()) ; return tid; }
Consider the TypeId of an ns-3 Object class as an extended form of run time type information (RTTI). The C++ language includes a simple kind of RTTI in order to support dynamic_cast and typeid operators. The .SetParent<Object> () call in the declaration above is used in conjunction with our object aggregation mechanisms to allow safe up- and down-casting in inheritance trees during GetObject. The .AddConstructor<Node> () call is used in conjunction with our abstract object factory mechanisms to allow us to construct C++ objects without forcing a user to know the concrete class of the object she is building. The three calls to .AddAttribute associate a given string with a strongly typed value in the class. Notice that you must provide a help string which may be displayed, for example, via command line processors. Each Attribute is associated with mechanisms for accessing the underlying member variable in the object (for example, MakeUintegerAccessor tells the generic Attribute code how to get to the node ID above). There are also Checker methods which are used to validate values. When users want to create Nodes, they will usually call some form of CreateObject,:
Ptr<Node> n = CreateObject<Node> ();
or more abstractly, using an object factory, you can create a Node object without even knowing the concrete C++ type:
ObjectFactory factory; const std::string typeId = "ns3::Node; factory.SetTypeId (typeId); Ptr<Object> node = factory.Create <Object> ();
Both of these methods result in fully initialized attributes being available in the resulting Object instances. We next discuss how attributes (values associated with member variables or functions of the class) are plumbed into the above TypeId.
23
Similarly, users may want ne-grained access to internal variables in the simulation, or may want to broadly change the initial value used for a particular parameter in all subsequently created objects. Finally, users may wish to know what variables are settable and retrievable in a simulation conguration. This is not just for direct simulation interaction on the command line; consider also a (future) graphical user interface that would like to be able to provide a feature whereby a user might right-click on an node on the canvas and see a hierarchical, organized list of parameters that are settable on the node and its constituent member objects, and help text and default values for each parameter.
Lets consider things that a user may want to do with the value of m_maxPackets: Set a default value for the system, such that whenever a new DropTailQueue is created, this member is initialized to that default. Set or get the value on an already instantiated queue. The above things typically require providing Set() and Get() functions, and some type of global default value. In the ns-3 attribute system, these value denitions and accessor functions are moved into the TypeId class; e.g.::
NS_OBJECT_ENSURE_REGISTERED (DropTailQueue); TypeId DropTailQueue::GetTypeId (void) { static TypeId tid = TypeId ("ns3::DropTailQueue") .SetParent<Queue> () .AddConstructor<DropTailQueue> () .AddAttribute ("MaxPackets", "The maximum number of packets accepted by this DropTailQueue.", UintegerValue (100), MakeUintegerAccessor (&DropTailQueue::m_maxPackets), MakeUintegerChecker<uint32_t> ()) ; return tid; }
The AddAttribute() method is performing a number of things with this value: Binding the variable m_maxPackets to a string MaxPackets Providing a default value (100 packets) Providing some help text dening the value
24
Chapter 5. Attributes
Providing a checker (not used in this example) that can be used to set bounds on the allowable range of values The key point is that now the value of this variable and its default value are accessible in the attribute namespace, which is based on strings such as MaxPackets and TypeId strings. In the next section, we will provide an example script that shows how users may manipulate these values. Note that initialization of the attribute relies on the macro NS_OBJECT_ENSURE_REGISTERED (DropTailQueue) being called; if you leave this out of your new class implementation, your attributes will not be initialized correctly. While we have described how to create attributes, we still havent described how to access and manage these values. For instance, there is no globals.h header le where these are stored; attributes are stored with their classes. Questions that naturally arise are how do users easily learn about all of the attributes of their models, and how does a user access these attributes, or document their values as part of the record of their simulation?
The main thing to notice in the above are the two calls to Config::SetDefault. This is how we set the default value for all subsequently instantiated DropTailQueues. We illustrate that two types of Value classes, a StringValue and a UintegerValue class, can be used to assign the value to the attribute named by ns3::DropTailQueue::MaxPackets. Now, we will create a few objects using the low-level API; here, our newly created queues will not have a m_maxPackets initialized to 100 packets but to 80 packets, because of what we did above with default values.:
Ptr<Node> n0 = CreateObject<Node> (); Ptr<PointToPointNetDevice> net0 = CreateObject<PointToPointNetDevice> (); n0->AddDevice (net0); Ptr<Queue> q = CreateObject<DropTailQueue> (); net0->AddQueue(q);
25
At this point, we have created a single node (Node 0) and a single PointToPointNetDevice (NetDevice 0) and added a DropTailQueue to it. Now, we can manipulate the MaxPackets value of the already instantiated DropTailQueue. Here are various ways to do that.
Using the GetObject function, we can perform a safe downcast to a DropTailQueue, where MaxPackets is a member:
Ptr<DropTailQueue> dtq = txQueue->GetObject <DropTailQueue> (); NS_ASSERT (dtq != 0);
Next, we can get the value of an attribute on this queue. We have introduced wrapper Value classes for the underlying data types, similar to Java wrappers around these types, since the attribute system stores values and not disparate types. Here, the attribute value is assigned to a UintegerValue, and the Get() method on this value produces the (unwrapped) uint32_t.:
UintegerValue limit; dtq->GetAttribute ("MaxPackets", limit); NS_LOG_INFO ("1. dtq limit: " << limit.Get () << " packets");
Note that the above downcast is not really needed; we could have done the same using the Ptr<Queue> even though the attribute is a member of the subclass:
txQueue->GetAttribute ("MaxPackets", limit); NS_LOG_INFO ("2. txQueue limit: " << limit.Get () << " packets");
26
Chapter 5. Attributes
We could have also used wildcards to set this value for all nodes and all net devices (which in this simple example has the same effect as the previous Set()):
Config::Set ("/NodeList/*/DeviceList/*/TxQueue/MaxPackets", UintegerValue (15)); txQueue->GetAttribute ("MaxPackets", limit); NS_LOG_INFO ("5. txQueue limit changed through wildcarded namespace: " << limit.Get () << " packets");
27
The system provides some macros that help users declare and dene new AttributeValue subclasses for new types that they want to introduce into the attribute system: ATTRIBUTE_HELPER_HEADER ATTRIBUTE_HELPER_CPP Initialization order Attributes in the system must not depend on the state of any other Attribute in this system. This is because an ordering of Attribute initialization is not specied, nor enforced, by the system. A specic example of this can be seen in automated conguration programs such as ns3::ConfigStore. Although a given model may arrange it so that Attributes are initialized in a particular order, another automatic congurator may decide independently to change Attributes in, for example, alphabetic order. Because of this non-specic ordering, no Attribute in the system may have any dependence on any other Attribute. As a corollary, Attribute setters must never fail due to the state of another Attribute. No Attribute setter may change (set) any other Attribute value as a result of changing its value. This is a very strong restriction and there are cases where Attributes must set consistently to allow correct operation. To this end we do allow for consistency checking when the attribute is used (cf. NS_ASSERT_MSG or NS_ABORT_MSG). In general, the attribute code to assign values to the underlying class member variables is executed after an object is constructed. But what if you need the values assigned before the constructor body executes, because you need them in the logic of the constructor? There is a way to do this, used for example in the class ns3::ConfigStore: call ObjectBase::ConstructSelf () as follows::
ConfigStore::ConfigStore () { ObjectBase::ConstructSelf (AttributeList ()); // continue on with constructor. }
Suppose that someone working with TCP wanted to get or set the value of that variable using the metadata system. If it were not already provided by ns-3, the user could declare the following addition in the runtime metadata system (to the TypeId declaration for TcpSocket)::
.AddAttribute ("Congestion window", "Tcp congestion window (bytes)", UintegerValue (1), MakeUintegerAccessor (&TcpSocket::m_cWnd), MakeUintegerChecker<uint16_t> ())
28
Chapter 5. Attributes
Now, the user with a pointer to the TcpSocket can perform operations such as setting and getting the value, without having to add these functions explicitly. Furthermore, access controls can be applied, such as allowing the parameter to be read and not written, or bounds checking on the permissible values can be applied.
The declaration for this in the class declaration is one-line public member method::
public: static TypeId GetTypeId (void);
Typical mistakes here involve: Not calling the SetParent method or calling it with the wrong type Not calling the AddConstructor method of calling it with the wrong type Introducing a typographical error in the name of the TypeId in its constructor Not using the fully-qualied c++ typename of the enclosing c++ class as the name of the TypeId None of these mistakes can be detected by the ns-3 codebase so, users are advised to check carefully multiple times that they got these right.
29
5.5.1 Header le
/** * \brief a 2d rectangle */ class Rectangle { ... double double double double }; xMin; xMax; yMin; yMax;
One macro call and two operators, must be added below the class declaration in order to turn a Rectangle into a value usable by the Attribute system::
std::ostream &operator << (std::ostream &os, const Rectangle &rectangle); std::istream &operator >> (std::istream &is, Rectangle &rectangle); ATTRIBUTE_HELPER_HEADER (Rectangle);
5.5.2 Implementation le
In the class denition (.cc le), the code looks like this::
ATTRIBUTE_HELPER_CPP (Rectangle); std::ostream & operator << (std::ostream &os, const Rectangle &rectangle) { os << rectangle.xMin << "|" << rectangle.xMax << "|" << rectangle.yMin << "|" << rectangle.yMax; return os; } std::istream & operator >> (std::istream &is, Rectangle &rectangle) { char c1, c2, c3; is >> rectangle.xMin >> c1 >> rectangle.xMax >> c2 >> rectangle.yMin >> c3 >> rectangle.yMax; if (c1 != | || c2 != | || c3 != |) { is.setstate (std::ios_base::failbit); } return is; }
These stream operators simply convert from a string representation of the Rectangle (xMin|xMax|yMin|yMax) to the underlying Rectangle, and the modeler must specify these operators and the string syntactical representation of an instance of the new class.
30
Chapter 5. Attributes
5.6 CongStore
The CongStore is a specialized database for attribute values and default values. Although it is a separately maintained module in src/config-store/ directory, we document it here because of its sole dependency on ns-3 core module and attributes. Values for ns-3 attributes can be stored in an ASCII or XML text le and loaded into a future simulation. This feature is known as the ns-3 CongStore. We can explore this system by using an example from src/config-store/examples/config-store-save.cc. First, all users must include the following statement::
#include "ns3/config-store-module.h"
Next, this program adds a sample object A to show how the system is extended::
class A : public Object { public: static TypeId GetTypeId (void) { static TypeId tid = TypeId ("ns3::A") .SetParent<Object> () .AddAttribute ("TestInt16", "help text", IntegerValue (-2), MakeIntegerAccessor (&A::m_int16), MakeIntegerChecker<int16_t> ()) ; return tid; } int16_t m_int16; }; NS_OBJECT_ENSURE_REGISTERED (A);
Next, we use the Cong subsystem to override the defaults in a couple of ways::
Config::SetDefault ("ns3::A::TestInt16", IntegerValue (-5));
Ptr<A> a_obj = CreateObject<A> (); NS_ABORT_MSG_UNLESS (a_obj->m_int16 == -5, "Cannot set As integer attribute via Config::SetDefault") Ptr<A> a2_obj = CreateObject<A> (); a2_obj->SetAttribute ("TestInt16", IntegerValue (-3)); IntegerValue iv; a2_obj->GetAttribute ("TestInt16", iv); NS_ABORT_MSG_UNLESS (iv.Get () == -3, "Cannot set As integer attribute via SetAttribute");
The next statement is necessary to make sure that (one of) the objects created is rooted in the conguration namespace as an object instance. This normally happens when you aggregate objects to ns3::Node or ns3::Channel but here, since we are working at the core level, we need to create a new root namespace object::
Config::RegisterRootNamespaceObject (a2_obj);
Next, we want to output the conguration store. The examples show how to do it in two formats, XML and raw text. In practice, one should perform this step just before calling Simulator::Run (); it will allow the conguration to be saved just before running the simulation. There are three attributes that govern the behavior of the CongStore: Mode, Filename, and FileFormat. The Mode (default None) congures whether ns-3 should load conguration from a previously saved le (specify Mode=Load) or save it to a le (specify Mode=Save). The Filename (default ) is where the CongStore 5.6. CongStore 31
should store its output data. The FileFormat (default RawText) governs whether the CongStore format is Xml or RawText format. The example shows::
Config::SetDefault ("ns3::ConfigStore::Filename", StringValue ("output-attributes.xml")); Config::SetDefault ("ns3::ConfigStore::FileFormat", StringValue ("Xml")); Config::SetDefault ("ns3::ConfigStore::Mode", StringValue ("Save")); ConfigStore outputConfig; outputConfig.ConfigureDefaults (); outputConfig.ConfigureAttributes (); // Output config store to txt format Config::SetDefault ("ns3::ConfigStore::Filename", StringValue ("output-attributes.txt")); Config::SetDefault ("ns3::ConfigStore::FileFormat", StringValue ("RawText")); Config::SetDefault ("ns3::ConfigStore::Mode", StringValue ("Save")); ConfigStore outputConfig2; outputConfig2.ConfigureDefaults (); outputConfig2.ConfigureAttributes (); Simulator::Run (); Simulator::Destroy ();
In the above, all of the default values for attributes for the core module are shown. Then, all the values for the ns-3 global values are recorded. Finally, the value of the instance of A that was rooted in the conguration namespace is shown. In a real ns-3 program, many more models, attributes, and defaults would be shown. An XML version also exists in output-attributes.xml::
<?xml version="1.0" encoding="UTF-8"?> <ns3> <default name="ns3::RealtimeSimulatorImpl::SynchronizationMode" value="BestEffort"/> <default name="ns3::RealtimeSimulatorImpl::HardLimit" value="+100000000.0ns"/>
32
Chapter 5. Attributes
<default name="ns3::PcapFileWrapper::CaptureSize" value="65535"/> <default name="ns3::PacketSocket::RcvBufSize" value="131072"/> <default name="ns3::ErrorModel::IsEnabled" value="true"/> <default name="ns3::RateErrorModel::ErrorUnit" value="EU_BYTE"/> <default name="ns3::RateErrorModel::ErrorRate" value="0"/> <default name="ns3::RateErrorModel::RanVar" value="Uniform:0:1"/> <default name="ns3::DropTailQueue::Mode" value="Packets"/> <default name="ns3::DropTailQueue::MaxPackets" value="100"/> <default name="ns3::DropTailQueue::MaxBytes" value="6553500"/> <default name="ns3::Application::StartTime" value="+0.0ns"/> <default name="ns3::Application::StopTime" value="+0.0ns"/> <default name="ns3::ConfigStore::Mode" value="Save"/> <default name="ns3::ConfigStore::Filename" value="output-attributes.xml"/> <default name="ns3::ConfigStore::FileFormat" value="Xml"/> <default name="ns3::A::TestInt16" value="-5"/> <global name="RngSeed" value="1"/> <global name="RngRun" value="1"/> <global name="SimulatorImplementationType" value="ns3::DefaultSimulatorImpl"/> <global name="SchedulerType" value="ns3::MapScheduler"/> <global name="ChecksumEnabled" value="false"/> <value path="/$ns3::A/TestInt16" value="-3"/> </ns3>
This le can be archived with your simulation script and output data. While it is possible to generate a sample cong le and lightly edit it to change a couple of values, there are cases where this process will not work because the same value on the same object can appear multiple times in the same automatically-generated conguration le under different conguration paths. As such, the best way to use this class is to use it to generate an initial conguration le, extract from that conguration le only the strictly necessary elements, and move these minimal elements to a new conguration le which can then safely be edited and loaded in a subsequent simulation run. When the CongStore object is instantiated, its attributes Filename, Mode, and FileFormat must be set, either via command-line or via program statements. As a more complicated example, lets assume that we want to read in a conguration of defaults from an input le named input-defaults.xml, and write out the resulting attributes to a separate le called output-attributes.xml. (Note to get this input xml le to begin with, it is sometimes helpful to run the program to generate an output xml le rst, then hand-edit that le and re-input it for the next simulation run).:
#include "ns3/config-store-module.h" ... int main (...) { Config::SetDefault ("ns3::ConfigStore::Filename", StringValue ("input-defaults.xml")); Config::SetDefault ("ns3::ConfigStore::Mode", StringValue ("Load")); Config::SetDefault ("ns3::ConfigStore::FileFormat", StringValue ("Xml")); ConfigStore inputConfig; inputConfig.ConfigureDefaults (); // // Allow the user to override any of the defaults and the above Bind() at // run-time, via command-line arguments // CommandLine cmd; cmd.Parse (argc, argv);
5.6. CongStore
33
// setup topology ... // Invoke just before entering Simulator::Run () Config::SetDefault ("ns3::ConfigStore::Filename", StringValue ("output-attributes.xml")); Config::SetDefault ("ns3::ConfigStore::Mode", StringValue ("Save")); ConfigStore outputConfig; outputConfig.ConfigureAttributes (); Simulator::Run (); }
To check whether it is congured or not, check the output of the step::: ./waf congure enable-examples enable-tests - Summary of optional NS-3 features: Threading Primitives : enabled Real Time Simulator : enabled GtkCongStore : not enabled (library gtk+-2.0 >= 2.12 not found) In the above example, it was not enabled, so it cannot be used until a suitable version is installed and ./waf congure enable-examples enable-tests; ./waf is rerun. Usage is almost the same as the non-GTK-based version, but there are no CongStore attributes involved::
// Invoke just before entering Simulator::Run () GtkConfigStore config; config.ConfigureDefaults (); config.ConfigureAttributes ();
Now, when you run the script, a GUI should pop up, allowing you to open menus of attributes on different nodes/objects, and then launch the simulation execution when you are done.
34
Chapter 5. Attributes
CHAPTER
SIX
OBJECT NAMES
Placeholder chapter
35
36
CHAPTER
SEVEN
LOGGING
This chapter not yet written. For now, the ns-3 tutorial contains logging information.
37
38
Chapter 7. Logging
CHAPTER
EIGHT
TRACING
The tracing subsystem is one of the most important mechanisms to understand in ns-3. In most cases, ns-3 users will have a brilliant idea for some new and improved networking feature. In order to verify that this idea works, the researcher will make changes to an existing system and then run experiments to see how the new feature behaves by gathering statistics that capture the behavior of the feature. In other words, the whole point of running a simulation is to generate output for further study. In ns-3, the subsystem that enables a researcher to do this is the tracing subsystem.
This is workable in small environments, but as your simulations get more and more complicated, you end up with more and more prints and the task of parsing and performing computations on the output begins to get harder and harder. Another thing to consider is that every time a new tidbit is needed, the software core must be edited and another print introduced. There is no standardized way to control all of this output, so the amount of output tends to grow without bounds. Eventually, the bandwidth required for simply outputting this information begins to limit the running time of the simulation. The output les grow to enormous sizes and parsing them becomes a problem. ns-3 provides a simple mechanism for logging and providing some control over output via Log Components, but the level of control is not very ne grained at all. The logging module is a relatively blunt instrument. It is desirable to have a facility that allows one to reach into the core system and only get the information required without having to change and recompile the core system. Even better would be a system that notied the user when an item of interest changed or an interesting event happened. The ns-3 tracing system is designed to work along those lines and is well-integrated with the Attribute and Cong substems allowing for relatively simple use scenarios.
39
8.2 Overview
The tracing subsystem relies heavily on the ns-3 Callback and Attribute mechanisms. You should read and understand the corresponding sections of the manual before attempting to understand the tracing system. The ns-3 tracing system is built on the concepts of independent tracing sources and tracing sinks; along with a uniform mechanism for connecting sources to sinks. Trace sources are entities that can signal events that happen in a simulation and provide access to interesting underlying data. For example, a trace source could indicate when a packet is received by a net device and provide access to the packet contents for interested trace sinks. A trace source might also indicate when an interesting state change happens in a model. For example, the congestion window of a TCP model is a prime candidate for a trace source. Trace sources are not useful by themselves; they must be connected to other pieces of code that actually do something useful with the information provided by the source. The entities that consume trace information are called trace sinks. Trace sources are generators of events and trace sinks are consumers. This explicit division allows for large numbers of trace sources to be scattered around the system in places which model authors believe might be useful. Unless a user connects a trace sink to one of these sources, nothing is output. This arrangement allows relatively unsophisticated users to attach new types of sinks to existing tracing sources, without requiring editing and recompiling the core or models of the simulator. There can be zero or more consumers of trace events generated by a trace source. One can think of a trace source as a kind of point-to-multipoint information link. The transport protocol for this conceptual point-to-multipoint link is an ns-3 Callback. Recall from the Callback Section that callback facility is a way to allow two modules in the system to communicate via function calls while at the same time decoupling the calling function from the called class completely. This is the same requirement as outlined above for the tracing system. Basically, a trace source is a callback to which multiple functions may be registered. When a trace sink expresses interest in receiving trace events, it adds a callback to a list of callbacks held by the trace source. When an interesting event happens, the trace source invokes its operator() providing zero or more parameters. This tells the source to go through its list of callbacks invoking each one in turn. In this way, the parameter(s) are communicated to the trace sinks, which are just functions.
The rst thing to do is include the required les. As mentioned above, the trace system makes heavy use of the Object and Attribute systems. The rst two includes bring in the declarations for those systems. The le, traced-value.h brings in the required declarations for tracing data that obeys value semantics. In general, value semantics just means that you can pass the object around, not an address. In order to use value semantics at all you have to have an object with an associated copy constructor and assignment operator available. We extend the requirements to talk about the set of operators that are pre-dened for plain-old-data (POD) types. Operator=, operator++, operator, operator+, operator==, etc.
40
Chapter 8. Tracing
What this all means is that you will be able to trace changes to an object made using those operators.:
class MyObject : public Object { public: static TypeId GetTypeId (void) { static TypeId tid = TypeId ("MyObject") .SetParent (Object::GetTypeId ()) .AddConstructor<MyObject> () .AddTraceSource ("MyInteger", "An integer value to trace.", MakeTraceSourceAccessor (&MyObject::m_myInt)) ; return tid; } MyObject () {} TracedValue<uint32_t> m_myInt; };
Since the tracing system is integrated with Attributes, and Attributes work with Objects, there must be an ns-3 Object for the trace source to live in. The two important lines of code are the .AddTraceSource and the TracedValue declaration. The .AddTraceSource provides the hooks used for connecting the trace source to the outside world. The TracedValue declaration provides the infrastructure that overloads the operators mentioned above and drives the callback process.:
void IntTrace (Int oldValue, Int newValue) { std::cout << Traced << oldValue << to << newValue << std::endl; }
This is the denition of the trace sink. It corresponds directly to a callback function. This function will be called whenever one of the operators of the TracedValue is executed.:
int main (int argc, char *argv[]) { Ptr<MyObject> myObject = CreateObject<MyObject> (); myObject->TraceConnectWithoutContext ("MyInteger", MakeCallback(&IntTrace)); myObject->m_myInt = 1234; }
In this snippet, the rst thing that needs to be done is to create the object in which the trace source lives. The next step, the TraceConnectWithoutContext, forms the connection between the trace source and the trace sink. Notice the MakeCallback template function. Recall from the Callback section that this creates the specialized functor responsible for providing the overloaded operator() used to re the callback. The overloaded operators (++, , etc.) will use this operator() to actually invoke the callback. The TraceConnectWithoutContext, takes a string parameter that provides the name of the Attribute assigned to the trace source. Lets ignore the bit about context for now since it is not important yet. Finally, the line,:
8.2. Overview
41
myObject->m_myInt = 1234;
should be interpreted as an invocation of operator= on the member variable m_myInt with the integer 1234 passed as a parameter. It turns out that this operator is dened (by TracedValue) to execute a callback that returns void and takes two integer values as parameters an old value and a new value for the integer in question. That is exactly the function signature for the callback function we provided IntTrace. To summarize, a trace source is, in essence, a variable that holds a list of callbacks. A trace sink is a function used as the target of a callback. The Attribute and object type information systems are used to provide a way to connect trace sources to trace sinks. The act of hitting a trace source is executing an operator on the trace source which res callbacks. This results in the trace sink callbacks registering interest in the source being called with the parameters provided by the source.
This should look very familiar. It is the same thing as the previous example, except that a static member function of class Config is being called instead of a method on Object; and instead of an Attribute name, a path is being provided. The rst thing to do is to read the path backward. The last segment of the path must be an Attribute of an Object. In fact, if you had a pointer to the Object that has the CongestionWindow Attribute handy (call it theObject), you could write this just like the previous example::
void CwndTracer (uint32_t oldval, uint32_t newval) {} ... theObject->TraceConnectWithoutContext ("CongestionWindow", MakeCallback (&CwndTracer));
It turns out that the code for Config::ConnectWithoutContext does exactly that. This function takes a path that represents a chain of Object pointers and follows them until it gets to the end of the path and interprets the last segment as an Attribute on the last object. Lets walk through what happens. The leading / character in the path refers to a so-called namespace. One of the predened namespaces in the cong system is NodeList which is a list of all of the nodes in the simulation. Items in the list are referred to by indices into the list, so /NodeList/0 refers to the zeroth node in the list of nodes created by the simulation. This node is actually a Ptr<Node> and so is a subclass of an ns3::Object. As described in the Object Model section, ns-3 supports an object aggregation model. The next path segment begins with the $ character which indicates a GetObject call should be made looking for the type that follows. When a node is initialized by an InternetStackHelper a number of interfaces are aggregated to the node. One of these is the TCP level four protocol. The runtime type of this protocol object is ns3::TcpL4Protocol. When the GetObject is executed, it returns a pointer to the object of this type. 42 Chapter 8. Tracing
The TcpL4Protocol class denes an Attribute called SocketList which is a list of sockets. Each socket is actually an ns3::Object with its own Attributes. The items in the list of sockets are referred to by index just as in the NodeList, so SocketList/0 refers to the zeroth socket in the list of sockets on the zeroth node in the NodeList the rst node constructed in the simulation. This socket, the type of which turns out to be an ns3::TcpSocketImpl denes an attribute called CongestionWindow which is a TracedValue<uint32_t>. The Config::ConnectWithoutContext now does a,:
object->TraceConnectWithoutContext ("CongestionWindow", MakeCallback (&CwndTracer));
using the object pointer from SocketList/0 which makes the connection between the trace source dened in the socket to the callback CwndTracer. Now, whenever a change is made to the TracedValue<uint32_t> representing the congestion window in the TCP socket, the registered callback will be executed and the function CwndTracer will be called printing out the old and new values of the TCP congestion window.
What may not be obvious, though, is that there is a consistent model for all of the trace-related methods found in the system. We will now take a little time and take a look at the big picture. There are currently two primary use cases of the tracing helpers in ns-3: Device helpers and protocol helpers. Device helpers look at the problem of specifying which traces should be enabled through a node, device pair. For example, you may want to specify that pcap tracing should be enabled on a particular device on a specic node. This follows from the ns-3 device conceptual model, and also the conceptual models of the various device helpers. Following naturally from this, the les created follow a <prex>-<node>-<device> naming convention. Protocol helpers look at the problem of specifying which traces should be enabled through a protocol and interface pair. This follows from the ns-3 protocol stack conceptual model, and also the conceptual models of internet stack helpers. Naturally, the trace les should follow a <prex>-<protocol>-<interface> naming convention. The trace helpers therefore fall naturally into a two-dimensional taxonomy. There are subtleties that prevent all four classes from behaving identically, but we do strive to make them all work as similarly as possible; and whenever possible there are analogs for all methods in all classes.: 8.3. Using the Tracing API 43
We use an approach called a mixin to add tracing functionality to our helper classes. A mixin is a class that provides functionality to that is inherited by a subclass. Inheriting from a mixin is not considered a form of specialization but is really a way to collect functionality. Lets take a quick look at all four of these cases and their respective mixins.
The signature of this method reects the device-centric view of the situation at this level. All of the public methods inherited from class PcapUserHelperForDevice reduce to calling this single device-dependent implementation method. For example, the lowest level pcap method,:
void EnablePcap (std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilena
will call the device implementation of EnablePcapInternal directly. All other public pcap tracing methods build on this implementation to provide additional user-level functionality. What this means to the user is that all device helpers in the system will have all of the pcap trace methods available; and these methods will all work in the same way across devices if the device implements EnablePcapInternal correctly. Pcap Tracing Device Helper Methods
void void void void void void
EnablePcap (std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilena EnablePcap (std::string prefix, std::string ndName, bool promiscuous = false, bool explicitFilen EnablePcap (std::string prefix, NetDeviceContainer d, bool promiscuous = false); EnablePcap (std::string prefix, NodeContainer n, bool promiscuous = false); EnablePcap (std::string prefix, uint32_t nodeid, uint32_t deviceid, bool promiscuous = false); EnablePcapAll (std::string prefix, bool promiscuous = false);
In each of the methods shown above, there is a default parameter called promiscuous that defaults to false. This parameter indicates that the trace should not be gathered in promiscuous mode. If you do want your traces to include all trafc seen by the device (and if the device supports a promiscuous mode) simply add a true parameter to any of the calls above. For example,:
Ptr<NetDevice> nd; ... helper.EnablePcap ("prefix", nd, true);
will enable promiscuous mode captures on the NetDevice specied by nd. The rst two methods also include a default parameter called explicitFilename that will be discussed below. 44 Chapter 8. Tracing
You are encouraged to peruse the Doxygen for class PcapHelperForDevice to nd the details of these methods; but to summarize ... You can enable pcap tracing on a particular node/net-device pair by providing a Ptr<NetDevice> to an EnablePcap method. The Ptr<Node> is implicit since the net device must belong to exactly one Node. For example,:
Ptr<NetDevice> nd; ... helper.EnablePcap ("prefix", nd);
You can enable pcap tracing on a particular node/net-device pair by providing a std::string representing an object name service string to an EnablePcap method. The Ptr<NetDevice> is looked up from the name string. Again, the <Node> is implicit since the named net device must belong to exactly one Node. For example,:
Names::Add ("server" ...); Names::Add ("server/eth0" ...); ... helper.EnablePcap ("prefix", "server/ath0");
You can enable pcap tracing on a collection of node/net-device pairs by providing a NetDeviceContainer. For each NetDevice in the container the type is checked. For each device of the proper type (the same type as is managed by the device helper), tracing is enabled. Again, the <Node> is implicit since the found net device must belong to exactly one Node. For example,:
NetDeviceContainer d = ...; ... helper.EnablePcap ("prefix", d);
You can enable pcap tracing on a collection of node/net-device pairs by providing a NodeContainer. For each Node in the NodeContainer its attached NetDevices are iterated. For each NetDevice attached to each node in the container, the type of that device is checked. For each device of the proper type (the same type as is managed by the device helper), tracing is enabled.:
NodeContainer n; ... helper.EnablePcap ("prefix", n);
You can enable pcap tracing on the basis of node ID and device ID as well as with explicit Ptr. Each Node in the system has an integer node ID and each device connected to a node has an integer device ID.:
helper.EnablePcap ("prefix", 21, 1);
Finally, you can enable pcap tracing for all devices in the system, with the same type as that managed by the device helper.:
helper.EnablePcapAll ("prefix");
Pcap Tracing Device Helper Filename Selection Implicit in the method descriptions above is the construction of a complete lename by the implementation method. By convention, pcap traces in the ns-3 system are of the form <prefix>-<node id>-<device id>.pcap As previously mentioned, every node in the system will have a system-assigned node id; and every device will have an interface index (also called a device id) relative to its node. By default, then, a pcap trace le created as a result of enabling tracing on the rst device of node 21 using the prex prex would be prefix-21-1.pcap. You can always use the ns-3 object name service to make this more clear. For example, if you use the object name service to assign the name server to node 21, the resulting pcap trace le name will automatically become, 8.4. Using Trace Helpers 45
prefix-server-1.pcap and if you also assign the name eth0 to the device, your pcap le name will automatically pick this up and be called prefix-server-eth0.pcap. Finally, two of the methods shown above,:
void EnablePcap (std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilena void EnablePcap (std::string prefix, std::string ndName, bool promiscuous = false, bool explicitFilen
have a default parameter called explicitFilename. When set to true, this parameter disables the automatic lename completion mechanism and allows you to create an explicit lename. This option is only available in the methods which enable pcap tracing on a single device. For example, in order to arrange for a device helper to create a single promiscuous pcap capture le of a specic name (my-pcap-file.pcap) on a given device, one could::
Ptr<NetDevice> nd; ... helper.EnablePcap ("my-pcap-file.pcap", nd, true, true);
The rst true parameter enables promiscuous mode traces and the second tells the helper to interpret the prefix parameter as a complete lename.
The signature of this method reects the device-centric view of the situation at this level; and also the fact that the helper may be writing to a shared output stream. All of the public ascii-trace-related methods inherited from class AsciiTraceHelperForDevice reduce to calling this single device- dependent implementation method. For example, the lowest level ascii trace methods,:
void EnableAscii (std::string prefix, Ptr<NetDevice> nd); void EnableAscii (Ptr<OutputStreamWrapper> stream, Ptr<NetDevice> nd);
will call the device implementation of EnableAsciiInternal directly, providing either a valid prex or stream. All other public ascii tracing methods will build on these low-level functions to provide additional user-level functionality. What this means to the user is that all device helpers in the system will have all of the ascii trace methods available; and these methods will all work in the same way across devices if the devices implement EnablAsciiInternal correctly. Ascii Tracing Device Helper Methods
void EnableAscii (std::string prefix, Ptr<NetDevice> nd); void EnableAscii (Ptr<OutputStreamWrapper> stream, Ptr<NetDevice> nd); void EnableAscii (std::string prefix, std::string ndName); void EnableAscii (Ptr<OutputStreamWrapper> stream, std::string ndName); void EnableAscii (std::string prefix, NetDeviceContainer d); void EnableAscii (Ptr<OutputStreamWrapper> stream, NetDeviceContainer d);
46
Chapter 8. Tracing
void EnableAscii (std::string prefix, NodeContainer n); void EnableAscii (Ptr<OutputStreamWrapper> stream, NodeContainer n); void EnableAscii (std::string prefix, uint32_t nodeid, uint32_t deviceid); void EnableAscii (Ptr<OutputStreamWrapper> stream, uint32_t nodeid, uint32_t deviceid); void EnableAsciiAll (std::string prefix); void EnableAsciiAll (Ptr<OutputStreamWrapper> stream);
You are encouraged to peruse the Doxygen for class TraceHelperForDevice to nd the details of these methods; but to summarize ... There are twice as many methods available for ascii tracing as there were for pcap tracing. This is because, in addition to the pcap-style model where traces from each unique node/device pair are written to a unique le, we support a model in which trace information for many node/device pairs is written to a common le. This means that the <prex>-<node>-<device> le name generation mechanism is replaced by a mechanism to refer to a common le; and the number of API methods is doubled to allow all combinations. Just as in pcap tracing, you can enable ascii tracing on a particular node/net-device pair by providing a Ptr<NetDevice> to an EnableAscii method. The Ptr<Node> is implicit since the net device must belong to exactly one Node. For example,:
Ptr<NetDevice> nd; ... helper.EnableAscii ("prefix", nd);
In this case, no trace contexts are written to the ascii trace le since they would be redundant. The system will pick the le name to be created using the same rules as described in the pcap section, except that the le will have the sufx .tr instead of .pcap. If you want to enable ascii tracing on more than one net device and have all traces sent to a single le, you can do that as well by using an object to refer to a single le::
Ptr<NetDevice> nd1; Ptr<NetDevice> nd2; ... Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr"); ... helper.EnableAscii (stream, nd1); helper.EnableAscii (stream, nd2);
In this case, trace contexts are written to the ascii trace le since they are required to disambiguate traces from the two devices. Note that since the user is completely specifying the le name, the string should include the .tr for consistency. You can enable ascii tracing on a particular node/net-device pair by providing a std::string representing an object name service string to an EnablePcap method. The Ptr<NetDevice> is looked up from the name string. Again, the <Node> is implicit since the named net device must belong to exactly one Node. For example,:
Names::Add ("client" ...); Names::Add ("client/eth0" ...); Names::Add ("server" ...); Names::Add ("server/eth0" ...); ... helper.EnableAscii ("prefix", "client/eth0"); helper.EnableAscii ("prefix", "server/eth0");
This would result in two les named prefix-client-eth0.tr and prefix-server-eth0.tr with traces for each device in the respective trace le. Since all of the EnableAscii functions are overloaded to take a stream 8.4. Using Trace Helpers 47
This would result in a single trace le called trace-file-name.tr that contains all of the trace events for both devices. The events would be disambiguated by trace context strings. You can enable ascii tracing on a collection of node/net-device pairs by providing a NetDeviceContainer. For each NetDevice in the container the type is checked. For each device of the proper type (the same type as is managed by the device helper), tracing is enabled. Again, the <Node> is implicit since the found net device must belong to exactly one Node. For example,:
NetDeviceContainer d = ...; ... helper.EnableAscii ("prefix", d);
This would result in a number of ascii trace les being created, each of which follows the <prex>-<node id>-<device id>.tr convention. Combining all of the traces into a single le is accomplished similarly to the examples above::
NetDeviceContainer d = ...; ... Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr"); ... helper.EnableAscii (stream, d);
You can enable ascii tracing on a collection of node/net-device pairs by providing a NodeContainer. For each Node in the NodeContainer its attached NetDevices are iterated. For each NetDevice attached to each node in the container, the type of that device is checked. For each device of the proper type (the same type as is managed by the device helper), tracing is enabled.:
NodeContainer n; ... helper.EnableAscii ("prefix", n);
This would result in a number of ascii trace les being created, each of which follows the <prex>-<node id>-<device id>.tr convention. Combining all of the traces into a single le is accomplished similarly to the examples above: You can enable pcap tracing on the basis of node ID and device ID as well as with explicit Ptr. Each Node in the system has an integer node ID and each device connected to a node has an integer device ID.:
helper.EnableAscii ("prefix", 21, 1);
Of course, the traces can be combined into a single le as shown above. Finally, you can enable pcap tracing for all devices in the system, with the same type as that managed by the device helper.:
helper.EnableAsciiAll ("prefix");
This would result in a number of ascii trace les being created, one for every device in the system of the type managed by the helper. All of these les will follow the <prex>-<node id>-<device id>.tr convention. Combining all of the traces into a single le is accomplished similarly to the examples above.
48
Chapter 8. Tracing
Ascii Tracing Device Helper Filename Selection Implicit in the prex-style method descriptions above is the construction of the complete lenames by the implementation method. By convention, ascii traces in the ns-3 system are of the form <prefix>-<node id>-<device id>.tr. As previously mentioned, every node in the system will have a system-assigned node id; and every device will have an interface index (also called a device id) relative to its node. By default, then, an ascii trace le created as a result of enabling tracing on the rst device of node 21, using the prex prex, would be prefix-21-1.tr. You can always use the ns-3 object name service to make this more clear. For example, if you use the object name service to assign the name server to node 21, the resulting ascii trace le name will automatically become, prefix-server-1.tr and if you also assign the name eth0 to the device, your ascii trace le name will automatically pick this up and be called prefix-server-eth0.tr.
The signature of this method reects the protocol and interface-centric view of the situation at this level. All of the public methods inherited from class PcapHelperForIpv4 reduce to calling this single device-dependent implementation method. For example, the lowest level pcap method,:
void EnablePcapIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface);
will call the device implementation of EnablePcapIpv4Internal directly. All other public pcap tracing methods build on this implementation to provide additional user-level functionality. What this means to the user is that all protocol helpers in the system will have all of the pcap trace methods available; and these methods will all work in the same way across protocols if the helper implements EnablePcapIpv4Internal correctly. Pcap Tracing Protocol Helper Methods These methods are designed to be in one-to-one correspondence with the Node- and NetDevice- centric versions of the device versions. Instead of Node and NetDevice pair constraints, we use protocol and interface constraints. Note that just like in the device version, there are six methods::
void void void void EnablePcapIpv4 EnablePcapIpv4 EnablePcapIpv4 EnablePcapIpv4 (std::string (std::string (std::string (std::string prefix, prefix, prefix, prefix, Ptr<Ipv4> ipv4, uint32_t interface); std::string ipv4Name, uint32_t interface); Ipv4InterfaceContainer c); NodeContainer n);
49
void EnablePcapIpv4 (std::string prefix, uint32_t nodeid, uint32_t interface); void EnablePcapIpv4All (std::string prefix);
You are encouraged to peruse the Doxygen for class PcapHelperForIpv4 to nd the details of these methods; but to summarize ... You can enable pcap tracing on a particular protocol/interface pair by providing a Ptr<Ipv4> and interface to an EnablePcap method. For example,:
Ptr<Ipv4> ipv4 = node->GetObject<Ipv4> (); ... helper.EnablePcapIpv4 ("prefix", ipv4, 0);
You can enable pcap tracing on a particular node/net-device pair by providing a std::string representing an object name service string to an EnablePcap method. The Ptr<Ipv4> is looked up from the name string. For example,:
Names::Add ("serverIPv4" ...); ... helper.EnablePcapIpv4 ("prefix", "serverIpv4", 1);
You can enable pcap tracing on a collection of protocol/interface pairs by providing an Ipv4InterfaceContainer. For each Ipv4 / interface pair in the container the protocol type is checked. For each protocol of the proper type (the same type as is managed by the device helper), tracing is enabled for the corresponding interface. For example,:
NodeContainer nodes; ... NetDeviceContainer devices = deviceHelper.Install (nodes); ... Ipv4AddressHelper ipv4; ipv4.SetBase ("10.1.1.0", "255.255.255.0"); Ipv4InterfaceContainer interfaces = ipv4.Assign (devices); ... helper.EnablePcapIpv4 ("prefix", interfaces);
You can enable pcap tracing on a collection of protocol/interface pairs by providing a NodeContainer. For each Node in the NodeContainer the appropriate protocol is found. For each protocol, its interfaces are enumerated and tracing is enabled on the resulting pairs. For example,:
NodeContainer n; ... helper.EnablePcapIpv4 ("prefix", n);
You can enable pcap tracing on the basis of node ID and interface as well. In this case, the node-id is translated to a Ptr<Node> and the appropriate protocol is looked up in the node. The resulting protocol and interface are used to specify the resulting trace source.:
helper.EnablePcapIpv4 ("prefix", 21, 1);
Finally, you can enable pcap tracing for all interfaces in the system, with associated protocol being the same type as that managed by the device helper.:
helper.EnablePcapIpv4All ("prefix");
Pcap Tracing Protocol Helper Filename Selection Implicit in all of the method descriptions above is the construction of the complete lenames by the implementation method. By convention, pcap traces taken for devices in the ns-3 system are of the form <prefix>-<node 50 Chapter 8. Tracing
id>-<device id>.pcap. In the case of protocol traces, there is a one-to-one correspondence between protocols and Nodes. This is because protocol Objects are aggregated to Node Objects. Since there is no global protocol id in the system, we use the corresponding node id in le naming. Therefore there is a possibility for le name collisions in automatically chosen trace le names. For this reason, the le name convention is changed for protocol traces. As previously mentioned, every node in the system will have a system-assigned node id. Since there is a one-to-one correspondence between protocol instances and node instances we use the node id. Each interface has an interface id relative to its protocol. We use the convention <prex>-n<node id>-i<interface id>.pcap for trace le naming in protocol helpers. Therefore, by default, a pcap trace le created as a result of enabling tracing on interface 1 of the Ipv4 protocol of node 21 using the prex prex would be prex-n21-i1.pcap. You can always use the ns-3 object name service to make this more clear. For example, if you use the object name service to assign the name serverIpv4 to the Ptr<Ipv4> on node 21, the resulting pcap trace le name will automatically become, prex-nserverIpv4-i1.pcap.
The signature of this method reects the protocol- and interface-centric view of the situation at this level; and also the fact that the helper may be writing to a shared output stream. All of the public methods inherited from class PcapAndAsciiTraceHelperForIpv4 reduce to calling this single device- dependent implementation method. For example, the lowest level ascii trace methods,:
void EnableAsciiIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface); void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, Ptr<Ipv4> ipv4, uint32_t interface);
will call the device implementation of EnableAsciiIpv4Internal directly, providing either the prex or the stream. All other public ascii tracing methods will build on these low-level functions to provide additional user-level functionality. What this means to the user is that all device helpers in the system will have all of the ascii trace methods available; and these methods will all work in the same way across protocols if the protocols implement EnablAsciiIpv4Internal correctly. Ascii Tracing Device Helper Methods
void EnableAsciiIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface); void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, Ptr<Ipv4> ipv4, uint32_t interface); void EnableAsciiIpv4 (std::string prefix, std::string ipv4Name, uint32_t interface); void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, std::string ipv4Name, uint32_t interface); void EnableAsciiIpv4 (std::string prefix, Ipv4InterfaceContainer c); void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, Ipv4InterfaceContainer c);
51
void EnableAsciiIpv4 (std::string prefix, NodeContainer n); void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, NodeContainer n); void EnableAsciiIpv4 (std::string prefix, uint32_t nodeid, uint32_t deviceid); void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, uint32_t nodeid, uint32_t interface); void EnableAsciiIpv4All (std::string prefix); void EnableAsciiIpv4All (Ptr<OutputStreamWrapper> stream);
You are encouraged to peruse the Doxygen for class PcapAndAsciiHelperForIpv4 to nd the details of these methods; but to summarize ... There are twice as many methods available for ascii tracing as there were for pcap tracing. This is because, in addition to the pcap-style model where traces from each unique protocol/interface pair are written to a unique le, we support a model in which trace information for many protocol/interface pairs is written to a common le. This means that the <prex>-n<node id>-<interface> le name generation mechanism is replaced by a mechanism to refer to a common le; and the number of API methods is doubled to allow all combinations. Just as in pcap tracing, you can enable ascii tracing on a particular protocol/interface pair by providing a Ptr<Ipv4> and an interface to an EnableAscii method. For example,:
Ptr<Ipv4> ipv4; ... helper.EnableAsciiIpv4 ("prefix", ipv4, 1);
In this case, no trace contexts are written to the ascii trace le since they would be redundant. The system will pick the le name to be created using the same rules as described in the pcap section, except that the le will have the sufx .tr instead of .pcap. If you want to enable ascii tracing on more than one interface and have all traces sent to a single le, you can do that as well by using an object to refer to a single le. We have already something similar to this in the cwnd example above::
Ptr<Ipv4> protocol1 = node1->GetObject<Ipv4> (); Ptr<Ipv4> protocol2 = node2->GetObject<Ipv4> (); ... Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr"); ... helper.EnableAsciiIpv4 (stream, protocol1, 1); helper.EnableAsciiIpv4 (stream, protocol2, 1);
In this case, trace contexts are written to the ascii trace le since they are required to disambiguate traces from the two interfaces. Note that since the user is completely specifying the le name, the string should include the .tr for consistency. You can enable ascii tracing on a particular protocol by providing a std::string representing an object name service string to an EnablePcap method. The Ptr<Ipv4> is looked up from the name string. The <Node> in the resulting lenames is implicit since there is a one-to-one correspondence between protocol instances and nodes, For example,:
Names::Add ("node1Ipv4" ...); Names::Add ("node2Ipv4" ...); ... helper.EnableAsciiIpv4 ("prefix", "node1Ipv4", 1); helper.EnableAsciiIpv4 ("prefix", "node2Ipv4", 1);
This would result in two les named prex-nnode1Ipv4-i1.tr and prex-nnode2Ipv4-i1.tr with traces for each interface in the respective trace le. Since all of the EnableAscii functions are overloaded to take a stream wrapper, you can use that form as well:: 52 Chapter 8. Tracing
Names::Add ("node1Ipv4" ...); Names::Add ("node2Ipv4" ...); ... Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr"); ... helper.EnableAsciiIpv4 (stream, "node1Ipv4", 1); helper.EnableAsciiIpv4 (stream, "node2Ipv4", 1);
This would result in a single trace le called trace-le-name.tr that contains all of the trace events for both interfaces. The events would be disambiguated by trace context strings. You can enable ascii tracing on a collection of protocol/interface pairs by providing an Ipv4InterfaceContainer. For each protocol of the proper type (the same type as is managed by the device helper), tracing is enabled for the corresponding interface. Again, the <Node> is implicit since there is a one-to-one correspondence between each protocol and its node. For example,:
NodeContainer nodes; ... NetDeviceContainer devices = deviceHelper.Install (nodes); ... Ipv4AddressHelper ipv4; ipv4.SetBase ("10.1.1.0", "255.255.255.0"); Ipv4InterfaceContainer interfaces = ipv4.Assign (devices); ... ... helper.EnableAsciiIpv4 ("prefix", interfaces);
This would result in a number of ascii trace les being created, each of which follows the <prex>-n<node id>i<interface>.tr convention. Combining all of the traces into a single le is accomplished similarly to the examples above::
NodeContainer nodes; ... NetDeviceContainer devices = deviceHelper.Install (nodes); ... Ipv4AddressHelper ipv4; ipv4.SetBase ("10.1.1.0", "255.255.255.0"); Ipv4InterfaceContainer interfaces = ipv4.Assign (devices); ... Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr"); ... helper.EnableAsciiIpv4 (stream, interfaces);
You can enable ascii tracing on a collection of protocol/interface pairs by providing a NodeContainer. For each Node in the NodeContainer the appropriate protocol is found. For each protocol, its interfaces are enumerated and tracing is enabled on the resulting pairs. For example,:
NodeContainer n; ... helper.EnableAsciiIpv4 ("prefix", n);
This would result in a number of ascii trace les being created, each of which follows the <prex>-<node id>-<device id>.tr convention. Combining all of the traces into a single le is accomplished similarly to the examples above: You can enable pcap tracing on the basis of node ID and device ID as well. In this case, the node-id is translated to a Ptr<Node> and the appropriate protocol is looked up in the node. The resulting protocol and interface are used to specify the resulting trace source.:
53
Of course, the traces can be combined into a single le as shown above. Finally, you can enable ascii tracing for all interfaces in the system, with associated protocol being the same type as that managed by the device helper.:
helper.EnableAsciiIpv4All ("prefix");
This would result in a number of ascii trace les being created, one for every interface in the system related to a protocol of the type managed by the helper. All of these les will follow the <prex>-n<node id>-i<interface.tr convention. Combining all of the traces into a single le is accomplished similarly to the examples above. Ascii Tracing Device Helper Filename Selection Implicit in the prex-style method descriptions above is the construction of the complete lenames by the implementation method. By convention, ascii traces in the ns-3 system are of the form <prex>-<node id>-<device id>.tr. As previously mentioned, every node in the system will have a system-assigned node id. Since there is a one-to-one correspondence between protocols and nodes we use to node-id to identify the protocol identity. Every interface on a given protocol will have an interface index (also called simply an interface) relative to its protocol. By default, then, an ascii trace le created as a result of enabling tracing on the rst device of node 21, using the prex prex, would be prex-n21-i1.tr. Use the prex to disambiguate multiple protocols per node. You can always use the ns-3 object name service to make this more clear. For example, if you use the object name service to assign the name serverIpv4 to the protocol on node 21, and also specify interface one, the resulting ascii trace le name will automatically become, prex-nserverIpv4-1.tr.
54
Chapter 8. Tracing
CHAPTER
NINE
REALTIME
ns-3 has been designed for integration into testbed and virtual machine environments. To integrate with real network stacks and emit/consume packets, a real-time scheduler is needed to try to lock the simulation clock with the hardware clock. We describe here a component of this: the RealTime scheduler. The purpose of the realtime scheduler is to cause the progression of the simulation clock to occur synchronously with respect to some external time base. Without the presence of an external time base (wall clock), simulation time jumps instantly from one simulated time to the next.
9.1 Behavior
When using a non-realtime scheduler (the default in ns-3), the simulator advances the simulation time to the next scheduled event. During event execution, simulation time is frozen. With the realtime scheduler, the behavior is similar from the perspective of simulation models (i.e., simulation time is frozen during event execution), but between events, the simulator will attempt to keep the simulation clock aligned with the machine clock. When an event is nished executing, and the scheduler moves to the next event, the scheduler compares the next event execution time with the machine clock. If the next event is scheduled for a future time, the simulator sleeps until that realtime is reached and then executes the next event. It may happen that, due to the processing inherent in the execution of simulation events, that the simulator cannot keep up with realtime. In such a case, it is up to the user conguration what to do. There are two ns-3 attributes that govern the behavior. The rst is ns3::RealTimeSimulatorImpl::SynchronizationMode. The two entries possible for this attribute are BestEffort (the default) or HardLimit. In BestEffort mode, the simulator will just try to catch up to realtime by executing events until it reaches a point where the next event is in the (realtime) future, or else the simulation ends. In BestEffort mode, then, it is possible for the simulation to consume more time than the wall clock time. The other option HardLimit will cause the simulation to abort if the tolerance threshold is exceeded. This attribute is ns3::RealTimeSimulatorImpl::HardLimit and the default is 0.1 seconds. A different mode of operation is one in which simulated time is not frozen during an event execution. This mode of realtime simulation was implemented but removed from the ns-3 tree because of questions of whether it would be useful. If users are interested in a realtime simulator for which simulation time does not freeze during event execution (i.e., every call to Simulator::Now() returns the current wall clock time, not the time at which the event started executing), please contact the ns-developers mailing list.
9.2 Usage
The usage of the realtime simulator is straightforward, from a scripting perspective. Users just need to set the attribute SimulatorImplementationType to the Realtime simulator, such as follows:
55
There is a script in examples/realtime/realtime-udp-echo.cc that has an example of how to congure the realtime behavior. Try:
./waf --run realtime-udp-echo
Whether the simulator will work in a best effort or hard limit policy fashion is governed by the attributes explained in the previous section.
9.3 Implementation
The implementation is contained in the following les: src/core/model/realtime-simulator-impl.{cc,h} src/core/model/wall-clock-synchronizer.{cc,h} In order to create a realtime scheduler, to a rst approximation you just want to cause simulation time jumps to consume real time. We propose doing this using a combination of sleep- and busy- waits. Sleep-waits cause the calling process (thread) to yield the processor for some amount of time. Even though this specied amount of time can be passed to nanosecond resolution, it is actually converted to an OS-specic granularity. In Linux, the granularity is called a Jiffy. Typically this resolution is insufcient for our needs (on the order of a ten milliseconds), so we round down and sleep for some smaller number of Jifes. The process is then awakened after the specied number of Jifes has passed. At this time, we have some residual time to wait. This time is generally smaller than the minimum sleep time, so we busy-wait for the remainder of the time. This means that the thread just sits in a for loop consuming cycles until the desired time arrives. After the combination of sleep- and busy-waits, the elapsed realtime (wall) clock should agree with the simulation time of the next event and the simulation proceeds.
56
Chapter 9. RealTime
CHAPTER
TEN
HELPERS
The above chapters introduced you to various ns-3 programming concepts such as smart pointers for reference-counted memory management, attributes, namespaces, callbacks, etc. Users who work at this low-level API can interconnect ns-3 objects with ne granulariy. However, a simulation program written entirely using the low-level API would be quite long and tedious to code. For this reason, a separate so-called helper API has been overlaid on the core ns3 API. If you have read the ns-3 tutorial, you will already be familiar with the helper API, since it is the API that new users are typically introduced to rst. In this chapter, we introduce the design philosophy of the helper API and contrast it to the low-level API. If you become a heavy user of ns-3, you will likely move back and forth between these APIs even in the same program. The helper API has a few goals: 1. the rest of src/ has no dependencies on the helper API; anything that can be done with the helper API can be coded also at the low-level API 2. Containers: Often simulations will need to do a number of identical actions to groups of objects. The helper API makes heavy use of containers of similar objects to which similar or identical operations can be performed. 3. The helper API is not generic; it does not strive to maximize code reuse. So, programming constructs such as polymorphism and templates that achieve code reuse are not as prevalent. For instance, there are separate CsmaNetDevice helpers and PointToPointNetDevice helpers but they do not derive from a common NetDevice base class. 4. The helper API typically works with stack-allocated (vs. heap-allocated) objects. For some programs, ns-3 users may not need to worry about any low level Object Create or Ptr handling; they can make do with containers of objects and stack-allocated helpers that operate on them. The helper API is really all about making ns-3 programs easier to write and read, without taking away the power of the low-level interface. The rest of this chapter provides some examples of the programming conventions of the helper API.
57
58
CHAPTER
ELEVEN
This should produce the following gnuplot control les in the directory where the example is located:
plot-2d.plt plot-2d-with-error-bars.plt plot-3d.plt
59
This should produce the following graphics les in the directory where the example is located:
plot-2d.png plot-2d-with-error-bars.png plot-3d.png
You can view these graphics les in your favorite graphics viewer. If you have gimp installed on your machine, for example, you can do this:
gimp plot-2d.png gimp plot-2d-with-error-bars.png gimp plot-3d.png
was created using the following code from gnuplot-example.cc: 60 Chapter 11. Making Plots using the Gnuplot Class
using namespace std; string string string string string fileNameWithNoExtension graphicsFileName plotFileName plotTitle dataTitle = = = = = "plot-2d"; fileNameWithNoExtension + ".png"; fileNameWithNoExtension + ".plt"; "2-D Plot"; "2-D Data";
// Instantiate the plot and set its title. Gnuplot plot (graphicsFileName); plot.SetTitle (plotTitle); // Make the graphics file, which the plot file will create when it // is used with Gnuplot, be a PNG file. plot.SetTerminal ("png"); // Set the labels for each axis. plot.SetLegend ("X Values", "Y Values"); // Set the range for the x axis. plot.AppendExtra ("set xrange [-6:+6]"); // Instantiate the dataset, set its title, and make the points be // plotted along with connecting lines. Gnuplot2dDataset dataset; dataset.SetTitle (dataTitle); dataset.SetStyle (Gnuplot2dDataset::LINES_POINTS); double x; double y; // Create the 2-D dataset. for (x = -5.0; x <= +5.0; x += 1.0) { // Calculate the 2-D curve // // 2 // y = x . // y = x * x; // Add this point. dataset.Add (x, y); } // Add the dataset to the plot. plot.AddDataset (dataset); // Open the plot file. ofstream plotFile (plotFileName.c_str()); // Write the plot file. plot.GenerateOutput (plotFile); // Close the plot file. plotFile.close ();
61
// Instantiate the plot and set its title. Gnuplot plot (graphicsFileName); plot.SetTitle (plotTitle); // Make the graphics file, which the plot file will create when it // is used with Gnuplot, be a PNG file. plot.SetTerminal ("png"); // Set the labels for each axis. plot.SetLegend ("X Values", "Y Values");
62
// Set the range for the x axis. plot.AppendExtra ("set xrange [-6:+6]"); // Instantiate the dataset, set its title, and make the points be // plotted with no connecting lines. Gnuplot2dDataset dataset; dataset.SetTitle (dataTitle); dataset.SetStyle (Gnuplot2dDataset::POINTS); // Make the dataset have error bars in both the x and y directions. dataset.SetErrorBars (Gnuplot2dDataset::XY); double double double double x; xErrorDelta; y; yErrorDelta;
// Create the 2-D dataset. for (x = -5.0; x <= +5.0; x += 1.0) { // Calculate the 2-D curve // // 2 // y = x . // y = x * x; // Make the uncertainty in the x direction be constant and make // the uncertainty in the y direction be a constant fraction of // ys value. xErrorDelta = 0.25; yErrorDelta = 0.1 * y; // Add this point with uncertainties in both the x and y // direction. dataset.Add (x, y, xErrorDelta, yErrorDelta); } // Add the dataset to the plot. plot.AddDataset (dataset); // Open the plot file. ofstream plotFile (plotFileName.c_str()); // Write the plot file. plot.GenerateOutput (plotFile); // Close the plot file. plotFile.close ();
63
64
using namespace std; string string string string string fileNameWithNoExtension graphicsFileName plotFileName plotTitle dataTitle = = = = = "plot-3d"; fileNameWithNoExtension + ".png"; fileNameWithNoExtension + ".plt"; "3-D Plot"; "3-D Data";
// Instantiate the plot and set its title. Gnuplot plot (graphicsFileName); plot.SetTitle (plotTitle); // Make the graphics file, which the plot file will create when it // is used with Gnuplot, be a PNG file. plot.SetTerminal ("png"); // Rotate the plot 30 degrees around the x axis and then rotate the // plot 120 degrees around the new z axis. plot.AppendExtra ("set view 30, 120, 1.0, 1.0"); // Make the zero for the z-axis be in the x-axis and y-axis plane. plot.AppendExtra ("set ticslevel 0"); // Set the labels for each axis. plot.AppendExtra ("set xlabel X Values"); plot.AppendExtra ("set ylabel Y Values"); plot.AppendExtra ("set zlabel Z Values"); // Set the ranges for the x and y axis. plot.AppendExtra ("set xrange [-5:+5]"); plot.AppendExtra ("set yrange [-5:+5]"); // Instantiate the dataset, set its title, and make the points be // connected by lines. Gnuplot3dDataset dataset; dataset.SetTitle (dataTitle); dataset.SetStyle ("with lines"); double x; double y; double z; // Create the 3-D dataset. for (x = -5.0; x <= +5.0; x += 1.0) { for (y = -5.0; y <= +5.0; y += 1.0) { // Calculate the 3-D surface // // 2 2 // z = x . * y // z = x * x * y * y; // Add this point. dataset.Add (x, y, z); }
65
// The blank line is necessary at the end of each x values data // points for the 3-D surface grid to work. dataset.AddEmptyLine (); } // Add the dataset to the plot. plot.AddDataset (dataset); // Open the plot file. ofstream plotFile (plotFileName.c_str()); // Write the plot file. plot.GenerateOutput (plotFile); // Close the plot file. plotFile.close ();
66
CHAPTER
TWELVE
12.1 Introduction
The goal of Python bindings for ns-3 are two fold: 1. Allow the programmer to write complete simulation scripts in Python (http://www.python.org); 2. Prototype new models (e.g. routing protocols). For the time being, the primary focus of the bindings is the rst goal, but the second goal will eventually be supported as well. Python bindings for ns-3 are being developed using a new tool called PyBindGen (http://code.google.com/p/pybindgen).
ns.core.LogComponentEnable("UdpEchoClientApplication", ns.core.LOG_LEVEL_INFO) ns.core.LogComponentEnable("UdpEchoServerApplication", ns.core.LOG_LEVEL_INFO) nodes = ns.network.NodeContainer() nodes.Create(2) pointToPoint = ns.point_to_point.PointToPointHelper() pointToPoint.SetDeviceAttribute("DataRate", ns.core.StringValue("5Mbps")) pointToPoint.SetChannelAttribute("Delay", ns.core.StringValue("2ms")) devices = pointToPoint.Install(nodes)
67
stack = ns.internet.InternetStackHelper() stack.Install(nodes) address = ns.internet.Ipv4AddressHelper() address.SetBase(ns.network.Ipv4Address("10.1.1.0"), ns.network.Ipv4Mask("255.255.255.0")) interfaces = address.Assign (devices); echoServer = ns.applications.UdpEchoServerHelper(9) serverApps = echoServer.Install(nodes.Get(1)) serverApps.Start(ns.core.Seconds(1.0)) serverApps.Stop(ns.core.Seconds(10.0)) echoClient = ns.applications.UdpEchoClientHelper(interfaces.GetAddress(1), 9) echoClient.SetAttribute("MaxPackets", ns.core.UintegerValue(1)) echoClient.SetAttribute("Interval", ns.core.TimeValue(ns.core.Seconds (1.0))) echoClient.SetAttribute("PacketSize", ns.core.UintegerValue(1024)) clientApps = echoClient.Install(nodes.Get(0)) clientApps.Start(ns.core.Seconds(2.0)) clientApps.Stop(ns.core.Seconds(10.0)) ns.core.Simulator.Run() ns.core.Simulator.Destroy()
To run your own Python script that calls ns-3 /path/to/your/example/my-script.py, do the following:
./waf --shell python /path/to/your/example/my-script.py
and
that
has
this
path,
12.4 Caveats
Python bindings for ns-3 are a work in progress, and some limitations are known by developers. Some of these limitations (not all) are listed here.
68
Conversion constructors (http://publib.boulder.ibm.com/infocenter/compbgpl/v9v111/topic/com.ibm.xlcpp9.bg.doc/language_ref/cplr38 are not fully supported yet by PyBindGen, and they always act as explicit constructors when translating an API into Python. For example, in C++ you can do this:
Ipv4AddressHelper ipAddrs; ipAddrs.SetBase ("192.168.0.0", "255.255.255.0"); ipAddrs.Assign (backboneDevices);
12.4.3 CommandLine
CommandLine::AddValue() works differently in Python than it does in ns-3. In Python, the rst parameter is a string that represents the command-line option name. When the option is set, an attribute with the same name as the option name is set on the CommandLine() object. Example:
NUM_NODES_SIDE_DEFAULT = 3 cmd = ns3.CommandLine()
cmd.NumNodesSide = None cmd.AddValue("NumNodesSide", "Grid side number of nodes (total number of nodes will be this number sq cmd.Parse(argv) [...] if cmd.NumNodesSide is None: num_nodes_side = NUM_NODES_SIDE_DEFAULT else: num_nodes_side = int(cmd.NumNodesSide)
12.4. Caveats
69
12.4.4 Tracing
Callback based tracing is not yet properly supported for Python, as new ns-3 API needs to be provided for this to be supported. Pcap le writing is supported via the normal API. Ascii tracing is supported since ns-3.4 via the normal C++ API translated to Python. However, ascii tracing requires the creation of an ostream object to pass into the ascii tracing methods. In Python, the C++ std::ofstream has been minimally wrapped to allow this. For example:
ascii = ns3.ofstream("wifi-ap.tr") # create the file ns3.YansWifiPhyHelper.EnableAsciiAll(ascii) ns3.Simulator.Run() ns3.Simulator.Destroy() ascii.close() # close the file
There is one caveat: you must not allow the le object to be garbage collected while ns-3 is still using it. That means that the ascii variable above must not be allowed to go out of scope or else the program will crash.
70
If something goes wrong with compiling Python bindings and you just want to ignore them and move on with C++, you can disable Python with:
./waf --disable-python
With modular Python bindings: 1. There is one separate Python extension module for each ns-3 module; 2. Scanning API denitions (apidefs) is done on a per ns- module basis; 3. Each modules apidefs les are stored in a bindings subdirectory of the module directory; 12.6. Instructions for Handling New Files or Changed APIs 71
To scan the modular Python bindings for all of the modules, do the following:
./waf --apiscan=all
72
73
74
CHAPTER
THIRTEEN
TESTS
13.1 Overview
This document is concerned with the testing and validation of ns-3 software. This document provides background about terminology and software testing (Chapter 2); a description of the ns-3 testing framework (Chapter 3); a guide to model developers or new model contributors for how to write tests (Chapter 4); In brief, the rst three chapters should be read by ns developers and contributors who need to understand how to contribute test code and validated programs, and the remainder of the document provides space for people to report on what aspects of selected models have been validated.
13.2 Background
This chapter may be skipped by readers familiar with the basics of software testing. Writing defect-free software is a difcult proposition. There are many dimensions to the problem and there is much confusion regarding what is meant by different terms in different contexts. We have found it worthwhile to spend a little time reviewing the subject and dening some terms. Software testing may be loosely dened as the process of executing a program with the intent of nding errors. When one enters a discussion regarding software testing, it quickly becomes apparent that there are many distinct mind-sets with which one can approach the subject. For example, one could break the process into broad functional categories like correctness testing, performance testing, robustness testing and security testing. Another way to look at the problem is by life-cycle: requirements testing, design testing, acceptance testing, and maintenance testing. Yet another view is by the scope of the tested system. In this case one may speak of unit testing, component testing, integration testing, and system testing. These terms are also not standardized in any way, and so maintenance testing and regression testing may be heard interchangeably. Additionally, these terms are often misused. There are also a number of different philosophical approaches to software testing. For example, some organizations advocate writing test programs before actually implementing the desired software, yielding test-driven development. Some organizations advocate testing from a customer perspective as soon as possible, following a parallel with the agile development process: test early and test often. This is sometimes called agile testing. It seems that there is at least one approach to testing for every development methodology.
75
The ns-3 project is not in the business of advocating for any one of these processes, but the project as a whole has requirements that help inform the test process. Like all major software products, ns-3 has a number of qualities that must be present for the product to succeed. From a testing perspective, some of these qualities that must be addressed are that ns-3 must be correct, robust, performant and maintainable. Ideally there should be metrics for each of these dimensions that are checked by the tests to identify when the product fails to meet its expectations / requirements.
13.2.1 Correctness
The essential purpose of testing is to determine that a piece of software behaves correctly. For ns-3 this means that if we simulate something, the simulation should faithfully represent some physical entity or process to a specied accuracy and precision. It turns out that there are two perspectives from which one can view correctness. Verifying that a particular model is implemented according to its specication is generically called verication. The process of deciding that the model is correct for its intended use is generically called validation.
The ns-3 testing environment provides tools to allow for both model validation and testing, and encourages the publication of validation results.
13.2.3 Robustness
Robustness is the quality of being able to withstand stresses, or changes in environments, inputs or calculations, etc. A system or design is robust if it can deal with such changes with minimal loss of functionality. This kind of testing is usually done with a particular focus. For example, the system as a whole can be run on many different system congurations to demonstrate that it can perform correctly in a large number of environments. The system can be also be stressed by operating close to or beyond capacity by generating or simulating resource exhaustion of various kinds. This genre of testing is called stress testing. The system and its components may be exposed to so-called clean tests that demonstrate a positive result that is that the system operates correctly in response to a large variation of expected congurations. The system and its components may also be exposed to dirty tests which provide inputs outside the expected range. For example, if a module expects a zero-terminated string representation of an integer, a dirty test might provide an unterminated string of random characters to verify that the system does not crash as a result of this unexpected input. Unfortunately, detecting such dirty input and taking preventive measures to ensure the system does not fail catastrophically can require a huge amount of development overhead. In order to reduce development time, a decision was taken early on in the project to minimize the amount of parameter validation and error handling in the ns-3 codebase. For this reason, we do not spend much time on dirty testing it would just uncover the results of the design decision we know we took. We do want to demonstrate that ns-3 software does work across some set of conditions. We borrow a couple of denitions to narrow this down a bit. The domain of applicability is a set of prescribed conditions for which the model has been tested, compared against reality to the extent possible, and judged suitable for use. The range of accuracy is an agreement between the computerized model and reality within a domain of applicability. The ns-3 testing environment provides tools to allow for setting up and running test environments over multiple systems (buildbot) and provides classes to encourage clean tests to verify the operation of the system over the expected domain of applicability and range of accuracy.
13.2.4 Performant
Okay, performant isnt a real English word. It is, however, a very concise neologism that is quite often used to describe what we want ns-3 to be: powerful and fast enough to get the job done. This is really about the broad subject of software performance testing. One of the key things that is done is to compare two systems to nd which performs better (cf benchmarks). This is used to demonstrate that, for example, ns-3 can perform a basic kind of simulation at least as fast as a competing tool, or can be used to identify parts of the system that perform badly. In the ns-3 test framework, we provide support for timing various kinds of tests.
13.2.5 Maintainability
A software product must be maintainable. This is, again, a very broad statement, but a testing framework can help with the task. Once a model has been developed, validated and veried, we can repeatedly execute the suite of tests for the entire system to ensure that it remains valid and veried over its lifetime. When a feature stops functioning as intended after some kind of change to the system is integrated, it is called generically a regression. Originally the term regression referred to a change that caused a previously xed bug to reappear,
13.2. Background
77
but the term has evolved to describe any kind of change that breaks existing functionality. There are many kinds of regressions that may occur in practice. A local regression is one in which a change affects the changed component directly. For example, if a component is modied to allocate and free memory but stale pointers are used, the component itself fails. A remote regression is one in which a change to one component breaks functionality in another component. This reects violation of an implied but possibly unrecognized contract between components. An unmasked regression is one that creates a situation where a previously existing bug that had no affect is suddenly exposed in the system. This may be as simple as exercising a code path for the rst time. A performance regression is one that causes the performance requirements of the system to be violated. For example, doing some work in a low level function that may be repeated large numbers of times may suddenly render the system unusable from certain perspectives. The ns-3 testing framework provides tools for automating the process used to validate and verify the code in nightly test suites to help quickly identify possible regressions.
13.3.1 BuildBots
At the highest level of ns-3 testing are the buildbots (build robots). If you are unfamiliar with this system look at http://djmitche.github.com/buildbot/docs/0.7.11/. This is an open-source automated system that allows ns-3 to be rebuilt and tested each time something has changed. By running the buildbots on a number of different systems we can ensure that ns-3 builds and executes properly on all of its supported systems. Users (and developers) typically will not interact with the buildbot system other than to read its messages regarding test results. If a failure is detected in one of the automated build and test jobs, the buildbot will send an email to the ns-developers mailing list. This email will look something like:
The Buildbot has detected a new failure of osx-ppc-g++-4.2 on NsNam. Full details are available at: http://ns-regression.ee.washington.edu:8010/builders/osx-ppc-g%2B%2B-4.2/builds/0 Buildbot URL: http://ns-regression.ee.washington.edu:8010/ Buildslave for this Build: darwin-ppc Build Reason: The web-page force build button was pressed by ww: ww Build Source Stamp: HEAD Blamelist: BUILD FAILED: failed shell_5 shell_6 shell_7 shell_8 shell_9 shell_10 shell_11 shell_12 sincerely, -The Buildbot
78
In the full details URL shown in the email, one can search for the keyword failed and select the stdio link for the corresponding step to see the reason for the failure. The buildbot will do its job quietly if there are no errors, and the system will undergo build and test cycles every day to verify that all is well.
13.3.2 Test.py
The buildbots use a Python program, test.py, that is responsible for running all of the tests and collecting the resulting reports into a human- readable form. This program is also available for use by users and developers as well. test.py is very exible in allowing the user to specify the number and kind of tests to run; and also the amount and kind of output to generate. Before running test.py, make sure that ns3s examples and tests have been built by doing the following
./waf configure --enable-examples --enable-tests ./waf
By default, test.py will run all available tests and report status back in a very concise form. Running the command
./test.py
will result in a number of PASS, FAIL, CRASH or SKIP indications followed by the kind of test that was run and its display name.
Waf: Entering directory /home/craigdo/repos/ns-3-allinone-test/ns-3-dev/build Waf: Leaving directory /home/craigdo/repos/ns-3-allinone-test/ns-3-dev/build build finished successfully (0.939s) FAIL: TestSuite ns3-wifi-propagation-loss-models PASS: TestSuite object-name-service PASS: TestSuite pcap-file-object PASS: TestSuite ns3-tcp-cwnd ... PASS: TestSuite ns3-tcp-interoperability PASS: Example csma-broadcast PASS: Example csma-multicast
This mode is intended to be used by users who are interested in determining if their distribution is working correctly, and by developers who are interested in determining if changes they have made have caused any regressions. There are a number of options available to control the behavior of test.py. if you run test.py --help you should see a command summary like:
Usage: test.py [options] Options: -h, --help show this help message and exit -b BUILDPATH, --buildpath=BUILDPATH specify the path where ns-3 was built (defaults to the build directory for the current variant) -c KIND, --constrain=KIND constrain the test-runner by kind of test -e EXAMPLE, --example=EXAMPLE specify a single example to run (with relative path) -g, --grind run the test suites and examples using valgrind -k, --kinds print the kinds of tests available -l, --list print the list of known tests -m, --multiple report multiple failures from test suites and test
79
cases -n, --nowaf do not run waf before starting testing -p PYEXAMPLE, --pyexample=PYEXAMPLE specify a single python example to run (with relative path) -r, --retain retain all temporary files (which are normally deleted) -s TEST-SUITE, --suite=TEST-SUITE specify a single test suite to run -t TEXT-FILE, --text=TEXT-FILE write detailed test results into TEXT-FILE.txt -v, --verbose print progress and informational messages -w HTML-FILE, --web=HTML-FILE, --html=HTML-FILE write detailed test results into HTML-FILE.html -x XML-FILE, --xml=XML-FILE write detailed test results into XML-FILE.xml
If one species an optional output style, one can generate detailed descriptions of the tests and status. Available styles are text and HTML. The buildbots will select the HTML option to generate HTML test reports for the nightly builds using
./test.py --html=nightly.html
In this case, an HTML le named nightly.html would be created with a pretty summary of the testing done. A human readable format is available for users interested in the details.
./test.py --text=results.txt
In the example above, the test suite checking the ns-3 wireless device propagation loss models failed. By default no further information is provided. To further explore the failure, test.py allows a single test suite to be specied. Running the command
./test.py --suite=ns3-wifi-propagation-loss-models
To nd detailed information regarding the failure, one must specify the kind of output desired. For example, most people will probably be interested in a text le:
./test.py --suite=ns3-wifi-propagation-loss-models --text=results.txt
This will result in that single test suite being run with the test status written to the le results.txt. You should nd something similar to the following in that le:
FAIL: Test Suite ns3-wifi-propagation-loss-models (real 0.02 user 0.01 system 0.00) PASS: Test Case "Check ... Friis ... model ..." (real 0.01 user 0.00 system 0.00) FAIL: Test Case "Check ... Log Distance ... model" (real 0.01 user 0.01 system 0.00) Details: Message: Got unexpected SNR value Condition: [long description of what actually failed] Actual: 176.395 Limit: 176.407 +- 0.0005 File: ../src/test/ns3wifi/propagation-loss-models-test-suite.cc Line: 360
Notice that the Test Suite is composed of two Test Cases. The rst test case checked the Friis propagation loss model and passed. The second test case failed checking the Log Distance propagation model. In this case, an SNR of 176.395 80 Chapter 13. Tests
was found, and the test expected a value of 176.407 correct to three decimal places. The le which implemented the failing test is listed as well as the line of code which triggered the failure. If you desire, you could just as easily have written an HTML le using the --html option as described above. Typically a user will run all tests at least once after downloading ns-3 to ensure that his or her environment has been built correctly and is generating correct results according to the test suites. Developers will typically run the test suites before and after making a change to ensure that they have not introduced a regression with their changes. In this case, developers may not want to run all tests, but only a subset. For example, the developer might only want to run the unit tests periodically while making changes to a repository. In this case, test.py can be told to constrain the types of tests being run to a particular class of tests. The following command will result in only the unit tests being run:
./test.py --constrain=unit
Similarly, the following command will result in only the example smoke tests being run:
./test.py --constrain=unit
To see a quick list of the legal kinds of constraints, you can ask for them to be listed. The following command
./test.py --kinds
Waf: Entering directory /home/craigdo/repos/ns-3-allinone-test/ns-3-dev/build Waf: Leaving directory /home/craigdo/repos/ns-3-allinone-test/ns-3-dev/build build finished successfully (0.939s)Waf: Entering directory /home/craigdo/repos/ns-3-allinone-test bvt: Build Verification Tests (to see if build completed successfully) core: Run all TestSuite-based tests (exclude examples) example: Examples (to see if example programs run successfully) performance: Performance Tests (check to see if the system is as fast as expected) system: System Tests (spans modules to check integration of modules) unit: Unit Tests (within modules to check basic functionality)
Any of these kinds of test can be provided as a constraint using the --constraint option. To see a quick list of all of the test suites available, you can ask for them to be listed. The following command,
./test.py --list
will result in a list of the test suite being displayed, similar to:
Waf: Entering directory /home/craigdo/repos/ns-3-allinone-test/ns-3-dev/build Waf: Leaving directory /home/craigdo/repos/ns-3-allinone-test/ns-3-dev/build build finished successfully (0.939s) histogram ns3-wifi-interference ns3-tcp-cwnd ns3-tcp-interoperability sample devices-mesh-flame devices-mesh-dot11s devices-mesh ... object-name-service callback attributes config global-value command-line
81
basic-random-number object
Any of these listed suites can be selected to be run by itself using the --suite option as shown above. Similarly to test suites, one can run a single C++ example program using the --example option. Note that the relative path for the example must be included and that the executables built for C++ examples do not have extensions. Entering
./test.py --example=examples/udp/udp-echo
You can specify the directory where ns-3 was built using the --buildpath option as follows.
One can run a single Python example program using the --pyexample option. Note that the relative path for the example must be included and that Python examples do need their extensions. Entering
./test.py --pyexample=examples/tutorial/first.py
Because Python examples are not built, you do not need to specify the directory where ns-3 was built to run them. Normally when example programs are executed, they write a large amount of trace le data. This is normally saved to the base directory of the distribution (e.g., /home/user/ns-3-dev). When test.py runs an example, it really is completely unconcerned with the trace les. It just wants to to determine if the example can be built and run without error. Since this is the case, the trace les are written into a /tmp/unchecked-traces directory. If you run the above example, you should be able to nd the associated udp-echo.tr and udp-echo-n-1.pcap les there. The list of available examples is dened by the contents of the examples directory in the distribution. If you select an example for execution using the --example option, test.py will not make any attempt to decide if the example has been congured or not, it will just try to run it and report the result of the attempt. When test.py runs, by default it will rst ensure that the system has been completely built. This can be defeated by selecting the --nowaf option.
./test.py --list --nowaf
will result in a list of the currently built test suites being displayed, similar to:
ns3-wifi-propagation-loss-models ns3-tcp-cwnd ns3-tcp-interoperability pcap-file-object object-name-service random-number-generators
Note the absence of the Waf build messages. test.py also supports running the test suites and examples under valgrind. Valgrind is a exible program for debugging and proling Linux executables. By default, valgrind runs a tool called memcheck, which performs a range of memory- checking functions, including detecting accesses to uninitialised memory, misuse of allocated memory (double frees, access after free, etc.) and detecting memory leaks. This can be selected by using the --grind option.
82
./test.py --grind
As it runs, test.py and the programs that it runs indirectly, generate large numbers of temporary les. Usually, the content of these les is not interesting, however in some cases it can be useful (for debugging purposes) to view these les. test.py provides a --retain option which will cause these temporary les to be kept after the run is completed. The les are saved in a directory named testpy-output under a subdirectory named according to the current Coordinated Universal Time (also known as Greenwich Mean Time).
./test.py --retain
Finally, test.py provides a --verbose option which will print large amounts of information about its progress. It is not expected that this will be terribly useful unless there is an error. In this case, you can get access to the standard output and standard error reported by running test suites and examples. Select verbose in the following way:
./test.py --verbose
All of these options can be mixed and matched. For example, to run all of the ns-3 core test suites under valgrind, in verbose mode, while generating an HTML output le, one would do:
./test.py --verbose --grind --constrain=core --html=results.html
13.3.3 TestTaxonomy
As mentioned above, tests are grouped into a number of broadly dened classications to allow users to selectively run tests to address the different kinds of testing that need to be done. Build Verication Tests Unit Tests System Tests Examples Performance Tests BuildVericationTests These are relatively simple tests that are built along with the distribution and are used to make sure that the build is pretty much working. Our current unit tests live in the source les of the code they test and are built into the ns-3 modules; and so t the description of BVTs. BVTs live in the same source code that is built into the ns-3 code. Our current tests are examples of this kind of test. Unit Tests Unit tests are more involved tests that go into detail to make sure that a piece of code works as advertised in isolation. There is really no reason for this kind of test to be built into an ns-3 module. It turns out, for example, that the unit tests for the object name service are about the same size as the object name service code itself. Unit tests are tests that check a single bit of functionality that are not built into the ns-3 code, but live in the same directory as the code it tests. It is possible that these tests check integration of multiple implementation les in a module as well. The le src/core/test/names-test-suite.cc is an example of this kind of test. The le src/network/test/pcap-le-test-suite.cc is another example that uses a known good pcap le as a test vector le. This le is stored locally in the src/network directory.
83
System Tests System tests are those that involve more than one module in the system. We have lots of this kind of test running in our current regression framework, but they are typically overloaded examples. We provide a new place for this kind of test in the directory src/test. The le src/test/ns3tcp/ns3-interop-test-suite.cc is an example of this kind of test. It uses NSC TCP to test the ns-3 TCP implementation. Often there will be test vectors required for this kind of test, and they are stored in the directory where the test lives. For example, ns3tcp-interop-response-vectors.pcap is a le consisting of a number of TCP headers that are used as the expected responses of the ns-3 TCP under test to a stimulus generated by the NSC TCP which is used as a known good implementation. Examples The examples are tested by the framework to make sure they built and will run. Nothing is checked, and currently the pcap les are just written off into /tmp to be discarded. If the examples run (dont crash) they pass this smoke test. Performance Tests Performance tests are those which exercise a particular part of the system and determine if the tests have executed to completion in a reasonable time. Running Tests Tests are typically run using the high level test.py program. To get a list of the available command-line options, run test.py --help
84
--suite=suite-name: --verbose:
Run the test suite named suite-name Turn on messages in the run test suites
There are a number of things available to you which will be familiar to you if you have looked at test.py. This should be expected since the test- runner is just an interface between test.py and ns-3. You may notice that example-related commands are missing here. That is because the examples are really not ns-3 tests. test.py runs them as if they were to present a unied testing environment, but they are really completely different and not to be found here. The rst new option that appears here, but not in test.py is the --assert option. This option is useful when debugging a test case when running under a debugger like gdb. When selected, this option tells the underlying test case to cause a segmentation violation if an error is detected. This has the nice side-effect of causing program execution to stop (break into the debugger) when an error is detected. If you are using gdb, you could use this option something like,
./waf shell cd build/debug/utils gdb test-runner run --suite=global-value --assert
If an error is then found in the global-value test suite, a segfault would be generated and the (source level) debugger would stop at the NS_TEST_ASSERT_MSG that detected the error. Another new option that appears here is the --basedir option. It turns out that some tests may need to reference the source directory of the ns-3 distribution to nd local data, so a base directory is always required to run a test. If you run a test from test.py, the Python program will provide the basedir option for you. To run one of the tests directly from the test-runner using waf, you will need to specify the test suite to run along with the base directory. So you could use the shell and do:
./waf --run "test-runner --basedir=pwd --suite=pcap-file-object"
Note the backward quotation marks on the pwd command. If you are running the test suite out of a debugger, it can be quite painful to remember and constantly type the absolute path of the distribution base directory. Because of this, if you omit the basedir, the test-runner will try to gure one out for you. It begins in the current working directory and walks up the directory tree looking for a directory le with les named VERSION and LICENSE. If it nds one, it assumes that must be the basedir and provides it for you. Similarly, many test suites need to write temporary les (such as pcap les) in the process of running the tests. The tests then need a temporary directory to write to. The Python test utility (test.py) will provide a temporary le automatically, but if run stand-alone this temporary directory must be provided. Just as in the basedir case, it can be annoying to continually have to provide a --tempdir, so the test runner will gure one out for you if you dont provide one. It rst looks for environment variables named TMP and TEMP and uses those. If neither TMP nor TEMP are dened it picks /tmp. The code then tacks on an identier indicating what created the directory (ns-3) then the time (hh.mm.ss) followed by a large random number. The test runner creates a directory of that name to be used as the temporary directory. Temporary les then go into a directory that will be named something like
/tmp/ns-3.10.25.37.61537845
The time is provided as a hint so that you can relatively easily reconstruct what directory was used if you need to go back and look at the les that were placed in that directory. When you run a test suite using the test-runner it will run the test quietly by default. The only indication that you will get that the test passed is the absence of a message from waf saying that the program returned something other than a zero exit code. To get some output from the test, you need to specify an output le to which the tests will write their XML status using the --out option. You need to be careful interpreting the results because the test suites will append results onto this le. Try,
85
If you are familiar with XML this should be fairly self-explanatory. It is also not a complete XML le since test suites are designed to have their output appended to a master XML status le as described in the test.py section.
86
Build Verication Tests Unit Tests System Tests Examples Performance Tests This classication is exported from the TestSuite class. This class is quite simple, existing only as a place to export this type and to accumulate test cases. From a user perspective, in order to create a new TestSuite in the system one only has to dene a new class that inherits from class TestSuite and perform these two duties. The following code will dene a new class that can be run by test.py as a unit test with the display name, my-test-suite-name.
class MySuite : public TestSuite { public: MyTestSuite (); }; MyTestSuite::MyTestSuite () : TestSuite ("my-test-suite-name", UNIT) { AddTestCase (new MyTestCase); } MyTestSuite myTestSuite;
The base class takes care of all of the registration and reporting required to be a good citizen in the test framework.
87
Utilities There are a number of utilities of various kinds that are also part of the testing framework. Examples include a generalized pcap le useful for storing test vectors; a generic container useful for transient storage of test vectors during test execution; and tools for generating presentations based on validation and verication testing results. Debugging test suite failures To debug test crashes, such as:
CRASH: TestSuite ns3-wifi-interference
You can access the underlying test-runner program via gdb as follows, and then pass the basedir=pwd argument to run (you can also pass other arguments as needed, but basedir is the minimum needed):
./waf --command-template="gdb %s" --run "test-runner" Waf: Entering directory /home/tomh/hg/sep09/ns-3-allinone/ns-3-dev-678/build Waf: Leaving directory /home/tomh/hg/sep09/ns-3-allinone/ns-3-dev-678/build build finished successfully (0.380s) GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. L cense GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu"... (gdb) r --basedir=pwd Starting program: <..>/build/debug/utils/test-runner --basedir=pwd [Thread debugging using libthread_db enabled] assert failed. file=../src/core/model/type-id.cc, line=138, cond="uid <= m_information.size () && uid ...
Here is another example of how to use valgrind to debug a memory problem such as:
VALGR: TestSuite devices-mesh-dot11s-regression
88
What the test suite will be called What type of test it will be (Build Verication Test, Unit Test, System Test, or Performance Test) Where the test code will live (either in an existing ns-3 module or separately in src/test/ directory). You will have to edit the wscript le in that directory to compile your new code, if it is a new le. See the le src/template/test/sample-test-suite.cc and corresponding wscript le in that directory for a simple example, and see the directories under src/test for more complicated examples. The rest of this chapter remains to be written
13.4.2 How to add an example program to the test suite 13.4.3 Testing for boolean outcomes 13.4.4 Testing outcomes when randomness is involved 13.4.5 Testing output data against a known distribution 13.4.6 Providing non-trivial input vectors of data 13.4.7 Storing and referencing non-trivial output data 13.4.8 Presenting your output test data
89
90
CHAPTER
FOURTEEN
SUPPORT
14.1 Creating a new ns-3 model
This chapter walks through the design process of an ns-3 model. In many research cases, users will not be satised to merely adapt existing models, but may want to extend the core of the simulator in a novel way. We will use the example of adding an ErrorModel to a simple ns-3 link as a motivating example of how one might approach this problem and proceed through a design and implementation.
14.1.1 Design-approach
Consider how you want it to work; what should it do. Think about these things: functionality: What functionality should it have? What attributes or conguration is exposed to the user? reusability: How much should others be able to reuse my design? Can I reuse code from ns-2 to get started? How does a user integrate the model with the rest of another simulation? dependencies: How can I reduce the introduction of outside dependencies on my new code as much as possible (to make it more modular)? For instance, should I avoid any dependence on IPv4 if I want it to also be used by IPv6? Should I avoid any dependency on IP at all? Do not be hesitant to contact the ns-3-users or ns-developers list if you have questions. In particular, it is important to think about the public API of your new model and ask for feedback. It also helps to let others know of your work in case you are interested in collaborators. Example: ErrorModel An error model exists in ns-2. It allows packets to be passed to a stateful object that determines, based on a random variable, whether the packet is corrupted. The caller can then decide what to do with the packet (drop it, etc.). The main API of the error model is a function to pass a packet to, and the return value of this function is a boolean that tells the caller whether any corruption occurred. Note that depending on the error model, the packet data buffer may or may not be corrupted. Lets call this function IsCorrupt(). So far, in our design, we have::
class ErrorModel { public: /** * \returns true if the Packet is to be considered as errored/corrupted * \param pkt Packet to apply error model to
91
Note that we do not pass a const pointer, thereby allowing the function to modify the packet if IsCorrupt() returns true. Not all error models will actually modify the packet; whether or not the packet data buffer is corrupted should be documented. We may also want specialized versions of this, such as in ns-2, so although it is not the only design choice for polymorphism, we assume that we will subclass a base class ErrorModel for specialized classes, such as RateErrorModel, ListErrorModel, etc, such as is done in ns-2. You may be thinking at this point, Why not make IsCorrupt() a virtual method?. That is one approach; the other is to make the public non-virtual function indirect through a private virtual function (this in C++ is known as the non virtual interface idiom and is adopted in the ns-3 ErrorModel class). Next, should this device have any dependencies on IP or other protocols? We do not want to create dependencies on Internet protocols (the error model should be applicable to non-Internet protocols too), so well keep that in mind later. Another consideration is how objects will include this error model. We envision putting an explicit setter in certain NetDevice implementations, for example.:
/** * Attach a receive ErrorModel to the PointToPointNetDevice. * * The PointToPointNetDevice may optionally include an ErrorModel in * the packet receive chain. * * @see ErrorModel * @param em Ptr to the ErrorModel. */ void PointToPointNetDevice::SetReceiveErrorModel(Ptr<ErrorModel> em);
Again, this is not the only choice we have (error models could be aggregated to lots of other objects), but it satises our primary use case, which is to allow a user to force errors on otherwise successful packet transmissions, at the NetDevice level. After some thinking and looking at existing ns-2 code, here is a sample API of a base class and rst subclass that could be posted for initial review::
class ErrorModel { public: ErrorModel (); virtual ~ErrorModel (); bool IsCorrupt (Ptr<Packet> pkt); void Reset (void); void Enable (void); void Disable (void); bool IsEnabled (void) const; private: virtual bool DoCorrupt (Ptr<Packet> pkt) = 0; virtual void DoReset (void) = 0; }; enum ErrorUnit { EU_BIT, EU_BYTE, EU_PKT
92
}; // Determine which packets are errored corresponding to an underlying // random variable distribution, an error rate, and unit for the rate. class RateErrorModel : public ErrorModel { public: RateErrorModel (); virtual ~RateErrorModel (); enum ErrorUnit GetUnit (void) const; void SetUnit (enum ErrorUnit error_unit); double GetRate (void) const; void SetRate (double rate); void SetRandomVariable (const RandomVariable &ranvar); private: virtual bool DoCorrupt (Ptr<Packet> pkt); virtual void DoReset (void); };
14.1.2 Scaffolding
Lets say that you are ready to start implementing; you have a fairly clear picture of what you want to build, and you may have solicited some initial review or suggestions from the list. One way to approach the next step (implementation) is to create scaffolding and ll in the details as the design matures. This section walks through many of the steps you should consider to dene scaffolding, or a non-functional skeleton of what your model will eventually implement. It is usually good practice to not wait to get these details integrated at the end, but instead to plumb a skeleton of your model into the system early and then add functions later once the API and integration seems about right. Note that you will want to modify a few things in the below presentation for your model since if you follow the error model verbatim, the code you produce will collide with the existing error model. The below is just an outline of how ErrorModel was built that you can adapt to other models. Review the ns-3 coding style document At this point, you may want to pause and read the ns-3 coding style document, especially if you are considering to contribute your code back to the project. The coding style document is linked off the main project page: ns-3 coding style. Decide where in the source tree the model will reside in All of the ns-3 model source code is in the directory src/. You will need to choose which subdirectory it resides in. If it is new model code of some sort, it makes sense to put it into the src/ directory somewhere, particularly for ease of integrating with the build system. In the case of the error model, it is very related to the packet class, so it makes sense to implement this in the src/network/model directory where ns-3 packets are implemented. waf and wscript ns-3 uses the Waf build system. You will want to integrate your new ns-3 uses the Waf build system. You will want to integrate your new source les into this system. This requires that you add your les to the wscript le found in
93
each directory. Lets start with empty les error-model.h and error-model.cc, and add this to src/network/wscript. It is really just a matter of adding the .cc le to the rest of the source les, and the .h le to the list of the header les. Now, pop up to the top level directory and type ./test.py. You shouldnt have broken anything by this operation. include guards Next, lets add some include guards in our header le.:
#ifndef ERROR_MODEL_H #define ERROR_MODEL_H ... #endif
namespace ns3 ns-3 uses the ns-3 namespace to isolate its symbols from other namespaces. Typically, a user will next put an ns-3 namespace block in both the cc and h le.:
namespace ns3 { ... }
At this point, we have some skeletal les in which we can start dening our new classes. The header le looks like this::
#ifndef ERROR_MODEL_H #define ERROR_MODEL_H namespace ns3 { } // namespace ns3 #endif
These les should compile since they dont really have any contents. Were now ready to start adding classes.
the ns-3 type and attribute system (see Attributes) an object aggregation system a smart-pointer reference counting system (class Ptr) Classes that derive from class ObjectBase} get the rst two properties above, but do not get smart pointers. Classes that derive from class RefCountBase get only the smart-pointer reference counting system. In practice, class Object is the variant of the three above that the ns-3 developer will most commonly encounter. In our case, we want to make use of the attribute system, and we will be passing instances of this object across the ns-3 public API, so class Object is appropriate for us. initial classes One way to proceed is to start by dening the bare minimum functions and see if they will compile. Lets review what all is needed to implement when we derive from class Object.:
#ifndef ERROR_MODEL_H #define ERROR_MODEL_H #include "ns3/object.h" namespace ns3 { class ErrorModel : public Object { public: static TypeId GetTypeId (void); ErrorModel (); virtual ~ErrorModel (); }; class RateErrorModel : public ErrorModel { public: static TypeId GetTypeId (void); RateErrorModel (); virtual ~RateErrorModel (); }; #endif
A few things to note here. We need to include object.h. The convention in ns-3 is that if the header le is co-located in the same directory, it may be included without any path prex. Therefore, if we were implementing ErrorModel in src/core/model directory, we could have just said #include "object.h". But we are in src/network/model, so we must include it as #include "ns3/object.h". Note also that this goes outside the namespace declaration. Second, each class must implement a static public member function called GetTypeId (void). Third, it is a good idea to implement constructors and destructors rather than to let the compiler generate them, and to make the destructor virtual. In C++, note also that copy assignment operator and copy constructors are auto-generated if they are not dened, so if you do not want those, you should implement those as private members. This aspect of C++ is discussed in Scott Meyers Effective C++ book. item 45. Lets now look at some corresponding skeletal implementation code in the .cc le.:
95
#include "error-model.h" namespace ns3 { NS_OBJECT_ENSURE_REGISTERED (ErrorModel); TypeId ErrorModel::GetTypeId (void) { static TypeId tid = TypeId ("ns3::ErrorModel") .SetParent<Object> () ; return tid; } ErrorModel::ErrorModel () { } ErrorModel::~ErrorModel () { } NS_OBJECT_ENSURE_REGISTERED (RateErrorModel); TypeId RateErrorModel::GetTypeId (void) { static TypeId tid = TypeId ("ns3::RateErrorModel") .SetParent<ErrorModel> () .AddConstructor<RateErrorModel> () ; return tid; } RateErrorModel::RateErrorModel () { } RateErrorModel::~RateErrorModel () { }
What is the GetTypeId (void) function? This function does a few things. It registers a unique string into the TypeId system. It establishes the hierarchy of objects in the attribute system (via SetParent). It also declares that certain objects can be created via the object creation framework (AddConstructor). The macro NS_OBJECT_ENSURE_REGISTERED (classname) is needed also once for every class that denes a new GetTypeId method, and it does the actual registration of the class into the system. The Object model chapter discusses this in more detail. how to include les from elsewhere log component Here, write a bit about adding |ns3| logging macros. Note that LOG_COMPONENT_DEFINE is done outside the namespace ns3
96
constructor, empty function prototypes key variables (default values, attributes) test program 1 Object Framework :: static const ClassId cid; const InterfaceId ErrorModel::iid = MakeInterfaceId (ErrorModel, Object::iid); const ClassId ErrorModel::cid = MakeClassId<ErrorModel> (ErrorModel, ErrorModel::iid);
14.1.4 Adding-a-sample-script
At this point, one may want to try to take the basic scaffolding dened above and add it into the system. Performing this step now allows one to use a simpler model when plumbing into the system and may also reveal whether any design or API modications need to be made. Once this is done, we will return to building out the functionality of the ErrorModels themselves. Add basic support in the class
point-to-point-net-device.h class ErrorModel; /** * Error model for receive packet events */ Ptr<ErrorModel> m_receiveErrorModel;
Add Accessor
void PointToPointNetDevice::SetReceiveErrorModel (Ptr<ErrorModel> em) { NS_LOG_FUNCTION (this << em); m_receiveErrorModel = em; } .AddAttribute ("ReceiveErrorModel", "The receiver error model used to simulate packet loss", PointerValue (), MakePointerAccessor (&PointToPointNetDevice::m_receiveErrorModel), MakePointerChecker<ErrorModel> ())
97
if (m_receiveErrorModel && m_receiveErrorModel->IsCorrupt (packet) ) { // // If we have an error model and it indicates that it is time to lose a // corrupted packet, dont forward this packet up, let it go. // m_dropTrace (packet); } else { // // Hit the receive trace hook, strip off the point-to-point protocol header // and forward this packet up the protocol stack. // m_rxTrace (packet); ProcessHeader(packet, protocol); m_rxCallback (this, packet, protocol, GetRemote ()); if (!m_promiscCallback.IsNull ()) { m_promiscCallback (this, packet, protocol, GetRemote (), GetAddress (), NetDevice::PACKET_HOST); } } }
At this point, we can run the program with our trivial ErrorModel plumbed into the receive path of the PointToPointNetDevice. It prints out the string Corrupt! for each packet received at node n3. Next, we return to the error model to add in a subclass that performs more interesting error modeling.
98
What properties do we want this to have, from a user interface perspective? We would like for the user to be able to trivially swap out the type of ErrorModel used in the NetDevice. We would also like the capability to set congurable parameters. Here are a few simple requirements we will consider: Ability to set the random variable that governs the losses (default is UniformVariable) Ability to set the unit (bit, byte, packet, time) of granularity over which errors are applied. Ability to set the rate of errors (e.g. 10^-3) corresponding to the above unit of granularity. Ability to enable/disable (default is enabled) How to subclass We declare BasicErrorModel to be a subclass of ErrorModel as follows,:
class BasicErrorModel : public ErrorModel { public: static TypeId GetTypeId (void); ... private: // Implement base class pure virtual functions virtual bool DoCorrupt (Ptr<Packet> p); virtual bool DoReset (void); ... }
and congure the subclass GetTypeId function by setting a unique TypeId string and setting the Parent to ErrorModel::
TypeId RateErrorModel::GetTypeId (void) { static TypeId tid = TypeId ("ns3::RateErrorModel") .SetParent<ErrorModel> () .AddConstructor<RateErrorModel> () ...
14.1.6 Build-core-functions-and-unit-tests
assert macros Writing unit tests
src/spectrum
A prototypical module has the following directory structure and required les:
src/ module-name/ bindings/ doc/ examples/ wscript helper/ model/ test/ examples-to-run.py wscript
14.2.2 Step 2 - Create your new module based on the template module
A python program is provided in the source directory that will create a skeleton for a new module
src/create-module.py
For the purposes of this discussion we will assume that your new module is called new-module. From the src directory, do the following to create the new module:
./create-module.py new-module
We next walk through how to customize this module. All ns-3 modules depend on the core module and usually on other modules. This dependency is specied in the wscript le. Lets assume that new-module depends on the internet, mobility, and aodv modules. Then the call to the function that will create this module should look like this before editing:
def build(bld): module = bld.create_ns3_module(new-module, [core])
Your module will most likely have model source les. Initial skeletons (which will compile successfully) are created in model/new-module.cc and model/new-module.h. If your module will have helper source les, then they will go into the helper/ directory; again, initial skeletons are created in that directory. Finally, it is good practice to write tests. A skeleton test suite and test case is created in the test/ directory. The below constructor species that it will be a unit test named new-module:
New-moduleTestSuite::New-moduleTestSuite () : TestSuite ("new-module", UNIT) {
100
le by modifying it with your text editor. As an example, the source les for the spectrum module are specied in
src/spectrum/wscript
le by modifying it with your text editor. As an example, the header les for the spectrum module are specied in
src/spectrum/wscript
with the following function call, module name, and list of header les. Note that the argument for the function new_task_gen() tells waf to install this modules headers with the other ns-3 headers:
headers = bld.new_task_gen(features=[ns3header]) headers.module = spectrum headers.source = [ model/spectrum-model.h, model/spectrum-value.h, . . . model/microwave-oven-spectrum-value-helper.h, helper/spectrum-helper.h,
101
le by modifying it with your text editor. As an example, the tests for the spectrum module are specied in
src/spectrum/wscript
le by modifying it with your text editor. As an example, the examples for the core module are specied in
src/core/examples/wscript
The core modules C++ examples are specied using the following function calls and source le names. Note that the second argument for the function create_ns3_program() is the list of modules that the program being created depends on:
obj = bld.create_ns3_program(main-callback, [core]) obj.source = main-callback.cc obj = bld.create_ns3_program(sample-simulator, [core]) obj.source = sample-simulator.cc
The core modules Python examples are specied using the following function call. Note that the second argument for the function register_ns3_script() is the list of modules that the Python example depends on:
bld.register_ns3_script(sample-simulator.py, [core])
102
14.2.7 Step 7 - Specify which of your modules examples should be run as tests
The test framework can also be instrumented to run example programs to try to catch regressions in the examples. However, not all examples are suitable for regression tests. A le called examples-to-run.py that exists in each modules test directory can control the invocation of the examples when the test framework runs. As an example, the examples that are run by test.py for the core module are specied in
src/core/test/examples-to-run.py
where example_name is the executable to be run, do_run is a condition under which to run the example, and do_valgrind_run is a condition under which to run the example under valgrind. This is needed because NSC causes illegal instruction crashes with some tests when they are run under valgrind. Note that the two conditions are Python statements that can depend on waf conguration variables. For example,
("tcp-nsc-lfn", "NSC_ENABLED == True", "NSC_ENABLED == False"),
where example_name is the Python script to be run and do_run is a condition under which to run the example. Note that the condition is a Python statement that can depend on waf conguration variables. For example,
("realtime-udp-echo.py", "ENABLE_REAL_TIME == False"),
If your new module has examples, then you must specify which of them should be run in your
103
src/new-module/test/examples-to-run.py
le by modifying it with your text editor. These examples are run by test.py.
and look for your new modules test suite (and example programs, if enabled) in the test output.
If the module has a test library and test libraries are being built, then
libns3-modulename-test.so
will be built, too. Other modules that the module depends on and their test libraries will also be built. By default, all modules are built in ns-3. There are two ways to enable a subset of these modules: 1. Using wafs enable-modules option 2. Using the ns-3 conguration le Enable modules using wafs enable-modules option To enable only the core module with example and tests, for example, try these commands:
./waf clean ./waf configure --enable-examples --enable-tests --enable-modules=core ./waf build cd build/debug/ ls
104
Note the ./waf clean step is done here only to make it more obvious which module libraries were built. You dont have to do ./waf clean in order to enable subsets of modules. Running test.py will cause only those tests that depend on module core to be run:
24 of 24 tests passed (24 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
Repeat the above steps for the network module instead of the core module, and the following will be built, since network depends on core:
bindings examples libns3-core.so libns3-core-test.so libns3-network.so libns3-network-test.so ns3 samples scratch src utils
Running test.py will cause those tests that depend on only the core and network modules to be run:
31 of 31 tests passed (31 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
Enable modules using the ns-3 conguration le A conguration le, .ns3rc, has been added to ns-3 that allows users to specify which modules are to be included in the build. When enabling a subset of ns-3 modules, the precedence rules are as follows: 1. the enable-modules congure string overrides any .ns3rc le 2. the .ns3rc le in the top level ns-3 directory is next consulted, if present 3. the system searches for ~/.ns3rc if the above two are unspecied If none of the above limits the modules to be built, all modules that waf knows about will be built. The maintained version of the .ns3rc le in the ns-3 source code repository resides in the utils directory. The reason for this is if it were in the top-level directory of the repository, it would be prone to accidental checkins from maintainers that enable the modules they want to use. Therefore, users need to manually copy the .ns3rc from the utils directory to their preferred place (top level directory or their home directory) to enable persistent modular build conguration. Assuming that you are in the top level ns-3 directory, you can get a copy of the .ns3rc le that is in the utils directory as follows:
cp utils/.ns3rc .
The .ns3rc le should now be in your top level ns-3 directory, and it contains the following:
#! /usr/bin/env python # A list of the modules that will be enabled when ns-3 is run. # Modules that depend on the listed modules will be enabled also. # # All modules can be enabled by choosing all_modules. modules_enabled = [all_modules] # Set this equal to true if you want examples to be run. examples_enabled = False # Set this equal to true if you want tests to be run. tests_enabled = False
Use your favorite editor to modify the .ns3rc le to only enable the core module with examples and tests like this:
105
#! /usr/bin/env python # A list of the modules that will be enabled when ns-3 is run. # Modules that depend on the listed modules will be enabled also. # # All modules can be enabled by choosing all_modules. modules_enabled = [core] # Set this equal to true if you want examples to be run. examples_enabled = True # Set this equal to true if you want tests to be run. tests_enabled = True
Only the core module will be enabled now if you try these commands:
./waf clean ./waf configure ./waf build cd build/debug/ ls
Note the ./waf clean step is done here only to make it more obvious which module libraries were built. You dont have to do ./waf clean in order to enable subsets of modules. Running test.py will cause only those tests that depend on module core to be run:
24 of 24 tests passed (24 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
Repeat the above steps for the network module instead of the core module, and the following will be built, since network depends on core:
bindings examples libns3-core.so libns3-core-test.so libns3-network.so libns3-network-test.so ns3 samples scratch src utils
Running test.py will cause those tests that depend on only the core and network modules to be run:
31 of 31 tests passed (31 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
106
2. Using waf once ns-3 has been built 3. Using the ns-3 conguration le once ns-3 has been built Enable/disable examples and tests using build.py You can use build.py to enable/disable examples and tests when ns-3 is built for the rst time. By default, examples and tests are not built in ns-3. From the ns-3-allinone directory, you can build ns-3 without any examples or tests simply by doing:
./build.py
Running test.py in the top level ns-3 directory now will cause no examples or tests to be run:
0 of 0 tests passed (0 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
If you would like build ns-3 with examples and tests, then do the following from the ns-3-allinone directory:
./build.py --enable-examples --enable-tests
Running test.py in the top level ns-3 directory will cause all of the examples and tests to be run:
170 of 170 tests passed (170 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
Enable/disable examples and tests using waf You can use waf to enable/disable examples and tests once ns-3 has been built. By default, examples and tests are not built in ns-3. From the top level ns-3 directory, you can build ns-3 without any examples or tests simply by doing:
./waf configure ./waf build
If you would like build ns-3 with examples and tests, then do the following from the top level ns-3 directory:
./waf configure --enable-examples --enable-tests ./waf build
Running test.py will cause all of the examples and tests to be run:
170 of 170 tests passed (170 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
Enable/disable examples and tests using the ns-3 conguration le A conguration le, .ns3rc, has been added to ns-3 that allows users to specify whether examples and tests should be built or not. You can use this le to enable/disable examples and tests once ns-3 has been built. When enabling disabling examples and tests, the precedence rules are as follows: 1. the enable-examples/disable-examples congure strings override any .ns3rc le 2. the enable-tests/disable-tests congure strings override any .ns3rc le 14.4. Enabling/disabling ns-3 Tests and Examples 107
3. the .ns3rc le in the top level ns-3 directory is next consulted, if present 4. the system searches for ~/.ns3rc if the .ns3rc le was not found in the previous step If none of the above exists, then examples and tests will not be built. The maintained version of the .ns3rc le in the ns-3 source code repository resides in the utils directory. The reason for this is if it were in the top-level directory of the repository, it would be prone to accidental checkins from maintainers that enable the modules they want to use. Therefore, users need to manually copy the .ns3rc from the utils directory to their preferred place (top level directory or their home directory) to enable persistent enabling of examples and tests. Assuming that you are in the top level ns-3 directory, you can get a copy of the .ns3rc le that is in the utils directory as follows:
cp utils/.ns3rc .
The .ns3rc le should now be in your top level ns-3 directory, and it contains the following:
#! /usr/bin/env python # A list of the modules that will be enabled when ns-3 is run. # Modules that depend on the listed modules will be enabled also. # # All modules can be enabled by choosing all_modules. modules_enabled = [all_modules] # Set this equal to true if you want examples to be run. examples_enabled = False # Set this equal to true if you want tests to be run. tests_enabled = False
From the top level ns-3 directory, you can build ns-3 without any examples or tests simply by doing:
./waf configure ./waf build
If you would like build ns-3 with examples and tests, use your favorite editor to change the values in the .ns3rc le for examples_enabled and tests_enabled le to be True:
#! /usr/bin/env python # A list of the modules that will be enabled when ns-3 is run. # Modules that depend on the listed modules will be enabled also. # # All modules can be enabled by choosing all_modules. modules_enabled = [all_modules] # Set this equal to true if you want examples to be run. examples_enabled = True # Set this equal to true if you want tests to be run. tests_enabled = True
From the top level ns-3 directory, you can build ns-3 with examples and tests simply by doing:
108
Running test.py will cause all of the examples and tests to be run:
170 of 170 tests passed (170 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
14.5 Troubleshooting
This chapter posts some information about possibly common errors in building or running ns-3 programs. Please note that the wiki (http://www.nsnam.org/wiki/index.php/Troubleshooting) may have contributed items.
The error message says that the program terminated unsuccessfully, but it is not clear from this information what might be wrong. To examine more closely, try running it under the gdb debugger::
ns-old:~/ns-3-nsc$ ./waf --run tcp-point-to-point --command-template="gdb %s" Entering directory /home/tomh/ns-3-nsc/build Compilation finished successfully GNU gdb Red Hat Linux (6.3.0.0-1.134.fc5rh) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db library "/lib/libthread_db.so.1". (gdb) run Starting program: /home/tomh/ns-3-nsc/build/debug/examples/tcp-point-to-point Reading symbols from shared object read from target memory...done. Loaded system supplied DSO at 0xf5c000 Program received signal SIGSEGV, Segmentation fault. 0x0804aa12 in main (argc=1, argv=0xbfdfefa4) at ../examples/tcp-point-to-point.cc:136 136 Ptr<Socket> localSocket = socketFactory->CreateSocket (); (gdb) p localSocket $1 = {m_ptr = 0x3c5d65} (gdb) p socketFactory $2 = {m_ptr = 0x0}
14.5. Troubleshooting
109
Exit anyway? (y or n) y
Note rst the way the program was invoked pass the command to run as an argument to the command template gdb %s. This tells us that there was an attempt to dereference a null pointer socketFactory. Lets look around line 136 of tcp-point-to-point, as gdb suggests::
Ptr<SocketFactory> socketFactory = n2->GetObject<SocketFactory> (Tcp::iid); Ptr<Socket> localSocket = socketFactory->CreateSocket (); localSocket->Bind ();
The culprit here is that the return value of GetObject is not being checked and may be null. Sometimes you may need to use the valgrind memory checker for more subtle errors. Again, you invoke the use of valgrind similarly::
ns-old:~/ns-3-nsc$ ./waf --run tcp-point-to-point --command-template="valgrind %s"
110