Skip to content
Open
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
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ option(ENABLE_SIMD_PLUGIN "Enable the SIMD vector plugin" ON)
option(ENABLE_STD_PLUGIN "Enable the standard library wrapper plugin" ON)
option(ENABLE_SPECIALIZATION_PLUGIN "Enable the code specialization hint plugin (profile + assume)" ON)
option(ENABLE_INLINING_PLUGIN "Enable the LLVM function inlining plugin" ON)
cmake_dependent_option(ENABLE_PROFILING_PLUGIN "Enable the inline region profiling plugin" ON "ENABLE_TRACING" OFF)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
Expand Down Expand Up @@ -187,6 +188,9 @@ endif ()
if (ENABLE_INLINING_PLUGIN)
add_subdirectory(plugins/inlining)
endif ()
if (ENABLE_PROFILING_PLUGIN)
add_subdirectory(plugins/profiling)
endif ()
add_make_format()


Expand Down
4 changes: 4 additions & 0 deletions example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ set(ENABLE_STD_PLUGIN ON CACHE BOOL "" FORCE)
set(ENABLE_SIMD_PLUGIN ON CACHE BOOL "" FORCE)
set(ENABLE_SPECIALIZATION_PLUGIN ON CACHE BOOL "" FORCE)
set(ENABLE_INLINING_PLUGIN ON CACHE BOOL "" FORCE)
set(ENABLE_PROFILING_PLUGIN ON CACHE BOOL "" FORCE)
# Enable the AsmJit backend so demo_backends can include it in its comparison.
# This is OFF by default in the top-level nautilus build but is safe on
# x86-64 Linux / macOS and ARM64 Linux.
Expand Down Expand Up @@ -83,3 +84,6 @@ if (ENABLE_INLINING_PLUGIN)
message(STATUS "Skipping demo_inlining_plugin: inlining plugin is not supported on this toolchain.")
endif ()
endif ()

add_executable(demo_profiling_plugin src/DemoProfilingPlugin.cpp)
target_link_libraries(demo_profiling_plugin PRIVATE nautilus nautilus-profiling)
138 changes: 138 additions & 0 deletions example/src/DemoProfilingPlugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// DemoProfilingPlugin.cpp — automatic profiling of a compiled module.
//
// Demonstrates the nautilus-profiling plugin with automatic function
// instrumentation via the ProfilingIRPass. No manual ProfileRegion
// placement needed — the IR pass inserts timing calls at every
// function entry and return in the module.
//
// The module contains three separate functions:
// 1. scale — bulk array transform via a native call
// 2. hashAll — per-element hash in a traced loop
// 3. reduce — array reduction via a native call
//
// Each is compiled, auto-instrumented, and profiled independently.
//
// Run:
// ./demo_profiling_plugin
//
// Output:
// - Per-function timing report on stdout
// - Chrome Trace JSON → open in chrome://tracing or Perfetto

#include <cstdint>
#include <filesystem>
#include <iostream>
#include <nautilus/Engine.hpp>
#include <nautilus/function.hpp>
#include <nautilus/profiling/profiler.hpp>

using namespace nautilus;
using namespace nautilus::profiling;

// ============================================================================
// Native functions called via invoke() from the compiled functions.
// These are opaque to the tracer — the system profiler captures their stacks.
// ============================================================================

static void nativeScale(int32_t* data, int32_t size) {
for (int round = 0; round < 50; ++round) {
for (int i = 0; i < size; ++i) {
data[i] = data[i] * 3 - 10;
}
}
}

static int32_t nativeHash(int32_t value) {
uint32_t h = static_cast<uint32_t>(value);
for (int round = 0; round < 50; ++round) {
h ^= h >> 16;
h *= 0x45d9f3b;
h ^= h >> 16;
h *= 0x119de1f3;
}
return static_cast<int32_t>(h);
}

static int64_t nativeReduce(int32_t* data, int32_t size) {
int64_t sum = 0;
for (int round = 0; round < 50; ++round) {
for (int i = 0; i < size; ++i) {
sum += data[i];
}
}
return sum;
}

// ============================================================================
// Three nautilus functions — no manual profiling annotations needed.
// The ProfilingIRPass auto-instruments each at compile time.
// ============================================================================

void scale(val<int32_t*> data, val<int32_t> size) {
invoke(nativeScale, data, size);
}

void hashAll(val<int32_t*> data, val<int32_t> size) {
for (val<int32_t> i = 0; i < size; i = i + 1) {
data[i] = invoke(nativeHash, data[i]);
}
}

val<int64_t> reduce(val<int32_t*> data, val<int32_t> size) {
return invoke(nativeReduce, data, size);
}

// ============================================================================

