Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

764 lines
28 KiB
C++

//=== OmptTracingBuffer.cpp - Target independent OpenMP target RTL -- C++ -===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Implementation of OMPT device trace record generation and flushing.
//
//===----------------------------------------------------------------------===//
#ifdef OMPT_SUPPORT
#include "OpenMP/OMPT/OmptTracingBuffer.h"
#include "OpenMP/OMPT/OmptTracing.h"
#include "Shared/Debug.h"
#include <atomic>
#include <cassert>
#include <cstdlib>
#include <limits>
using namespace llvm::omp::target::debug;
// When set to true, helper threads terminate their work
static bool DoneTracing{false};
// Unique buffer id in creation order
static std::atomic<uint64_t> BufId{0};
// Unique id in buffer flush order
static std::atomic<uint64_t> FlushId{0};
thread_local OmptTracingBufferMgr::BufPtr
OmptTracingBufferMgr::ArrayOfBufPtr[MAX_NUM_DEVICES];
static uint64_t get_and_inc_buf_id() { return BufId++; }
static uint64_t get_and_inc_flush_id() { return FlushId++; }
static uint64_t get_flush_id() { return FlushId; }
/*
* Used by OpenMP threads for assigning space for a trace record. If
* there is no space in the last buffer allocated by this thread, the
* last buffer is marked full and scheduled for flushing. Otherwise,
* space is assigned for a trace record and the new cursor returned.
* Since the memory allocated by a thread is used by that thread alone
* for creating trace records, a lock need not be held. In the less
* common branch when memory is allocated, a lock needs to be acquired
* for updating shared metadata. The common path of allocating a trace
* record from an existing buffer proceeds without locking.
*/
void *OmptTracingBufferMgr::assignCursor(ompt_callbacks_t Type,
int64_t DeviceId) {
// The caller should handle nullptr by not tracing for this event.
if (DeviceId < 0 || DeviceId > MAX_NUM_DEVICES - 1)
return nullptr;
size_t RecSize = getTRSize();
// If the buffer fills up, it will be scheduled for flushing with the
// following cursor.
void *ToBeFlushedCursor = nullptr;
BufPtr ToBeFlushedBuf = nullptr;
// Thread local buffer pointer should be non-null once an allocation
// has been done by this thread.
BufPtr DeviceBuf = getDeviceSpecificBuffer(DeviceId);
if (DeviceBuf != nullptr) {
assert(DeviceBuf->DeviceId == DeviceId && "Unexpected device id in buffer");
void *OldCursor = DeviceBuf->Cursor.load(std::memory_order_acquire);
// Try to assign a trace record from the last allocated buffer
if (RecSize <= DeviceBuf->RemainingBytes) {
assert((char *)DeviceBuf->Start + DeviceBuf->TotalBytes -
DeviceBuf->RemainingBytes ==
(char *)OldCursor + RecSize);
DeviceBuf->RemainingBytes -= RecSize;
// Note the trace record status must be initialized before setting
// the cursor, ensuring that a helper thread always sees an initialized
// trace record status.
void *NewCursor = (char *)OldCursor + RecSize;
initTraceRecordMetaData(NewCursor);
DeviceBuf->Cursor.store(NewCursor, std::memory_order_release);
ODBG(ODT_Tool) << "Thread " << llvm::omp::target::ompt::getThreadId()
<< ": Assigned " << RecSize << " bytes at " << NewCursor
<< " in existing buffer " << DeviceBuf->Start
<< " for device " << DeviceId;
return NewCursor;
} else {
ToBeFlushedCursor = OldCursor;
ToBeFlushedBuf = DeviceBuf;
// Mark that no space is present for any more trace records.
// The following is atomic but there is no logical order between when
// it is set here and when it is checked by a helper thread. That works
// because the helper thread uses this info to decide whether a buffer
// can be scheduled for removal. In the worst case, the buffer will be
// removed late.
DeviceBuf->IsFull.store(true, std::memory_order_release);
}
}
void *NewBuffer = nullptr;
size_t TotalBytes = 0;
// TODO Move the buffer allocation to a helper thread
llvm::omp::target::ompt::ompt_callback_buffer_request(DeviceId, &NewBuffer,
&TotalBytes);
// The caller should handle nullptr by not tracing for this event.
if (NewBuffer == nullptr || TotalBytes < RecSize)
return nullptr;
uint64_t NewBufId = get_and_inc_buf_id();
auto new_buf = std::make_shared<Buffer>(
NewBufId, DeviceId, /*Start=*/NewBuffer, TotalBytes,
/*RemainingBytes=*/TotalBytes - RecSize,
/*Cursor=*/NewBuffer,
/*IsFull=*/false);
// Initialize trace record status before publishing it to helper threads.
initTraceRecordMetaData(new_buf->Cursor.load(std::memory_order_acquire));
setDeviceSpecificBuffer(DeviceId, new_buf);
// Make this trace record visible to helper threads by adding to shared
// metadata.
std::unique_lock<std::mutex> lck(BufferMgrMutex);
assert(Id2BufferMap.find(NewBufId) == Id2BufferMap.end());
Id2BufferMap[NewBufId] = new_buf;
lck.unlock();
// Schedule the full buffer for flushing till the corresponding cursor.
if (OMPX_FlushOnBufferFull && ToBeFlushedCursor)
triggerFlushOnBufferFull(ToBeFlushedCursor, ToBeFlushedBuf);
ODBG(ODT_Tool) << "Thread " << llvm::omp::target::ompt::getThreadId()
<< ": Assigned " << RecSize << " bytes at " << NewBuffer
<< " in new buffer with id " << NewBufId << " for device "
<< DeviceId;
return NewBuffer;
}
/*
* Called by an OpenMP thread when a buffer fills up and should be
* flushed. This function assigns a new FlushId to the buffer, adds
* to the flush-related metadata and wakes up a helper thread to
* dispatch a buffer-completion callback. This function should be
* called without holding any lock.
* Note lock order: buf_lock -> flush_lock
*/
void OmptTracingBufferMgr::triggerFlushOnBufferFull(void *cursor, BufPtr Buf) {
std::unique_lock<std::mutex> buf_lock(BufferMgrMutex);
// Between calling this function and this check, a flush-all may have
// delivered this buffer to the tool and deleted it. So the buffer
// may not exist.
if (Id2BufferMap.find(Buf->Id) == Id2BufferMap.end())
return;
// Cannot assert that the state of the cursor is ready since a
// different thread may be in the process of populating it. If it
// remains in init state when the range of trace records is
// determined for dispatching the buffer-completion callback, it
// will not be included.
std::unique_lock<std::mutex> flush_lock(FlushMutex);
uint64_t flush_id;
auto flush_itr = FlushBufPtr2IdMap.find(Buf);
if (flush_itr == FlushBufPtr2IdMap.end()) {
// This buffer has not been flushed yet
addNewFlushEntry(Buf, cursor);
} else {
// This buffer has been flushed before
flush_id = flush_itr->second;
auto flush_md_itr = Id2FlushMdMap.find(flush_id);
assert(flush_md_itr != Id2FlushMdMap.end());
flush_md_itr->second.FlushCursor = cursor; // update the cursor
// Do not update the flush status since it may be under processing
// by another thread
ODBG(ODT_Tool) << "Updated id " << flush_id << " cursor " << cursor
<< " buf " << flush_md_itr->second.FlushBuf->Start;
}
flush_lock.unlock();
buf_lock.unlock();
// Wake up a helper thread to invoke the buffer-completion callback
FlushCv.notify_one();
}
// This is the driver routine for the completion thread
void OmptTracingBufferMgr::driveCompletion() {
while (true) {
bool should_signal_workers = false;
std::unique_lock<std::mutex> flush_lock(FlushMutex);
if (DoneTracing) {
// An upper layer serializes flush_trace and stop_trace. In
// addition, before DoneTracing is set, a flush is performed as
// part of stop_trace. So assert that no flush is in progress.
assert(ThreadFlushTracker == 0);
break;
}
FlushCv.wait(flush_lock, [this] {
return DoneTracing ||
(!Id2FlushMdMap.empty() &&
llvm::omp::target::ompt::TracingActive) ||
isThisThreadFlushWaitedUpon();
});
if (isThisThreadFlushWaitedUpon()) {
resetThisThreadFlush();
if (ThreadFlushTracker == 0)
should_signal_workers = true;
}
flush_lock.unlock();
invokeCallbacks();
if (should_signal_workers)
ThreadFlushCv.notify_all();
// There is a scenario where a buffer was processed but not full
// or owned, so it was put back in waiting state. So this thread
// would not wait but keep on looping without having any actual
// work until new trace records are added and this thread
// signaled. Hence, this thread yields.
std::this_thread::yield();
}
bool is_last_helper = false;
std::unique_lock<std::mutex> flush_lock(FlushMutex);
assert(DoneTracing && "Helper thread exiting but not yet done");
assert(isThisThreadShutdownWaitedUpon() &&
"Helper thread exiting but not waited upon");
resetThisThreadShutdown();
if (ThreadShutdownTracker == 0)
is_last_helper = true;
flush_lock.unlock();
if (is_last_helper)
ThreadShutdownCv.notify_all();
// Note that some trace records may have been written but not
// delivered to the tool. If flush/stop APIs are not called by the
// tool, those trace records may never be delivered to the tool and
// the corresponding buffers not reclaimed. TODO Explore whether
// this cleanup must be done.
}
/*
* Called by a buffer-completion helper thread. This function examines
* the flushed buffers in flush order and dispatches
* callbacks. Lock holding is minimized by reserving a buffer,
* processing it, and then unreserving it if there are more trace
* records to flush later. If all trace records are flushed, a
* callback is dispatched informing the tool that the buffer can be
* deallocated. If the buffer can be deallocated, all metadata is
* destroyed.
* Note that this function must be called without holding any locks.
*/
void OmptTracingBufferMgr::invokeCallbacks() {
ODBG(ODT_Tool) << "Looking for callbacks to invoke";
auto max_id = std::numeric_limits<uint64_t>::max();
auto curr_id = max_id;
auto end_id = get_flush_id();
ODBG(ODT_Tool) << "End id is " << end_id;
while (true) {
// Set the status of the flushed buffer to in-processing so that
// another helper thread does not process it concurrently. An
// OpenMP worker thread may, however, populate a trace record in a
// reserved buffer concurrently.
FlushInfo flush_info = findAndReserveFlushedBuf(curr_id);
// no entry found, nothing to process
if (curr_id == max_id && flush_info.FlushCursor == nullptr)
return;
if (flush_info.FlushCursor != nullptr) {
// increment curr_id to get the candidate for the next iteration
curr_id = flush_info.FlushId + 1;
} else {
assert(curr_id != max_id && "Cannot increment max id");
++curr_id;
}
ODBG(ODT_Tool) << "Next id will be " << curr_id;
if (flush_info.FlushCursor == nullptr) {
// This buffer must have been processed already
if (curr_id < end_id)
continue;
else
return; // nothing else to process
}
ODBG(ODT_Tool) << "Buf " << flush_info.FlushBuf->Start
<< " Cursor " << flush_info.FlushCursor
<< " Id " << flush_info.FlushId
<< " will be flushed";
// Examine the status of the trace records and dispatch
// buffer-completion callbacks as appropriate.
flushBuffer(flush_info);
// TODO optimize to set buffer-owned in the same pass above.
// Currently, this is the only way a buffer is deallocated
if (isBufferFull(flush_info)) {
// All trace records have been delivered to the tool
if (isBufferOwned(flush_info)) {
// erase element from buffer and flush maps
destroyFlushedBuf(flush_info);
// dispatch callback with a null range and have the tool
// deallocate the buffer
dispatchBufferOwnedCallback(flush_info);
} else {
unreserveFlushedBuf(flush_info);
}
} else {
unreserveFlushedBuf(flush_info);
}
if (curr_id >= end_id)
return;
}
}
/*
* This function is called on a buffer that is already reserved by
* this thread. Buffer-completion callbacks are dispatched for every
* range of trace records that are ready.
* This routine must be called without holding locks
*/
void OmptTracingBufferMgr::flushBuffer(FlushInfo flush_info) {
assert(flush_info.FlushBuf && "Cannot flush an empty buffer");
assert(flush_info.FlushCursor && "Cannot flush upto a null cursor");
void *curr_tr = flush_info.FlushBuf->Start;
void *last_tr = flush_info.FlushCursor;
// Compute a range [first_cursor,last_cursor] to flush
void *first_cursor = nullptr;
void *last_cursor = nullptr;
while (curr_tr <= last_tr) {
TRStatus tr_status = getTRStatus(curr_tr);
if (tr_status == TR_init || tr_status == TR_released) {
if (first_cursor == nullptr) {
// This TR won't be part of a range
assert(last_cursor == nullptr &&
"Begin/last cursors mutually inconsistent");
} else {
// End the current interval
dispatchCallback(flush_info.FlushBuf->DeviceId,
flush_info.FlushBuf->Start, first_cursor, last_cursor);
first_cursor = last_cursor = nullptr;
}
} else {
assert(tr_status == TR_ready && "Unknown trace record status");
setTRStatus(curr_tr, TR_released);
if (first_cursor == nullptr)
first_cursor = curr_tr;
last_cursor = curr_tr;
}
curr_tr = getNextTR(curr_tr);
}
if (first_cursor != nullptr) {
assert(last_cursor != nullptr);
dispatchCallback(flush_info.FlushBuf->DeviceId, flush_info.FlushBuf->Start,
first_cursor, last_cursor);
}
}
// Given a range of trace records, dispatch a buffer-completion callback
void OmptTracingBufferMgr::dispatchCallback(int64_t DeviceId, void *Buffer,
void *FirstCursor,
void *LastCursor) {
assert(FirstCursor != nullptr && LastCursor != nullptr &&
"Callback with nullptr");
addLastCursor(LastCursor);
// This is best effort.
// There is a small window when the buffer-completion callback may
// be invoked even after tracing has been disabled.
// Note that we don't want to hold a lock when dispatching the callback.
if (llvm::omp::target::ompt::isTracedDevice(DeviceId)) {
ODBG(ODT_Tool) << "Dispatch callback w/ range (inclusive) to be flushed: "
<< FirstCursor << " -> " << LastCursor;
llvm::omp::target::ompt::ompt_callback_buffer_complete(
DeviceId, Buffer,
/* bytes returned in this callback */
(char *)getNextTR(LastCursor) - (char *)FirstCursor,
(ompt_buffer_cursor_t)FirstCursor, false /* buffer_owned */);
}
removeLastCursor(LastCursor);
}
// Dispatch a buffer-completion callback with buffer_owned set so that
// the tool can deallocate the buffer
void OmptTracingBufferMgr::dispatchBufferOwnedCallback(
const FlushInfo &flush_info) {
// This is best effort.
// There is a small window when the buffer-completion callback may
// be invoked even after tracing has been disabled.
// Note that we don't want to hold a lock when dispatching the callback.
if (llvm::omp::target::ompt::isTracedDevice(flush_info.FlushBuf->DeviceId)) {
ODBG(ODT_Tool) << "Dispatch callback with buffer "
<< flush_info.FlushBuf->Start << " owned";
llvm::omp::target::ompt::ompt_callback_buffer_complete(
flush_info.FlushBuf->DeviceId, flush_info.FlushBuf->Start, 0,
(ompt_buffer_cursor_t)0, true /* buffer owned */);
}
}
void OmptTracingBufferMgr::initTraceRecordMetaData(void *Rec) {
setTRStatus(Rec, TR_init);
}
OmptTracingBufferMgr::BufPtr
OmptTracingBufferMgr::getDeviceSpecificBuffer(int64_t DeviceId) {
if (DeviceId < 0 || DeviceId > MAX_NUM_DEVICES - 1) {
REPORT() << "getDeviceSpecificBuffer: Device id " << DeviceId
<< " invalid or exceeds supported max: "
<< MAX_NUM_DEVICES - 1;
return nullptr;
}
return ArrayOfBufPtr[DeviceId];
}
void OmptTracingBufferMgr::setDeviceSpecificBuffer(int64_t DeviceId,
BufPtr Buf) {
if (DeviceId < 0 || DeviceId > MAX_NUM_DEVICES - 1) {
REPORT() << "setDeviceSpecificBuffer: Device id " << DeviceId
<< " invalid or exceeds supported max: "
<< MAX_NUM_DEVICES - 1;
return;
}
ArrayOfBufPtr[DeviceId] = Buf;
}
void OmptTracingBufferMgr::setTRStatus(void *Rec, TRStatus Status) {
TraceRecord *TR = static_cast<TraceRecord *>(Rec);
TR->TRState.store(Status, std::memory_order_release);
}
OmptTracingBufferMgr::TRStatus OmptTracingBufferMgr::getTRStatus(void *Rec) {
return static_cast<TraceRecord *>(Rec)->TRState.load(
std::memory_order_acquire);
}
void *OmptTracingBufferMgr::getNextTR(void *TR) {
size_t RecSize = getTRSize();
// warning: no overflow check done
return (char *)TR + RecSize;
}
bool OmptTracingBufferMgr::isBufferFull(const FlushInfo &flush_info) {
std::unique_lock<std::mutex> buf_lock(BufferMgrMutex);
return flush_info.FlushBuf->IsFull;
}
void *OmptTracingBufferMgr::getBufferCursor(BufPtr buf) {
return buf->Cursor.load(std::memory_order_acquire);
}
/*
* Traverse all the trace records of a buffer and return true if all
* of them have been released to the tool, otherwise return false
*/
bool OmptTracingBufferMgr::isBufferOwned(const FlushInfo &flush_info) {
assert(isBufferFull(flush_info) && "Compute buffer-owned when it is full");
void *curr_tr = flush_info.FlushBuf->Start;
// Since the buffer is full, the cursor must be the last valid
// TR. Note that this may be more up-to-date than the cursor in the
// flush_info. Use the last valid TR to avoid dropping trace records
void *last_tr = getBufferCursor(flush_info.FlushBuf);
while (curr_tr <= last_tr) {
if (getTRStatus(curr_tr) != TR_released)
return false;
curr_tr = getNextTR(curr_tr);
}
return true;
}
/*
* A buffer must be reserved by a thread before it can be processed
* and callbacks dispatched for that buffer. Reservation is done by
* setting the status to in-processing.
*
* If a buffer is found in the flush metadata for the given id and it
* is not in in-processing mode, reserve it by setting its mode to
* in-processing and return the corresponding flush metadata. If the
* given id is set to max, return the first waiting buffer in the
* list of buffers to be flushed.
*/
OmptTracingBufferMgr::FlushInfo
OmptTracingBufferMgr::findAndReserveFlushedBuf(uint64_t FlushId) {
std::unique_lock<std::mutex> flush_lock(FlushMutex);
MapId2Md::iterator flush_itr;
if (FlushId == std::numeric_limits<uint64_t>::max()) {
// Reserve the first waiting buffer and return it
if (Id2FlushMdMap.empty())
return FlushInfo();
for (flush_itr = Id2FlushMdMap.begin(); flush_itr != Id2FlushMdMap.end();
++flush_itr) {
// Reserve only if waiting
if (flush_itr->second.FlushStatus == Flush_waiting)
break;
}
if (flush_itr == Id2FlushMdMap.end())
return FlushInfo();
} else {
flush_itr = Id2FlushMdMap.find(FlushId);
if (flush_itr == Id2FlushMdMap.end() ||
flush_itr->second.FlushStatus == Flush_processing)
return FlushInfo();
}
assert(flush_itr->second.FlushStatus == Flush_waiting);
flush_itr->second.FlushStatus = Flush_processing;
// Update the metadata cursor since more trace records may have been
// generated.
flush_itr->second.FlushCursor =
flush_itr->second.FlushBuf->Cursor.load(std::memory_order_acquire);
FlushInfo flush_info(flush_itr->first, flush_itr->second.FlushCursor,
flush_itr->second.FlushBuf);
ODBG(ODT_Tool) << "Reserved buffer: flush_id:" << flush_itr->first
<< ", cursor:" << flush_itr->second.FlushCursor
<< ", buf:" << flush_itr->second.FlushBuf->Start;
return flush_info;
}
/*
* Given a buffer, verify that it is in processing state and set its
* status to waiting, removing the reservation. The same thread that
* reserved it should be unreserving it but currently there is no such
* check.
*/
void OmptTracingBufferMgr::unreserveFlushedBuf(const FlushInfo &flush_info) {
std::unique_lock<std::mutex> flush_lock(FlushMutex);
auto itr = Id2FlushMdMap.find(flush_info.FlushId);
assert(itr != Id2FlushMdMap.end() &&
itr->second.FlushStatus == Flush_processing);
itr->second.FlushStatus = Flush_waiting;
ODBG(ODT_Tool) << "Unreserved buffer: flush_id:" << flush_info.FlushId
<< ", cursor:" << flush_info.FlushCursor
<< ", buf:" << flush_info.FlushBuf->Start;
}
/*
* This function must be called after all of the trace records in the
* buffer have been released to the tool. The buffer is removed from
* all metadata maps.
* Note lock order: buf_lock -> flush_lock
*/
void OmptTracingBufferMgr::destroyFlushedBuf(const FlushInfo &flush_info) {
ODBG(ODT_Tool) << "Destroying buffer: flush_id:" << flush_info.FlushId
<< ", cursor:" << flush_info.FlushCursor
<< ", buf:" << flush_info.FlushBuf->Start;
BufPtr buf = flush_info.FlushBuf;
std::unique_lock<std::mutex> buf_lock(BufferMgrMutex);
Id2BufferMap.erase(buf->Id);
std::unique_lock<std::mutex> flush_lock(FlushMutex);
auto flush_itr = Id2FlushMdMap.find(flush_info.FlushId);
assert(flush_itr != Id2FlushMdMap.end());
assert(flush_itr->second.FlushBuf == buf);
Id2FlushMdMap.erase(flush_itr);
FlushBufPtr2IdMap.erase(buf);
}
/*
* Generate a new flush id and add the buffer to the flush metadata
* maps. This function must be called while holding the flush lock.
*/
uint64_t OmptTracingBufferMgr::addNewFlushEntry(BufPtr Buf, void *Cursor) {
assert(FlushBufPtr2IdMap.find(Buf) == FlushBufPtr2IdMap.end());
uint64_t FlushId = get_and_inc_flush_id();
FlushBufPtr2IdMap.emplace(Buf, FlushId);
assert(Id2FlushMdMap.find(FlushId) == Id2FlushMdMap.end());
Id2FlushMdMap.emplace(FlushId, FlushMd(Cursor, Buf, Flush_waiting));
ODBG(ODT_Tool) << "Added new flush id "
<< FlushId << " cursor "
<< Cursor << " buf " << Buf->Start;
return FlushId;
}
/*
* Called by ompt_flush_trace and ompt_stop_trace. Traverse the
* existing buffers in creation order and flush all the ready TRs
*/
int OmptTracingBufferMgr::flushAllBuffers(int DeviceId) {
ODBG(ODT_Tool) << "Flushing buffers for device " << DeviceId << " :: START";
// Overloading MAX_NUM_DEVICES to mean all devices.
if (DeviceId < 0 || DeviceId > MAX_NUM_DEVICES)
return 0; // failed to flush
if (!areHelperThreadsAvailable())
return 0; // failed to flush
// If flush is called from a helper thread, just bail out
if (amIHelperThread())
return 0; // failed to flush
// To avoid holding the mutex for too long, get the ids of the first
// and the last TRs under lock, and then go through that range,
// holding the mutex for an individual TR
std::unique_lock<std::mutex> buf_lock(BufferMgrMutex);
if (Id2BufferMap.empty())
return 1; // no trace records to flush
uint64_t curr_buf_id = Id2BufferMap.begin()->first;
uint64_t last_buf_id = Id2BufferMap.rbegin()->first;
buf_lock.unlock();
while (curr_buf_id <= last_buf_id) {
std::unique_lock<std::mutex> buf_lock(BufferMgrMutex);
// Another thread may have deleted this buffer by now
auto buf_itr = Id2BufferMap.find(curr_buf_id);
if (buf_itr == Id2BufferMap.end()) {
++curr_buf_id;
continue;
}
BufPtr curr_buf = buf_itr->second;
// If the device-id does not match, skip it. A device-id of MAX_NUM_DEVICES
// indicates flushing for all devices.
if (DeviceId != MAX_NUM_DEVICES && curr_buf->DeviceId != DeviceId) {
++curr_buf_id;
continue;
}
// If this buffer is in the flush-map, skip it. It is either in
// process by another thread or will be processed
std::unique_lock<std::mutex> flush_lock(FlushMutex);
auto flush_itr = FlushBufPtr2IdMap.find(curr_buf);
if (flush_itr != FlushBufPtr2IdMap.end()) {
++curr_buf_id;
continue;
}
// This buffer has not been flushed yet
void *CurrBufCursor = getBufferCursor(curr_buf);
uint64_t flush_id = addNewFlushEntry(curr_buf, CurrBufCursor);
(void)flush_id; // Silence warning.
ODBG(ODT_Tool) << "flushAllBuffers: Added new id "
<< flush_id << " cursor " << CurrBufCursor
<< " buf " << curr_buf->Start;
flush_lock.unlock();
buf_lock.unlock();
++curr_buf_id;
}
ODBG(ODT_Tool) << "Flushing buffers for device " << DeviceId << " :: WAIT";
// This is best effort. It is possible that some trace records are
// not flushed when the wait is done.
waitForFlushCompletion();
ODBG(ODT_Tool) << "Flushing buffers for device " << DeviceId << " :: STOP";
return 1; // success
}
void OmptTracingBufferMgr::waitForFlushCompletion() {
{
std::unique_lock<std::mutex> flush_lock(FlushMutex);
// Setting the flush bit for a given helper thread indicates that the worker
// thread is ready for the helper thread to do some work.
for (uint32_t i = 0; i < OMPT_NUM_HELPER_THREADS; ++i)
setThreadFlush(i);
}
// Wake up all helper threads to invoke buffer-completion callbacks.
FlushCv.notify_all();
// Now wait for all helper threads to complete flushing.
{
std::unique_lock<std::mutex> flush_lock(FlushMutex);
ThreadFlushCv.wait(flush_lock, [this] { return ThreadFlushTracker == 0; });
}
}
void OmptTracingBufferMgr::init() {
for (int i = 0; i < MAX_NUM_DEVICES; ++i)
ArrayOfBufPtr[i] = nullptr;
ThreadFlushTracker = 0;
ThreadShutdownTracker = 0;
DoneTracing = false; // TODO make it a class member
}
void OmptTracingBufferMgr::startHelperThreads() {
// All helper threads are stopped while holding FlushMutex. So if
// any helper thread is present, just return. This takes care of
// repeated calls to start-trace.
std::unique_lock<std::mutex> flush_lock(FlushMutex);
if (!HelperThreadIdMap.empty()) {
assert(!DoneTracing && "Helper threads exist but tracing is done");
return;
}
init();
createHelperThreads();
}
bool OmptTracingBufferMgr::areHelperThreadsAvailable() {
std::unique_lock<std::mutex> flush_lock(FlushMutex);
if (DoneTracing // If another thread called stop, assume there are no threads
|| HelperThreadIdMap.empty() // Threads were never started
) {
// Don't assert on HelperThreadIdMap since shutdown by another
// thread may be in progress
return false;
}
return true;
}
void OmptTracingBufferMgr::shutdownHelperThreads() {
if (!areHelperThreadsAvailable())
return;
std::unique_lock<std::mutex> flush_lock(FlushMutex);
// If I am destroying the threads, then at least one thread must be present
assert(!CompletionThreads.empty());
assert(!HelperThreadIdMap.empty());
assert(ThreadShutdownTracker == 0);
// Set the done flag which helper threads will look at
DoneTracing = true;
// Wait to make sure all helper threads exit
for (uint32_t i = 0; i < OMPT_NUM_HELPER_THREADS; ++i)
setThreadShutdown(i);
// Signal indicating that DoneTracing is set
FlushCv.notify_all();
ThreadShutdownCv.wait(flush_lock,
[this] { return ThreadShutdownTracker == 0; });
// Now destroy all the helper threads
destroyHelperThreads();
}
void OmptTracingBufferMgr::flushAndShutdownHelperThreads() {
std::unique_lock<std::mutex> Lock(llvm::omp::target::ompt::TraceControlMutex);
// Flush buffers for all devices.
if (OMPX_FlushOnShutdown)
flushAllBuffers(MAX_NUM_DEVICES);
else
waitForFlushCompletion(); // Dont initiate but wait for outstanding flushes.
shutdownHelperThreads();
}
void OmptTracingBufferMgr::createHelperThreads() {
for (uint32_t i = 0; i < OMPT_NUM_HELPER_THREADS; ++i) {
CompletionThreads.emplace_back(
std::thread(&OmptTracingBufferMgr::driveCompletion, this));
HelperThreadIdMap[CompletionThreads.back().get_id()] = i;
}
}
void OmptTracingBufferMgr::destroyHelperThreads() {
for (auto &thd : CompletionThreads)
thd.join();
CompletionThreads.clear();
HelperThreadIdMap.clear();
}
#endif