Task Groups Overview

Use task groups to group entities together so that they can be analyzed as a single unit.

Task group instances apply to all threads in a process. Every task group instance must have an ID. Task groups are useful for logical groups of work in your application that do not make sense to nest within a regular task.

The unique property of task groups that distinguish them from other entities is that they themselves do not represent execution time within the application. Consider, for example, a “rendering pass” in graphics – logically, this consists of the work for a number of draw calls, yet nowhere in the execution of the system will you see a single “rendering pass” task. In this context, the rendering pass is a task group.

By definition, task groups have no duration. However, using the relations API, you can assign tasks to a task group and thus enable the Intel® GPA Platform Analyzer to aggregate the work associated with the task group. For example, to find out how much work frame 7 does, you should instrument the code in the following way:

  1. Create a task group for each frame.
  2. For every task that contributes time to the overall frame’s work, relate it back to the frame task group using the relations.

Recall that we use task groups to measure abstract concepts like “Render Pass Duration.” Consider the following simplified code for a graphics renderer – the draw call creates the vertex task, then the vertex task creates the pixel shading task.

 

 void DrawIndexedPrimitive(DrawCall* draw) {

    EnqueueTask(DoVertexShadingWork, draw);

 }

 void DoVertexShadingWork(DrawCall* draw) {

    // shade vertices…

    EnqueueTask(DoPixelShadingWork, draw);

 }

 void DoPixelShadingWork(DrawCall* d) {

    // here is a task where we do pixel shading work…

 }

 

To obtain the render pass costs, you first have to “hook” the concept of a render pass within our application. A low-overhead, simple place to insert this would be in the original draw call with a static variable and a quick check. Recall that relations API enables you to relate tasks across disparate threads. To measure RenderPass duration, you should create a task group that represents the RenderPass itself. Then, we will add each of the individual tasks (drawing function, vertex shading, and pixel shading) to that task group.

 

 // Create a domain and required string handles

 __itt_domain* domain = __itt_domain_create(L”MyTraces.MyDomain”);

 __itt_string_handle* strRenderPass = __itt_string_handle_create(“RenderPass”);

 

 // Choose a number such that rendertargets have their own namespace

 #define RENDERPASS_NS 3

 __itt_string_handle* strDrawIndexedPrimitive = __itt_string_handle_create(“DrawIndexedPrimitive”);

 void DrawIndexedPrimitive(DrawCall* draw)

 {

    static RenderPass* sLastRP = NULL;

    __itt_id rtID = __itt_id_make(sLastRP, RENDERPASS_NS);

    if(draw->RenderState->RenderPass != sLastRP)

    {

       sLastRP = renderState->RenderPass;

       // create the ID. This must happen before we use the ID.

       __itt_id_create(domain, rtID);

       // Create the task group.

       __itt_task_group(domain, rtID, 0, strRenderPass);

       // Task groups can have metadata, just like tasks

       int params[2] = { sLastRP->Width, sLastRP->Height };

       __itt_metadata_add(domain, rtID, NULL, __itt_metadata_s32, 2, (void*)params);

    }

    // Now, create a task for the drawing thread’s work.

    // Notice that we create this AFTER the task group.

    // This is *mandatory* because we are about to add a reference to the task group.

    __itt_task_begin(domain, __itt_null, __itt_null, strDrawIndexedPrimitive);

    // Make this task a child of the RenderPass task

    __itt_relation_add_to_current(domain, __itt_relation_is_child_of, rtID);

    // Our actual work

    EnqueueTask(DoVertexShadingWork, draw);

    // End the draw call task, we’re done there. We can still add work to the RenderPass, however…

    __itt_task_end(domain);

 }

 __itt_string_handle* strDoVertexShadingWork = __itt_string_handle_create(“DoVertexShadingWork”);

 void DoVertexShadingWork(DrawCall* draw)

 {

    __itt_task_begin(domain, __itt_null, __itt_null, strDoVertexShadingWork);

    // Add this task to the RenderPass as well.

    // We don’t need to *create* the ID since it is used by the task group, however we can *make*

    // the ID value that gives us the same ID value as that of the task group.

    __itt_id rtID = __itt_id_make(sLastRP, RENDERPASS_NS);

    __itt_relation_add_to_current(domain, __itt_relation_is_child_of, rtID);

    // … shade vertices…

    EnqueueTask(DoPixelShadingWork, draw);

    __itt_task_end(domain);

 }

 __itt_string_handle* strDoPixelShadingWork = __itt_string_handle_create(“DoPixelShadingWork”);

 void DoPixelShadingWork(DrawCall* d)

 {

    __itt_task_begin(domain, __itt_null, __itt_null, strDoPixelShadingWork);

    // add this task to the RenderPass as well

    __itt_id rtID = __itt_id_make(sLastRP, RENDERPASS_NS);

    __itt_relation_add_to_current(domain, __itt_relation_is_child_of, rtID);

    __itt_task_end(domain);

 }

 

For correctness, every time you call __itt_id_create(), you need to later call __itt_id_destroy(). The rule is that until you destroy the ID, you cannot assign it to any other entity instance. The code above does not call __itt_id_destroy() because there is no clean place to put it. You can’t put it in the DrawIndexedPrimitive task; when the RenderTarget task changes on that thread, there still may be outstanding pixel shading work.

The best way to fix this is to find when the RenderPass task is truly completed. Sometimes you will find this information in your overall task scheduler, or sometimes it will be more easily accessible. The best solution is when your ID is derived from a specific allocation for the task – when this happens, as is the case with this RenderPass example, we can just move the ID calls to the RenderPass constructor/destructor:

 

 class RenderPass {

 public:

    RenderPass() {

       __itt_id_create(pD, __itt_id_make(this, RENDERPASS_NS));

    }

    ~RenderPass() {

       __itt_id_destroy(pD, __itt_id_make(this, RENDERPASS_NS));

    }

 }

 //Task groups can nest.

 __itt_id_create(domain, id1);

 __itt_id_create(domain, id2);

 …

 // A parent of __itt_null means that this task group is a top-level task group.

 __itt_task_group(domain, id1, __itt_null, NULL);

 // We specify id1 as the parent ID for this task group, making this task group a member of the previous task group.

 __itt_task_group(domain, id2, id1, NULL);

 …

 __itt_id_destroy(id1);

 __itt_id_destroy(id2);

See Also

__itt_task_group
__itt_id_create
__itt_id_destroy
__itt_id_destroy

Task Groups Overview