int main(int, char*[]) {
engine::Options options;
options.setOption("engine.backend", "mlir");
auto engine = engine::NautilusEngine(options);

std::cout << "Backend: " << engine.getNameOfBackend() << "\n\n";

// Install the profiler on the engine so every function the engine compiles
// gets instrumented automatically.
auto profiler = Profiler::enableForEngine(engine);
auto module = engine.createModule();
module.registerFunction<void(val<int32_t*>, val<int32_t>)>("scale", scale);
module.registerFunction<void(val<int32_t*>, val<int32_t>)>("hashAll", hashAll);
module.registerFunction<val<int64_t>(val<int32_t*>, val<int32_t>)>("reduce", reduce);
auto compiled = module.compile();

auto scaleFn = compiled.getFunction<void(int32_t*, int32_t)>("scale");
auto hashFn = compiled.getFunction<void(int32_t*, int32_t)>("hashAll");
auto reduceFn = compiled.getFunction<int64_t(int32_t*, int32_t)>("reduce");

constexpr int N = 1024;
constexpr int ITERS = 10000;
int32_t data[N];

profiler->start();
for (int iter = 0; iter < ITERS; ++iter) {
for (int i = 0; i < N; ++i) {
data[i] = (i * 7 + iter) % 100 - 30;
}
scaleFn(data, N);
hashFn(data, N);
auto result = reduceFn(data, N);
(void) result;
}
profiler->stop();

std::cout << "=================================================================\n";
std::cout << " MODULE PROFILE (" << ITERS << " iterations, N=" << N << ")\n";
std::cout << "=================================================================\n";
profiler->report();

auto outDir = std::filesystem::temp_directory_path() / "nautilus_demo_profiling";
std::filesystem::create_directories(outDir);

auto chromePath = outDir / "flame_chart.json";
profiler->exportChromeTrace(chromePath.string());

std::cout << "\nChrome Trace: file://" << chromePath << "\n";
std::cout << "Tip: open in chrome://tracing or https://ui.perfetto.dev\n";

return 0;
}
44 changes: 33 additions & 11 deletions nautilus/include/nautilus/Engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <any>
#include <functional>
#include <memory>
#include <vector>

#ifdef ENABLE_TRACING
#include "nautilus/CompilableFunction.hpp"
Expand All @@ -20,6 +21,18 @@ class Arena;
class ArenaPool;
} // namespace nautilus::common

namespace nautilus::compiler::ir {
// IRPass is defined in the private header `nautilus/compiler/ir/passes/IRPass.hpp`.
// Plugins (which have access to the src/ tree) include that header to derive from
// it; this forward declaration is enough for the public surface to refer to it.
class IRPass;
} // namespace nautilus::compiler::ir

namespace nautilus {
/// Plugin-facing alias for the compiler's IR pass interface.
using IRPass = compiler::ir::IRPass;
} // namespace nautilus

