Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ target_sources(gpufl PRIVATE
include/gpufl/backends/amd/amd_capture_capabilities.cpp
include/gpufl/backends/amd/amd_trace_policy.cpp
include/gpufl/core/dictionary_manager.cpp
include/gpufl/core/source_capture_policy.cpp
include/gpufl/core/sass_compressor.cpp
include/gpufl/core/logger/logger.cpp
include/gpufl/core/logger/lifecycle_control_journal.cpp
Expand Down
1 change: 1 addition & 0 deletions daemon/launcher/cli_parse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct TraceArgs {
bool verbose = false; // -v
bool quiet = false; // -q
bool no_source = false; // --no-source: explicit source-content opt-out
std::string source_root; // --source-root: approved project source boundary
bool upload = false; // --upload: start gpufl-agent for live upload
std::string backend_url; // --backend-url; else GPUFL_BACKEND_URL
std::string api_key; // --api-key; else GPUFL_API_KEY
Expand Down
5 changes: 5 additions & 0 deletions daemon/launcher/cli_trace_options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,11 @@ const CliOptionManager<TraceArgs>& traceOptions() {
"correlation",
kSection(TraceHelpSection::Capture),
&setFlag<&TraceArgs::no_source>)
.add({"--source-root"}, "<DIR>",
"Allow correlated CUDA/C++ source only within this project "
"directory (default: current working directory)",
kSection(TraceHelpSection::Capture),
&parseNonEmptyStringOption<&TraceArgs::source_root>)

// Runtime
.add({"-q", "--quiet"}, "",
Expand Down
25 changes: 25 additions & 0 deletions daemon/launcher/trace_command_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ fs::path findInjectLib(const TracePlatform& platform, const fs::path& exe) {
return {};
}

fs::path resolveSourceRoot(const TraceArgs& args, std::string& error) {
std::error_code ec;
fs::path root = args.source_root.empty()
? fs::current_path(ec)
: fs::path(args.source_root);
if (ec) {
error = "cannot resolve the current working directory for source "
"capture: " + ec.message();
return {};
}
root = fs::weakly_canonical(root, ec);
if (ec || !fs::is_directory(root, ec) || ec) {
error = "--source-root is not an accessible directory";
return {};
}
return root;
}

// A '+'-joined pass token ("Trace+PcSampling") runs those engines together in
// one process via GPUFL_ENGINE_COMBO. Returns the comma-joined combo for a
// composite token, or "" for a single-engine token.
Expand Down Expand Up @@ -736,6 +754,11 @@ int runTraceCommon(const TraceArgs& args, const TracePlatform& platform) {
DebugLogger::setEnabled(args.verbose);

std::string error;
const fs::path source_root = resolveSourceRoot(args, error);
if (source_root.empty()) {
std::fprintf(stderr, "gpufl: %s\n", error.c_str());
return 2;
}
if (!platform.prepareInjectionEnv(inject_lib, error)) {
std::fprintf(stderr, "gpufl: %s\n", error.c_str());
return 2;
Expand All @@ -749,6 +772,8 @@ int runTraceCommon(const TraceArgs& args, const TracePlatform& platform) {
!setEnvOrPrint(platform, env::kInjectProfile, inject::kProfileComprehensive) ||
!setEnvOrPrint(platform, env::kIncludeSource,
args.no_source ? "0" : "1") ||
!setEnvOrPrint(platform, env::kSourceRoot,
source_root.string()) ||
!setEnvOrPrint(platform, env::kInjectUpload, "0")) {
return 2;
}
Expand Down
99 changes: 80 additions & 19 deletions include/gpufl/core/dictionary_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <vector>
Expand Down Expand Up @@ -75,6 +76,45 @@ void appendDict(std::ostringstream& oss, const char* key,
oss << '}';
}

std::string buildSourceCaptureManifestJson(
const detail::SourceCaptureManifest& manifest,
const std::string& session_id) {
std::ostringstream oss;
oss << "{\"version\":1,\"type\":\"source_capture_manifest\""
<< ",\"session_id\":\"" << model::jsonEscape(session_id) << '"'
<< ",\"policy\":\"correlated-project-source-v1\""
<< ",\"enabled\":" << (manifest.enabled ? "true" : "false")
<< ",\"approved_root_count\":" << manifest.approved_root_count
<< ",\"limits\":{\"max_files\":" << manifest.limits.max_files
<< ",\"max_bytes_per_file\":"
<< manifest.limits.max_bytes_per_file
<< ",\"max_total_bytes\":" << manifest.limits.max_total_bytes
<< ",\"max_line_bytes\":" << manifest.limits.max_line_bytes
<< ",\"max_manifest_entries\":"
<< manifest.limits.max_manifest_entries << "},\"files\":[";
bool first = true;
for (const auto& file : manifest.files) {
if (!first) oss << ',';
first = false;
oss << "{\"source_file_id\":" << file.source_file_id
<< ",\"logical_path\":\""
<< model::jsonEscape(file.logical_path) << '"'
<< ",\"discovery_reason\":\""
<< model::jsonEscape(file.discovery_reason) << '"'
<< ",\"disposition\":\""
<< detail::sourceCaptureDispositionName(file.disposition) << '"'
<< ",\"bytes\":" << file.bytes << '}';
}
oss << "],\"totals\":{\"captured_files\":"
<< manifest.captured_files
<< ",\"captured_bytes\":" << manifest.captured_bytes
<< ",\"skipped_files\":" << manifest.skipped_files
<< ",\"truncated_files\":0"
<< ",\"omitted_manifest_entries\":"
<< manifest.omitted_manifest_entries << "}}";
return oss.str();
}

#ifndef _WIN32
// Launch argv[0] with stdout connected to a pipe we read and stderr sent
// to /dev/null, via posix_spawn - deliberately NOT popen/system.
Expand Down Expand Up @@ -123,6 +163,12 @@ FILE *spawnReadPipe(char *const argv[], pid_t &outPid) {

} // namespace

void DictionaryManager::configureSourceCapture(
const bool enabled, const SourceCaptureSettings& settings) {
std::lock_guard lk(mu_);
source_capture_.configure(enabled, settings);
}

uint32_t DictionaryManager::internSourceFile(const std::string& path) {
if (path.empty()) return 0;
std::lock_guard lk(mu_);
Expand All @@ -131,32 +177,39 @@ uint32_t DictionaryManager::internSourceFile(const std::string& path) {
return it->second;
const uint32_t id = next_source_file_id_++;
source_file_dict_[path] = id;
dirty_source_files_[path] = id;

// Read file content eagerly when source collection is enabled.
// When disabled, we still intern the path (needed for function keys
// and source_file_id in profile samples) but skip reading the actual
// source code from disk - users who don't want their source code
// sent to the backend can set enable_source_collection = false.
if (enable_source_collection) {
std::ifstream f(path);
if (f.is_open()) {
std::vector<std::string> lines;
std::string line;
while (std::getline(f, line)) lines.push_back(line);
if (!lines.empty()) pending_source_content_[id] = std::move(lines);
}
auto capture = source_capture_.capture(
path, id, "profiler_source_correlation");
source_file_names_[id] = capture.record.logical_path;
dirty_source_files_[capture.record.logical_path] = id;
if (capture.record.disposition ==
detail::SourceCaptureDisposition::Captured) {
pending_source_content_[id] = std::move(capture.lines);
}
return id;
}

std::string DictionaryManager::sourceFileName(
const uint32_t source_file_id) {
std::lock_guard lk(mu_);
const auto it = source_file_names_.find(source_file_id);
return it == source_file_names_.end() ? std::string() : it->second;
}

void DictionaryManager::flushSourceContent(Logger& logger,
const std::string& session_id) {
std::unordered_map<uint32_t, std::vector<std::string>> pending;
std::optional<detail::SourceCaptureManifest> manifest;
{
std::lock_guard lk(mu_);
if (pending_source_content_.empty()) return;
pending = std::move(pending_source_content_);
if (source_capture_.manifestDirty()) {
manifest = source_capture_.manifest();
source_capture_.markManifestFlushed();
}
}
if (manifest) {
logger.write(SassLine{
buildSourceCaptureManifestJson(*manifest, session_id)});
}
for (auto& [file_id, lines] : pending) {
std::ostringstream oss;
Expand Down Expand Up @@ -732,8 +785,12 @@ void DictionaryManager::flushDisassembly(Logger& logger,
// Demangle the name so it merges with the PC-sample function
// entry (which is demangled), and carry the mangled symbol so
// the funcKey/disassembly join still keys off md5(symbol).
const auto source_id = sourceFileIds.find(primarySourceFile);
const std::string source_name = source_id == sourceFileIds.end()
? std::string()
: sourceFileName(source_id->second);
internFunction(
core::DemangleFunctionKey(funcName + "@" + primarySourceFile),
core::DemangleFunctionKey(funcName + "@" + source_name),
funcName);
}

Expand Down Expand Up @@ -781,8 +838,12 @@ void DictionaryManager::flushDisassembly(Logger& logger,
bestCount = cnt;
}
}
const auto source_id = sourceFileIds.find(primarySourceFile);
const std::string source_name = source_id == sourceFileIds.end()
? std::string()
: sourceFileName(source_id->second);
const std::string funcKey =
core::DemangleFunctionKey(funcName + "@" + primarySourceFile);
core::DemangleFunctionKey(funcName + "@" + source_name);
const uint32_t functionId = internFunction(funcKey, funcName);

// Build profile_sample_batch JSON
Expand Down Expand Up @@ -901,7 +962,7 @@ void DictionaryManager::flushDictionaryForSegment(
for (const auto& [name, id] : metric_dict_) {
if (emitter.metrics_.insert(id).second) dm.emplace(name, id);
}
for (const auto& [name, id] : source_file_dict_) {
for (const auto& [id, name] : source_file_names_) {
if (emitter.source_files_.insert(id).second) dsf.emplace(name, id);
}
}
Expand Down
18 changes: 13 additions & 5 deletions include/gpufl/core/dictionary_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include <unordered_map>
#include <vector>

#include "gpufl/core/source_capture_policy.hpp"

namespace gpufl {

class Logger;
Expand Down Expand Up @@ -38,11 +40,10 @@ class SegmentDictionaryEmitter {

class DictionaryManager {
public:
/// When false, internSourceFile() still interns the path (needed for
/// the function key) but does NOT read the file content from disk.
/// flushSourceContent() becomes a no-op. Users who don't want their
/// source code sent to the backend can set this to false.
bool enable_source_collection = true;
/// Configure the root/type/file-kind/budget policy used before any
/// profiler-discovered path is opened.
void configureSourceCapture(bool enabled,
const SourceCaptureSettings& settings);

uint32_t internKernel(const std::string& name) {
std::lock_guard lk(mu_);
Expand Down Expand Up @@ -97,6 +98,7 @@ class DictionaryManager {
}

uint32_t internSourceFile(const std::string& path);
std::string sourceFileName(uint32_t source_file_id);

// Emits a dictionary_update JSON line to Channel::All for any new entries
// accumulated since the last call. No-op if nothing is dirty.
Expand Down Expand Up @@ -148,9 +150,11 @@ class DictionaryManager {
dirty_metrics_.clear();
next_metric_id_ = 1;
source_file_dict_.clear();
source_file_names_.clear();
dirty_source_files_.clear();
next_source_file_id_ = 1;
pending_source_content_.clear();
source_capture_.reset();
pending_disasm_cubins_.clear();
}

Expand All @@ -177,11 +181,15 @@ class DictionaryManager {
uint32_t next_metric_id_ = 1;

std::unordered_map<std::string, uint32_t> source_file_dict_;
// file_id -> normalized logical path emitted to product-facing logs.
// source_file_dict_ keeps the local lookup key but is never serialized.
std::unordered_map<uint32_t, std::string> source_file_names_;
std::unordered_map<std::string, uint32_t> dirty_source_files_;
uint32_t next_source_file_id_ = 1;

// file_id → lines (populated once per new source file, flushed via flushSourceContent)
std::unordered_map<uint32_t, std::vector<std::string>> pending_source_content_;
detail::SourceCapturePolicy source_capture_;

// cubin_crc → raw bytes (populated once per cubin, flushed via flushDisassembly)
std::unordered_map<uint64_t, std::vector<uint8_t>> pending_disasm_cubins_;
Expand Down
4 changes: 4 additions & 0 deletions include/gpufl/core/env_vars.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ constexpr const char* kDebugOutput = "GPUFL_DEBUG";
// explicit value: "1" by default, or "0" for --no-source. Applying it after
// file configuration makes the command-line privacy choice authoritative.
constexpr const char* kIncludeSource = "GPUFL_INCLUDE_SOURCE";
// Launcher-owned approved project root for bounded correlated source capture.
// This is local process configuration only; it must not be serialized into
// product events or broad logs.
constexpr const char* kSourceRoot = "GPUFL_SOURCE_ROOT";

// Override the per-file log rotation threshold in bytes (default:
// Logger::kDefaultRotateBytes = 64 MiB). Mainly for tests - a tiny value
Expand Down
20 changes: 14 additions & 6 deletions include/gpufl/core/monitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,13 +387,15 @@ struct RecordProcessor {
static void handlePcSample(const ActivityRecord& rec, Runtime* rt) {
uint8_t kind = rec.metric_name[0] != '\0' ||
(rec.sample_kind[0] != '\0' && rec.sample_kind[0] == 's') ? 1 : 0;
const std::string func_key = std::string(rec.function_name) + "@" + rec.source_file;
const uint32_t source_file_id =
g_state.batches.internSourceFile(rec.source_file);
const std::string func_key = std::string(rec.function_name) + "@" +
g_state.batches.sourceFileName(source_file_id);
const uint32_t function_id = g_state.batches.internFunction(
g_state.metadata.demangleFunctionKey(func_key), std::string(rec.function_name));
const std::string metric_key = (rec.metric_name[0] != '\0') ? std::string(rec.metric_name) : rec.reason_name;
const uint32_t metric_id = g_state.batches.internMetric(metric_key);
const uint32_t scope_name_id = g_state.batches.activeScopeNameId();
const uint32_t source_file_id = g_state.batches.internSourceFile(rec.source_file);
const ProfileSampleBatchRow row = detail::MakeProfileSampleBatchRow(
rec, kind, function_id, metric_id, scope_name_id, source_file_id);

Expand Down Expand Up @@ -576,7 +578,8 @@ void Monitor::Initialize(const MonitorOptions& opts) {
detail::ActiveCounterProvider()->begin_session();
g_state.batches.reset();
g_state.metadata.reset();
g_state.batches.setSourceCollectionEnabled(opts.enable_source_collection);
g_state.batches.configureSourceCapture(opts.enable_source_collection,
opts.source_capture);
if (Runtime* rt = runtime(); rt && rt->hasSegmentContext()) {
g_state.batches.bindFlushRuntime(rt);
}
Expand Down Expand Up @@ -822,15 +825,20 @@ void Monitor::PushProfileSamples(const std::vector<ProfileSampleInput>& samples)
for (const auto& s : samples) {
ProfileSampleBatchRow row;
row.ts_ns = s.ts_ns; row.corr_id = s.corr_id; row.device_id = s.device_id;
const std::string funcSymbol = s.function_key.substr(0, s.function_key.find('@'));
row.function_id = g_state.batches.internFunction(demangledKey(s.function_key), funcSymbol);
const std::string funcSymbol =
s.function_key.substr(0, s.function_key.find('@'));
row.source_file_id =
g_state.batches.internSourceFile(s.source_file);
const std::string normalizedFunctionKey = funcSymbol + "@" +
g_state.batches.sourceFileName(row.source_file_id);
row.function_id = g_state.batches.internFunction(
demangledKey(normalizedFunctionKey), funcSymbol);
row.pc_offset = s.pc_offset;
row.metric_id = g_state.batches.internMetric(s.metric_name);
row.metric_value = s.metric_value;
row.stall_reason = s.stall_reason;
row.sample_kind = s.sample_kind;
row.scope_name_id = scope_name_id;
row.source_file_id = g_state.batches.internSourceFile(s.source_file);
row.source_line = s.source_line;
rows.push_back(row);
}
Expand Down
2 changes: 2 additions & 0 deletions include/gpufl/core/monitor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "gpufl/core/activity_record.hpp"
#include "gpufl/core/events.hpp"
#include "gpufl/core/ring_buffer.hpp"
#include "gpufl/core/source_capture_policy.hpp"
#include "gpufl/core/stream_handle.hpp"
#include "gpufl/core/trace_type.hpp"

Expand Down Expand Up @@ -157,6 +158,7 @@ struct MonitorOptions {
bool enable_debug_output = false;
bool enable_stack_trace = false;
bool enable_source_collection = true;
SourceCaptureSettings source_capture;
// Gate for CUPTI_ACTIVITY_KIND_EXTERNAL_CORRELATION. Mirror of
// InitOptions::enable_external_correlation; copied across in the
// gpufl::init() → CuptiBackend::initialize() conversion path.
Expand Down
10 changes: 8 additions & 2 deletions include/gpufl/core/monitor_batch_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ void MonitorBatchManager::clearFlushSink() {
flushSink_ = {};
}

void MonitorBatchManager::setSourceCollectionEnabled(bool enabled) {
dictManager_.enable_source_collection = enabled;
void MonitorBatchManager::configureSourceCapture(
const bool enabled, const SourceCaptureSettings& settings) {
dictManager_.configureSourceCapture(enabled, settings);
}

void MonitorBatchManager::flushAll(FlushMode mode) {
Expand Down Expand Up @@ -200,6 +201,11 @@ uint32_t MonitorBatchManager::internSourceFile(const std::string& path) {
return dictManager_.internSourceFile(path);
}

std::string MonitorBatchManager::sourceFileName(
const uint32_t source_file_id) {
return dictManager_.sourceFileName(source_file_id);
}

void MonitorBatchManager::enqueueDisassembly(uint64_t crc, const uint8_t* data, size_t size) {
dictManager_.enqueueDisassembly(crc, data, size);
}
Expand Down
Loading
Loading