Index: ps/trunk/source/ps/Profiler2.cpp =================================================================== --- ps/trunk/source/ps/Profiler2.cpp (nonexistent) +++ ps/trunk/source/ps/Profiler2.cpp (revision 10465) @@ -0,0 +1,446 @@ +/* Copyright (C) 2011 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "Profiler2.h" + +#include "lib/allocators/shared_ptr.h" +#include "ps/CLogger.h" +#include "third_party/mongoose/mongoose.h" + +CProfiler2 g_Profiler2; + +// A human-recognisable pattern (for debugging) followed by random bytes (for uniqueness) +const u8 CProfiler2::RESYNC_MAGIC[8] = {0x11, 0x22, 0x33, 0x44, 0xf4, 0x93, 0xbe, 0x15}; + +CProfiler2::CProfiler2() : + m_Initialised(false), m_MgContext(NULL) +{ +} + +/** + * Mongoose callback. Run in an arbitrary thread (possibly concurrently with other requests). + */ +static void* MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info) +{ + CProfiler2* profiler = (CProfiler2*)request_info->user_data; + ENSURE(profiler); + + void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling + + const char* header200 = + "HTTP/1.1 200 OK\r\n" + "Access-Control-Allow-Origin: *\r\n" // TODO: not great for security + "Content-Type: text/plain; charset=utf-8\r\n\r\n"; + + const char* header404 = + "HTTP/1.1 404 Not Found\r\n" + "Content-Type: text/plain; charset=utf-8\r\n\r\n" + "Unrecognised URI"; + + const char* header400 = + "HTTP/1.1 400 Bad Request\r\n" + "Content-Type: text/plain; charset=utf-8\r\n\r\n" + "Invalid request"; + + switch (event) + { + case MG_NEW_REQUEST: + { + std::stringstream stream; + + std::string uri = request_info->uri; + if (uri == "/overview") + { + profiler->ConstructJSONOverview(stream); + } + else if (uri == "/query") + { + if (!request_info->query_string) + { + mg_printf(conn, "%s (no query string)", header400); + return handled; + } + + // Identify the requested thread + char buf[256]; + int len = mg_get_var(request_info->query_string, strlen(request_info->query_string), "thread", buf, ARRAY_SIZE(buf)); + if (len < 0) + { + mg_printf(conn, "%s (no 'thread')", header400); + return handled; + } + std::string thread(buf); + + const char* err = profiler->ConstructJSONResponse(stream, thread); + if (err) + { + mg_printf(conn, "%s (%s)", header400, err); + return handled; + } + } + else + { + mg_printf(conn, header404); + return handled; + } + + mg_printf(conn, "%s", header200); + std::string str = stream.str(); + mg_write(conn, str.c_str(), str.length()); + return handled; + } + + case MG_HTTP_ERROR: + return NULL; + + case MG_EVENT_LOG: + // Called by Mongoose's cry() + LOGERROR(L"Mongoose error: %hs", request_info->log_message); + return NULL; + + case MG_INIT_SSL: + return NULL; + + default: + debug_warn(L"Invalid Mongoose event type"); + return NULL; + } +}; + +void CProfiler2::Initialise() +{ + ENSURE(!m_Initialised); + int err = pthread_key_create(&m_TLS, &CProfiler2::TLSDtor); + ENSURE(err == 0); + m_Initialised = true; +} + +void CProfiler2::EnableHTTP() +{ + ENSURE(m_Initialised); + + // Ignore multiple enablings + if (m_MgContext) + return; + + const char *options[] = { + "listening_ports", "127.0.0.1:8000", // bind to localhost for security + "num_threads", "6", // enough for the browser's parallel connection limit + NULL + }; + m_MgContext = mg_start(MgCallback, this, options); + ENSURE(m_MgContext); +} + +void CProfiler2::Shutdown() +{ + ENSURE(m_Initialised); + + if (m_MgContext) + { + mg_stop(m_MgContext); + m_MgContext = NULL; + } + + // TODO: free non-NULL keys, instead of leaking them + + int err = pthread_key_delete(m_TLS); + ENSURE(err == 0); + m_Initialised = false; +} + +/** + * Called by pthreads when a registered thread is destroyed. + */ +void CProfiler2::TLSDtor(void* data) +{ + ThreadStorage* storage = (ThreadStorage*)data; + + CProfiler2& profiler = storage->GetProfiler(); + + { + CScopeLock lock(profiler.m_Mutex); + profiler.m_Threads.erase(std::find(profiler.m_Threads.begin(), profiler.m_Threads.end(), storage)); + } + + delete (ThreadStorage*)data; +} + +void CProfiler2::RegisterCurrentThread(const std::string& name) +{ + ENSURE(m_Initialised); + + ENSURE(pthread_getspecific(m_TLS) == NULL); // mustn't register a thread more than once + + ThreadStorage* storage = new ThreadStorage(*this, name); + int err = pthread_setspecific(m_TLS, storage); + ENSURE(err == 0); + + RecordSyncMarker(); + RecordEvent("thread start"); + + CScopeLock lock(m_Mutex); + m_Threads.push_back(storage); +} + +CProfiler2::ThreadStorage::ThreadStorage(CProfiler2& profiler, const std::string& name) : + m_Profiler(profiler), m_Name(name), m_BufferPos0(0), m_BufferPos1(0), m_LastTime(timer_Time()) +{ + m_Buffer = new u8[BUFFER_SIZE]; + memset(m_Buffer, ITEM_NOP, BUFFER_SIZE); +} + +CProfiler2::ThreadStorage::~ThreadStorage() +{ + delete[] m_Buffer; +} + +std::string CProfiler2::ThreadStorage::GetBuffer() +{ + // Called from an arbitrary thread (not the one writing to the buffer). + // + // See comments on m_BufferPos0 etc. + + shared_ptr buffer(new u8[BUFFER_SIZE], ArrayDeleter()); + + u32 pos1 = m_BufferPos1; + COMPILER_FENCE; // must read m_BufferPos1 before m_Buffer + + memcpy(buffer.get(), m_Buffer, BUFFER_SIZE); + + COMPILER_FENCE; // must read m_BufferPos0 after m_Buffer + u32 pos0 = m_BufferPos0; + + // The range [pos1, pos0) modulo BUFFER_SIZE is invalid, so concatenate the rest of the buffer + + if (pos1 <= pos0) // invalid range is in the middle of the buffer + return std::string(buffer.get()+pos0, buffer.get()+BUFFER_SIZE) + std::string(buffer.get(), buffer.get()+pos1); + else // invalid wrap is wrapped around the end/start buffer + return std::string(buffer.get()+pos0, buffer.get()+pos1); +} + +void CProfiler2::ThreadStorage::RecordAttribute(const char* fmt, va_list argp) +{ + char buffer[MAX_ATTRIBUTE_LENGTH + 4] = {0}; // first 4 bytes are used for storing length + i32 len = vsprintf_s(buffer + 4, ARRAY_SIZE(buffer) - 4, fmt, argp); + if (len < 0) + { + debug_warn("Profiler attribute sprintf failed"); + return; + } + + // Store the length in the buffer + memcpy(buffer, &len, sizeof(len)); + + Write(ITEM_ATTRIBUTE, buffer, 4 + len); +} + + +void CProfiler2::ConstructJSONOverview(std::ostream& stream) +{ + CScopeLock lock(m_Mutex); + + stream << "{\"threads\":["; + for (size_t i = 0; i < m_Threads.size(); ++i) + { + if (i != 0) + stream << ","; + stream << "{\"name\":\"" << CStr(m_Threads[i]->GetName()).EscapeToPrintableASCII() << "\"}"; + } + stream << "]}"; +} + +/** + * Given a buffer and a visitor class (with functions OnEvent, OnEnter, OnLeave, OnAttribute), + * calls the visitor for every item in the buffer. + */ +template +void RunBufferVisitor(const std::string& buffer, V& visitor) +{ + // The buffer doesn't necessarily start at the beginning of an item + // (we just grabbed it from some arbitrary point in the middle), + // so scan forwards until we find a sync marker. + // (This is probably pretty inefficient.) + + u32 realStart = (u32)-1; // the start point decided by the scan algorithm + + for (u32 start = 0; start + 1 + sizeof(CProfiler2::RESYNC_MAGIC) <= buffer.length(); ++start) + { + if (buffer[start] == CProfiler2::ITEM_SYNC + && memcmp(buffer.c_str() + start + 1, &CProfiler2::RESYNC_MAGIC, sizeof(CProfiler2::RESYNC_MAGIC)) == 0) + { + realStart = start; + break; + } + } + + ENSURE(realStart != (u32)-1); // we should have found a sync point somewhere in the buffer + + u32 pos = realStart; // the position as we step through the buffer + + double lastTime = -1; + // set to non-negative by EVENT_SYNC; we ignore all items before that + // since we can't compute their absolute times + + while (pos < buffer.length()) + { + u8 type = buffer[pos]; + ++pos; + + switch (type) + { + case CProfiler2::ITEM_NOP: + { + // ignore + break; + } + case CProfiler2::ITEM_SYNC: + { + u8 magic[sizeof(CProfiler2::RESYNC_MAGIC)]; + double t; + memcpy(magic, buffer.c_str()+pos, ARRAY_SIZE(magic)); + ENSURE(memcmp(magic, &CProfiler2::RESYNC_MAGIC, sizeof(CProfiler2::RESYNC_MAGIC)) == 0); + pos += sizeof(CProfiler2::RESYNC_MAGIC); + memcpy(&t, buffer.c_str()+pos, sizeof(t)); + pos += sizeof(t); + lastTime = t; + break; + } + case CProfiler2::ITEM_EVENT: + { + CProfiler2::SItem_dt_id item; + memcpy(&item, buffer.c_str()+pos, sizeof(item)); + pos += sizeof(item); + if (lastTime >= 0) + { + lastTime = lastTime + (double)item.dt; + visitor.OnEvent(lastTime, item.id); + } + break; + } + case CProfiler2::ITEM_ENTER: + { + CProfiler2::SItem_dt_id item; + memcpy(&item, buffer.c_str()+pos, sizeof(item)); + pos += sizeof(item); + if (lastTime >= 0) + { + lastTime = lastTime + (double)item.dt; + visitor.OnEnter(lastTime, item.id); + } + break; + } + case CProfiler2::ITEM_LEAVE: + { + CProfiler2::SItem_dt_id item; + memcpy(&item, buffer.c_str()+pos, sizeof(item)); + pos += sizeof(item); + if (lastTime >= 0) + { + lastTime = lastTime + (double)item.dt; + visitor.OnLeave(lastTime, item.id); + } + break; + } + case CProfiler2::ITEM_ATTRIBUTE: + { + u32 len; + memcpy(&len, buffer.c_str()+pos, sizeof(len)); + ENSURE(len <= CProfiler2::MAX_ATTRIBUTE_LENGTH); + pos += sizeof(len); + std::string attribute(buffer.c_str()+pos, buffer.c_str()+pos+len); + pos += len; + if (lastTime >= 0) + { + visitor.OnAttribute(attribute); + } + break; + } + default: + debug_warn(L"Invalid profiler item when parsing buffer"); + return; + } + } +}; + +/** + * Visitor class that dumps events as JSON. + * TODO: this is pretty inefficient (in implementation and in output format). + */ +struct BufferVisitor_Dump +{ + NONCOPYABLE(BufferVisitor_Dump); +public: + BufferVisitor_Dump(std::ostream& stream) : m_Stream(stream) + { + } + + void OnEvent(double time, const char* id) + { + m_Stream << "[1," << std::fixed << std::setprecision(9) << time; + m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n"; + } + + void OnEnter(double time, const char* id) + { + m_Stream << "[2," << std::fixed << std::setprecision(9) << time; + m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n"; + } + + void OnLeave(double time, const char* id) + { + m_Stream << "[3," << std::fixed << std::setprecision(9) << time; + m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n"; + } + + void OnAttribute(const std::string& attr) + { + m_Stream << "[4,\"" << CStr(attr).EscapeToPrintableASCII() << "\"],\n"; + } + + std::ostream& m_Stream; +}; + +const char* CProfiler2::ConstructJSONResponse(std::ostream& stream, const std::string& thread) +{ + CScopeLock lock(m_Mutex); + + ThreadStorage* storage = NULL; + for (size_t i = 0; i < m_Threads.size(); ++i) + { + if (m_Threads[i]->GetName() == thread) + { + storage = m_Threads[i]; + break; + } + } + + if (!storage) + return "cannot find named thread"; + + stream << "{\"events\":[\n"; + + std::string buffer = storage->GetBuffer(); + BufferVisitor_Dump visitor(stream); + RunBufferVisitor(buffer, visitor); + + stream << "null\n]}"; + + return NULL; +} Index: ps/trunk/source/ps/Profiler2.h =================================================================== --- ps/trunk/source/ps/Profiler2.h (nonexistent) +++ ps/trunk/source/ps/Profiler2.h (revision 10465) @@ -0,0 +1,381 @@ +/* Copyright (C) 2011 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +/** + * @file New profiler (complementing the older CProfileManager) + * + * The profiler is designed for analysing framerate fluctuations or glitches, + * and temporal relationships between threads. + * This contrasts with CProfilerManager and most external profiling tools, + * which are designed more for measuring average throughput over a number of + * frames. + * + * To view the profiler output, press F11 to enable the HTTP output mode + * and then open source/tools/profiler2/profiler2.html in a web browser. + * + * There is a single global CProfiler2 instance (g_Profiler2), + * providing the API used by the rest of the game. + * The game can record the entry/exit timings of a region of code + * using the PROFILE2 macro, and can record other events using + * PROFILE2_EVENT. + * Regions and events can be annotated with arbitrary string attributes, + * specified with printf-style format strings, using PROFILE2_ATTR + * (e.g. PROFILE2_ATTR("frame: %d", m_FrameNum) ). + * + * This is designed for relatively coarse-grained profiling, or for rare events. + * Don't use it for regions that are typically less than ~0.1msecs, or that are + * called hundreds of times per frame. (The old CProfilerManager is better + * for that.) + * + * New threads must call g_Profiler2.RegisterCurrentThread before any other + * profiler functions. + * + * The main thread should call g_Profiler2.RecordFrameStart at the start of + * each frame. + * Other threads should call g_Profiler2.RecordSyncMarker occasionally, + * especially if it's been a long time since their last call to the profiler, + * or if they've made thousands of calls since the last sync marker. + * + * The profiler is implemented with thread-local fixed-size ring buffers, + * which store a sequence of variable-length items indicating the time + * of the event and associated data (pointers to names, attribute strings, etc). + * An HTTP server provides access to the data: when requested, it will make + * a copy of a thread's buffer, then parse the items and return them in JSON + * format. The profiler2.html requests and processes and visualises this data. + * + * The RecordSyncMarker calls are necessary to correct for time drift and to + * let the buffer parser accurately detect the start of an item in the byte stream. + * + * This design aims to minimise the performance overhead of recording data, + * and to simplify the visualisation of the data by doing it externally in an + * environment with better UI tools (i.e. HTML) instead of within the game engine. + * + * The initial setup of g_Profiler2 must happen in the game's main thread. + * RegisterCurrentThread and the Record functions may be called from any thread. + * The HTTP server runs its own threads, which may call the ConstructJSON functions. + */ + +#ifndef INCLUDED_PROFILER2 +#define INCLUDED_PROFILER2 + +#include "lib/timer.h" +#include "ps/ThreadUtil.h" + +struct mg_context; + +// Note: Lots of functions are defined inline, to hypothetically +// minimise performance overhead. + +class CProfiler2 +{ +public: + // Items stored in the buffers: + + /// Item type identifiers + enum EItem + { + ITEM_NOP = 0, + ITEM_SYNC = 1, // magic value used for parse syncing + ITEM_EVENT = 2, // single event + ITEM_ENTER = 3, // entering a region + ITEM_LEAVE = 4, // leaving a region (must be correctly nested) + ITEM_ATTRIBUTE = 5, // arbitrary string associated with current region, or latest event (if the previous item was an event) + }; + + static const size_t MAX_ATTRIBUTE_LENGTH = 256; // includes null terminator, which isn't stored + + /// An arbitrary number to help resyncing with the item stream when parsing. + static const u8 RESYNC_MAGIC[8]; + + /** + * An item with a relative time and an ID string pointer. + */ + struct SItem_dt_id + { + float dt; // time relative to last event + const char* id; + }; + +private: + // TODO: what's a good size? + // TODO: different threads might want different sizes + static const size_t BUFFER_SIZE = 128*1024; + + /** + * Class instantiated in every registered thread. + */ + class ThreadStorage + { + NONCOPYABLE(ThreadStorage); + public: + ThreadStorage(CProfiler2& profiler, const std::string& name); + ~ThreadStorage(); + + void RecordSyncMarker(double t) + { + // Store the magic string followed by the absolute time + // (to correct for drift caused by the precision of relative + // times stored in other items) + u8 buffer[sizeof(RESYNC_MAGIC) + sizeof(t)]; + memcpy(buffer, &RESYNC_MAGIC, sizeof(RESYNC_MAGIC)); + memcpy(buffer + sizeof(RESYNC_MAGIC), &t, sizeof(t)); + Write(ITEM_SYNC, buffer, ARRAY_SIZE(buffer)); + m_LastTime = t; + } + + void Record(EItem type, double t, const char* id) + { + // Store a relative time instead of absolute, so we can use floats + // (to save memory) without suffering from precision problems + SItem_dt_id item = { (float)(t - m_LastTime), id }; + Write(type, &item, sizeof(item)); + m_LastTime = t; + } + + void RecordFrameStart(double t) + { + RecordSyncMarker(t); + Record(ITEM_EVENT, t, "__framestart"); // magic string recognised by the visualiser + } + + void RecordAttribute(const char* fmt, va_list argp); + + CProfiler2& GetProfiler() + { + return m_Profiler; + } + + const std::string& GetName() + { + return m_Name; + } + + /** + * Returns a copy of a subset of the thread's buffer. + * Not guaranteed to start on an item boundary. + * May be called by any thread. + */ + std::string GetBuffer(); + + private: + /** + * Store an item into the buffer. + */ + void Write(EItem type, const void* item, u32 itemSize) + { + // See m_BufferPos0 etc for comments on synchronisation + + u32 size = 1 + itemSize; + u32 start = m_BufferPos0; + if (start + size > BUFFER_SIZE) + { + // The remainder of the buffer is too small - fill the rest + // with NOPs then start from offset 0, so we don't have to + // bother splitting the real item across the end of the buffer + + m_BufferPos0 = size; + COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer + + memset(m_Buffer + start, 0, BUFFER_SIZE - start); + start = 0; + } + else + { + m_BufferPos0 = start + size; + COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer + } + + m_Buffer[start] = (u8)type; + memcpy(&m_Buffer[start + 1], item, itemSize); + + COMPILER_FENCE; // must write m_BufferPos1 after m_Buffer + m_BufferPos1 = start + size; + } + + CProfiler2& m_Profiler; + std::string m_Name; + + double m_LastTime; // used for computing relative times + + u8* m_Buffer; + + // To allow hopefully-safe reading of the buffer from a separate thread, + // without any expensive synchronisation in the recording thread, + // two copies of the current buffer write position are stored. + // BufferPos0 is updated before writing; BufferPos1 is updated after writing. + // GetBuffer can read Pos1, copy the buffer, read Pos0, then assume any bytes + // outside the range Pos1 <= x < Pos0 are safe to use. (Any in that range might + // be half-written and corrupted.) (All ranges are modulo BUFFER_SIZE.) + // Outside of Write(), these will always be equal. + // + // TODO: does this attempt at synchronisation (plus use of COMPILER_FENCE etc) + // actually work in practice? + u32 m_BufferPos0; + u32 m_BufferPos1; + }; + +public: + CProfiler2(); + + /** + * Call in main thread to set up the profiler, + * before calling any other profiler functions. + */ + void Initialise(); + + /** + * Call in main thread to enable the HTTP server. + * (Disabled by default for security and performance + * and to avoid annoying a firewall.) + */ + void EnableHTTP(); + + /** + * Call in main thread to shut everything down. + * All other profiled threads should have been terminated already. + */ + void Shutdown(); + + /** + * Call in any thread to enable the profiler in that thread. + * @p name should be unique, and is used by the visualiser to identify + * this thread. + */ + void RegisterCurrentThread(const std::string& name); + + /** + * Non-main threads should call this occasionally, + * especially if it's been a long time since their last call to the profiler, + * or if they've made thousands of calls since the last sync marker. + */ + void RecordSyncMarker() + { + GetThreadStorage().RecordSyncMarker(GetTime()); + } + + /** + * Call in main thread at the start of a frame. + */ + void RecordFrameStart() + { + GetThreadStorage().RecordFrameStart(GetTime()); + } + + void RecordEvent(const char* id) + { + GetThreadStorage().Record(ITEM_EVENT, GetTime(), id); + } + + void RecordRegionEnter(const char* id) + { + GetThreadStorage().Record(ITEM_ENTER, GetTime(), id); + } + + void RecordRegionLeave(const char* id) + { + GetThreadStorage().Record(ITEM_LEAVE, GetTime(), id); + } + + void RecordAttribute(const char* fmt, ...) + { + va_list argp; + va_start(argp, fmt); + GetThreadStorage().RecordAttribute(fmt, argp); + va_end(argp); + } + + /** + * Call in any thread to produce a JSON representation of the general + * state of the application. + */ + void ConstructJSONOverview(std::ostream& stream); + + /** + * Call in any thread to produce a JSON representation of the buffer + * for a given thread. + * Returns NULL on success, or an error string. + */ + const char* ConstructJSONResponse(std::ostream& stream, const std::string& thread); + +private: + static void TLSDtor(void* data); + + ThreadStorage& GetThreadStorage() + { + ThreadStorage* storage = (ThreadStorage*)pthread_getspecific(m_TLS); + ASSERT(storage); + return *storage; + } + + double GetTime() + { + return timer_Time(); + } + + bool m_Initialised; + + mg_context* m_MgContext; + + pthread_key_t m_TLS; + + CMutex m_Mutex; + std::vector m_Threads; // thread-safe; protected by m_Mutex +}; + +extern CProfiler2 g_Profiler2; + +/** + * Scope-based enter/leave helper. + */ +class CProfile2Region +{ +public: + CProfile2Region(const char* name) : m_Name(name) + { + g_Profiler2.RecordRegionEnter(m_Name); + } + ~CProfile2Region() + { + g_Profiler2.RecordRegionLeave(m_Name); + } +private: + const char* m_Name; +}; + +/** + * Starts timing from now until the end of the current scope. + * @p region is the name to associate with this region (should be + * a constant string literal; the pointer must remain valid forever). + * Regions may be nested, but preferably shouldn't be nested deeply since + * it hurts the visualisation. + */ +#define PROFILE2(region) CProfile2Region profile2__(region) + +/** + * Record the named event at the current time. + */ +#define PROFILE2_EVENT(name) g_Profiler2.RecordEvent(name) + +/** + * Associates a string (with printf-style formatting) with the current + * region or event. + * (If the last profiler call was PROFILE2_EVENT, it associates with that + * event; otherwise it associates with the currently-active region.) + */ +#define PROFILE2_ATTR g_Profiler2.RecordAttribute + +#endif // INCLUDED_PROFILER2