namespace nautilus::engine {
namespace details {

Expand Down Expand Up @@ -183,6 +196,19 @@ class NautilusEngine {
template <typename R, typename... FunctionArguments>
auto registerFunction(std::function<R(val<FunctionArguments>...)> func) const;

/**
* @brief Register a plugin IR pass that runs during every subsequent
* compilation, alongside the built-in passes.
*
* Forwarded to the underlying `JITCompiler`, which appends the pass to
* the same `IRPassManager` pipeline it constructs internally — plugin
* and built-in passes share dump/verify/statistics handling. Defined
* out-of-line so the public header can keep `IRPass` forward-declared.
*
* No-op when compilation is disabled (interpreter mode).
*/
void addIRPass(std::unique_ptr<nautilus::IRPass> pass) const;

/**
* @brief Creates a new module for registering multiple functions to be compiled together.
* @return NautilusModule builder
Expand Down Expand Up @@ -251,6 +277,7 @@ class NautilusModule {
}
#endif


/**
* @brief Register a function with an explicit signature (needed for lambdas).
* @tparam Signature The val-typed function signature, e.g. val<int32_t>(val<int32_t>)
Expand Down Expand Up @@ -301,22 +328,17 @@ class NautilusModule {
/**
* @brief Compile all registered functions together into one compilation unit.
* When compilation is disabled, returns a module that interprets functions directly.
*
* Plugin IR passes are owned by the `JITCompiler` (via
* @ref NautilusEngine::addIRPass), so the module just delegates the
* compile call.
*
* @return CompiledModule with all functions accessible by name
*/
CompiledModule compile() {
#ifdef ENABLE_TRACING
if (compiled_) {
auto executable = jit_.compile(functions_);
auto module = CompiledModule(std::move(executable), std::move(interpretedFunctions_));

// If using tiered compilation, start background promotion.
// The TieredJITCompiler directly swaps the executable and bumps
// the version in the module state when tier-1 compilation completes.
// if (auto* tiered = dynamic_cast<const compiler::TieredJITCompiler*>(&jit_)) {
// tiered->promoteAsync(module.getState());
//}

return module;
return CompiledModule(jit_.compile(functions_), std::move(interpretedFunctions_));
}
#endif
return CompiledModule(std::move(interpretedFunctions_));
Expand Down
32 changes: 32 additions & 0 deletions nautilus/include/nautilus/JITCompiler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class CompilableFunction;

namespace ir {
class IRGraph;
class IRPass;
}

using CompilationUnitID = std::string;
Expand Down Expand Up @@ -52,6 +53,37 @@ class JITCompiler {
* @brief Get the engine options.
*/
virtual const engine::Options& getOptions() const = 0;

/**
* @brief Trace and convert functions to IR without backend code generation.
*
* Enables callers (e.g. `NautilusModule::compile()` when plugin IR passes
* are registered) to inspect or mutate the IR between the frontend and
* the backend.
*
* @param functions Named compilable functions to trace and convert.
* @return A shared IR graph that can be lowered via @ref compileIR.
*/
[[nodiscard]] virtual std::shared_ptr<ir::IRGraph> compileToIR(std::list<CompilableFunction>& functions) const = 0;

/**
* @brief Lower a pre-built IR graph with the named backend.
*
* Intended to be called after @ref compileToIR has produced an IR
* graph and any plugin IR passes have run.
*/
[[nodiscard]] virtual std::unique_ptr<Executable> compileIR(const std::shared_ptr<ir::IRGraph>& ir,
const std::string& backendName) const = 0;

/**
* @brief Register a plugin IR pass to be applied during every subsequent
* compilation, alongside the built-in passes the compiler runs.
*
* Ownership of the pass moves into the compiler. Implementations append
* it to the same `IRPassManager` pipeline they construct internally, so
* plugin and built-in passes share dump/verify/statistics handling.
*/
virtual void addIRPass(std::unique_ptr<ir::IRPass> pass) = 0;
};

} // namespace nautilus::compiler
11 changes: 11 additions & 0 deletions nautilus/src/nautilus/compiler/Engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "nautilus/common/Arena.hpp"
#include "nautilus/compiler/LegacyCompiler.hpp"
#include "nautilus/compiler/TieredCompiler.hpp"
#include "nautilus/compiler/ir/passes/IRPass.hpp"

namespace nautilus::engine {

Expand Down Expand Up @@ -42,4 +43,14 @@ NautilusEngine::NautilusEngine(std::unique_ptr<compiler::JITCompiler> jit, const
NautilusEngine::~NautilusEngine() = default;
NautilusEngine::NautilusEngine(NautilusEngine&&) noexcept = default;

void NautilusEngine::addIRPass(std::unique_ptr<nautilus::IRPass> pass) const {
#ifdef ENABLE_TRACING
if (pass != nullptr) {
jit_->addIRPass(std::move(pass));
}
#else
(void) pass;
#endif
}

} // namespace nautilus::engine
25 changes: 20 additions & 5 deletions nautilus/src/nautilus/compiler/LegacyCompiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,18 @@ std::shared_ptr<ir::IRGraph> LegacyCompiler::compileToIR(std::list<CompilableFun
ir::createGraphVizFromIr(ir, options, dumpHandler);
}

if (options.getOptionOrDefault("ir.runPasses", true)) {
if (options.getOptionOrDefault("ir.runPasses", true) || !pluginPasses_.empty()) {
ir::IRPassManager passManager(options, &dumpHandler, statistics);
if (!options.getOptionOrDefault("ir.disableConstantFolding", false)) {
passManager.addPass(std::make_unique<ir::ConstantFoldingAndCopyPropagationPass>());
if (options.getOptionOrDefault("ir.runPasses", true)) {
if (!options.getOptionOrDefault("ir.disableConstantFolding", false)) {
passManager.addPass(std::make_shared<ir::ConstantFoldingAndCopyPropagationPass>());
}
if (!options.getOptionOrDefault("ir.disableEmptyBlockElimination", false)) {
passManager.addPass(std::make_shared<ir::EmptyBlockEliminationPass>());
}
}
if (!options.getOptionOrDefault("ir.disableEmptyBlockElimination", false)) {
passManager.addPass(std::make_unique<ir::EmptyBlockEliminationPass>());
for (const auto& pluginPass : pluginPasses_) {
passManager.addPass(pluginPass);
}
passManager.run(*ir);
dumpHandler.dump("after_ir_passes", "ir", [&]() { return ir->toString(); });
Expand Down Expand Up @@ -204,8 +209,18 @@ std::unique_ptr<Executable> LegacyCompiler::compile(std::list<CompilableFunction
return executable;
}

void LegacyCompiler::addIRPass(std::unique_ptr<ir::IRPass> pass) {
if (pass != nullptr) {
pluginPasses_.push_back(std::move(pass));
}
}

#else

void LegacyCompiler::addIRPass(std::unique_ptr<ir::IRPass>) {
throw RuntimeException("Jit not initialised");
}

std::unique_ptr<Executable> LegacyCompiler::compile(JITCompiler::wrapper_function) const {
throw RuntimeException("Jit not initialised");
}
Expand Down
Loading
Loading