Context Collector

Filling the context of a measurement by hand can be cumbersome. Especially, when you want to collect context from different locations in your simulator. This is where the ContextCollector can help you. We will follow the example of the previous section and extend it to use the ContextCollector. You will see that the code for filling the context can be organized in a way that makes the particular structure of a context reusable.

Concepts

Context Provider
A Context Provider can provide context for measurement when asked.
Context Provider Collection
The name says it all. A collection of Context Providers.
Context Collector
Collects context. Additionally it wraps a ProbeBus and takes care of fetching the context and timestamp for the call to forwardMeasurement. You only need to provide the measurement value.

Simple use of the Context Collector

First of all we need to include the ContextCollector’s header file.

#include <WNS/probe/bus/ProbeBus.hpp>
#include <WNS/probe/bus/ProbeBusRegistry.hpp>
#include <WNS/probe/bus/ContextCollector.hpp>

Ok. This was not so hard. We will leave the Job class unchanged, but derive it from wns::RefCountable. This is needed because we will use wns::SmartPointer to do the memory management for all Job instances.

class Job:
    virtual public wns::RefCountable
{
public:
    enum Priority
    {
        control = 0,
        realtime,
        multimedia,
        bestEffort
    } priority;

    Job();

    Job(const Job&);

    Priority priority_;

    wns::simulator::Time startedAt_;
};

We add a ContextCollector as private member to the Processor class. The ContextCollector simplifies collecting measurements. It takes care of filling the context and also take care to pass the current simulation time to the underlying ProbeBus.

class Processor
{
public:

    Processor();

    void
    startJob(const wns::SmartPtr<Job>& job);

    void
    onJobEnded(const wns::SmartPtr<Job>& job);

    int
    getID();

private:
    int
    generateID();

    boost::shared_ptr<wns::distribution::Distribution> dis_;

    wns::probe::bus::ContextCollector processingDelayCC_;

    int id_;
};

The constructor of a the ContextCollector takes the name of the measurement source as a parameter. There is a more complex constructor that is described in the second part of this chapter.

Processor::Processor():
    dis_(new wns::distribution::Uniform(0.0, 1.0)),
    id_(generateID()),
    processingDelayCC_("processor.processingDelay")
{
}

If you want to publish a measurement you simply use the put method of the ContextCollector. The first argument is the measurement value. If you want to provide some additional context you can pass a boost::tuple (see boosttuple) as second parameter. Here we use boost::make_tuple to construct a tuple. The entries with odd indices (starting at 1) of the tuple are the keys and must always be of type std::string the even ones are the values and can either be of type int or std::string.

void
Processor::onJobEnded(const wns::SmartPtr<Job>& job)
{
    // Create the measurementValue
    double measurementValue =
        wns::simulator::getEventScheduler()->getTime() - job->startedAt_;

    // Pass it all to the evaluation framework
    processingDelayCC_.put(measurementValue,
                           boost::make_tuple("priority", job->priority_,
                                             "processorID", this->id_));
}

If you compare this to the Processor::onJobEnded method of the last example, you can see that you basically need two lines of code where you previously needed around 10.

Using and Implementing Context Providers

If you want to add context from other locations in your simulator without introducing thight coupling between the entity that provides the context and the entity that provides the measurement you should make use of ContextProviders. A ContextProvider encapsulates the provisioning of context entries in a separate class. This section shows you how to implement a ContextProvider and add it to a ContextProviderCollection which defines the Context of a ContextCollector. So lets get started. Again, we first need to include the ContextCollector’s header file.

#include <WNS/probe/bus/ProbeBus.hpp>
#include <WNS/probe/bus/ProbeBusRegistry.hpp>
#include <WNS/probe/bus/ContextCollector.hpp>

Ok. This was not so hard. We will leave the Job class unchanged, but derive it from wns::osi::PDU instead of wns::RefCountable?. openWNS is a system level simulation tool that focusses on protocol behaviour. The ContextCollector expects jobs to be PDUs. So it this is a reasonable approach.

class Job:
        public wns::osi::PDU
{
public:
    enum Priority
    {
        control = 0,
        realtime,
        multimedia,
        bestEffort
    } priority;

    Job();

    Job(const Job&);

    Priority priority_;

    wns::simulator::Time startedAt_;
};

We will make some extensions to the Processor class. First of all the signatures of startJob() and onJobEnded() now take a wns::SmartPtr<Job> as argument. This makes it more realistic, because normally a Job traverses many different classes in your simulator. None of these classes knows when to delete the Job, therefore memory management is handled by a SmartPtr.

class Processor
{
public:

    Processor();

    void
    startJob(const wns::SmartPtr<Job>& job);

    void
    onJobEnded(const wns::SmartPtr<Job>& job);

