We conclude in this article the series of posts about Instrumenting Nanos++. In the first article we discussed about the different components which take part on the instrumentation process, the different types of events which can be generated by the runtime and also, how the instrumentation output can be adapted to different formats (using plugins). In the second one we discussed about internal implementation details and how programmers can use instrumentation services in order to generate events. In this one we will talk about Instrumentation modules which can help programmers when instrumenting the code and we will show some practical examples using these modules.
Instrumentation Modules
Instrumentation modules help programmers in the instrumentation process by doing automatically some of the duties that users need to follow for correct instrumentation. So far its main utility is to take care about multiple exits in a given piece of code. As a module is a C++ object, we can use the constructor to open an instrumentation burst leaving the responsibility of closing it to the corresponding destructor. The simplest Instrumentation module is InstrumentState:
class InstrumentState {
private:
Instrumentation &_inst;
bool _closed;
public:
InstrumentState ( nanos_event_state_value_t state )
: _inst(*sys.getInstrumentor()), _closed(false)
{
_inst.raiseOpenStateEvent( state );
}
~InstrumentState ( ) { if (!_closed) close(); }
void close() { _closed=true; _inst.raiseCloseStateEvent(); }
};
Creating a new InstrumentState object will produce the opening of a State event (the value is specified in the object constructor). Once the object goes out of the scope where is declared the destructor will close it (if programmer has not closed it before). As most of instrumentation phases affect a whole function the programmer has just to create an object of a Instrumentation module at the beginning of the function.
Example: Instrumenting the API
API functions have – generally – a common behaviour. They open a Burst event with a pair <key,value>. The key is the internal code “api” and the value is an specific identifier of the function we are instrumenting on. API functions also open a State event with a value according with the function duty. Both events will be closed once the function execution finishes. Here it is an example using nanos_yield() implementation:
nanos_err_t nanos_yield ( void )
{
NANOS_INSTRUMENT( InstrumentStateAndBurst inst("api","yield",SCHEDULING) );
try {
Scheduler::yield();
} catch ( ... ) {
return NANOS_UNKNOWN_ERR;
}
return NANOS_OK;
}
Yield function will wrap its execution between <“api”,”yield”> Burst and SCHEDULING State events. Although the function can have other exit points (apart from the return) InstrumentStateAndBurst destructor will throw closing events automatically.
Example: Instrumenting Runtime Internal Functions
Different Nanos++ functions have different instrumentation approaches. In this sections we have chosen a scheduling related function: Scheduler::waitOnCondition(). Due space limitations we have abridged the code focusing our interest in the instrumentation parts.
void Scheduler::waitOnCondition (GenericSyncCond *condition)
{
NANOS_INSTRUMENT( InstrumentState inst(SYNCHRONIZATION) );
const int nspins = sys.getSchedulerConf().getNumSpins();
int spins = nspins;
WD * current = myThread->getCurrentWD();
while ( !condition->check() ) {
BaseThread *thread = getMyThreadSafe();
spins--;
if ( spins == 0 ) {
condition->lock();
if ( !( condition->check() ) ) {
condition->addWaiter( current );
NANOS_INSTRUMENT( InstrumentState inst1(SCHEDULING) );
WD *next = _schedulePolicy.atBlock( thread, current );
NANOS_INSTRUMENT( inst1.close() );
if ( next ) {
NANOS_INSTRUMENT( InstrumentState inst2(RUNTIME) );
switchTo ( next );
}
else {
condition->unlock();
NANOS_INSTRUMENT( InstrumentState inst3(YIELD) );
thread->yield();
}
} else {
condition->unlock();
}
spins = nspins;
}
}
}
In this function the instrumentation changes the thread state in several parts of the code. First, all the function code is surrounded by a SYNCHRONIZATION state (inst). A Opening state event is raised at the very beginning of the function and the corresponding close event will be thrown once the execution flow gets out from the function scope. During the function execution the thread state may change to SCHEDULING when calling _schedulePolicy.atBlock(), RUNTIME when we are context switching WorkDescriptors and YIELD when we are forcing a thread yield. In this case the SCHEDULING state change is the only one we have to force to close before getting out from its scope. Note, that if an C++ exception is raised by any of the lower layers the states that are open at point will close automatically. So, the use of the Instrumentation modules improves the general exception safety of the code.