    int
    getID();

private:
    int
    generateID();

    wns::probe::bus::ContextProviderCollection
    getContextProviderCollection();

    boost::shared_ptr<wns::distribution::Distribution> dis_;

    wns::probe::bus::ContextCollectorPtr processingDelayCC_;

    int id_;
};

You can see that there is a public method getID() and a private method generateID() as well as a private member id_. We assume that there will be many processor in a simulation run which will be true for any non-trivial simulation. Each processor has a unique processorID which you can get by using getID(). During construction of a processor a new unique processorID is automatically generated by generateID(). Here is the code for it:

int
Processor::generateID()
{
    static int lastID = 0;

    lastID++;

    return lastID;
}

What we will do now is to create a context provider collection during startup of each processor. Then a context collector will be initialized to use this collection. The collection will contain two context providers. One that can read the priority from the Job that is evaluated, the other will callback the processor to provide its own processorID. Both context entries will then be passed to the evaluation framework.

Let’s start by implementing the PriorityProvider that reads the priority of a job. Here is the class declaration:

class PriorityProvider:
        public wns::probe::bus::PDUContextProvider<Job>
{
public:
    PriorityProvider(std::string key);

private:
    virtual void
    doVisit(wns::probe::bus::IContext& c, const wns::SmartPtr<Job>& job) const;

    std::string key_;
};

We derive PriorityProvider from a template class wns::probe::bus::PDUContextProvider. The template parameter is the Job class. This will take care of some basic conversion for us. On construction we pass a key to the provider. This will be used when making entries in a context. The PDUContextProvider is an abstract class that forces us to implement the doVisit method. This method is called whenever a ContextCollector needs to construct context for a measurement value. It is rather simple:

void
PriorityProvider::doVisit(wns::probe::bus::IContext& c,
                          const wns::SmartPtr<Job>& job) const
{
    c.insertInt(key_, job->priority_);
}

The Visitor Pattern is used here. The context is passed to each context provider as reference, so it can be written by each of them. The current job is also passed along, but unlike the context it is passed in read-only mode. The only thing we do here is to insert the job’s priority into the context and give it the name key_, which is set upon construction.

To provide the processorID we will use a generic purpose provider that can be used whenever a simple property of some object is to be read. So basically now we have the context providers ready and can start defining the context collector and its context provider collection. Let’s have a look at the constructor of Processor.

Processor::Processor():
    dis_(new wns::distribution::Uniform(0.0, 1.0)),
    id_(generateID())
{
    processingDelayCC_ = wns::probe::bus::ContextCollectorPtr(
        new wns::probe::bus::ContextCollector(
            this->getContextProviderCollection(),
            "processor.processingDelay"));
}

Here, the processingDelayCC_ is constructed. This is the context collector for our processing delay. Focus on the inner construction of the ContextCollector. To ease the memory management we wrap that object by a smart pointer ContextCollectorPtr. There is no magic about that.

The constructor of a ContextCollector` takes a context provider collection as its first argument, and the name of its measurement source a second argument. The context provider collection is constructed by getContextProviderCollection(). Which is defined as:

wns::probe::bus::ContextProviderCollection
Processor::getContextProviderCollection()
{
    wns::probe::bus::ContextProviderCollection cpc;

    PriorityProvider jobPriorityProvider("priority");

    wns::probe::bus::contextprovider::Callback
        processorIDProvider("processorID",
                            boost::bind(&Processor::getID,this));

    cpc.addProvider(jobPriorityProvider);
    cpc.addProvider(processorIDProvider);

    return cpc;
}

First of all we construct an empty ContextProviderCollection named cpc. We construct one of our PriorityProvider``s and then use the general purpose ``Callback context provider. This provider takes the name of the measurement source as first argument. The second argument is any callable that returns an integer an takes zero arguments. We use boost::bind to tell Callback that whenever it needs context information it should call the getID method of this instance. Then we add both providers to the context provider collection and return it to the caller. This finishes the setup of the context provider collection and the context collector. How can it be used? Here is the the new code of onJobEnded().

void
Processor::onJobEnded(const wns::SmartPtr<Job>& job)
{
    // Create the measurementValue
    double measurementValue =
        wns::simulator::getEventScheduler()->getTime() - job->startedAt_;

    // Pass it all to the evaluation framework
    processingDelayCC_->put(job, measurementValue);
}

As you can see it became much shorter. We only calculate the processing time and the call put. The first argument is the job and the second the actual measurement. The ContextCollector takes care that the job is passed to the PriorityProvider and that the current processorID is retrieved. We have sucessfully captured all the code to create context in the constructor of Processor. The context provider collection can be copied, passed around and reused and even hierarchically organized. This will be demonstrated in the next sections.

Note

todo: Describe the usage of the ContextCollector within the Node/Component concept.