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
56 changes: 50 additions & 6 deletions src/server/compiler/compile_graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ void CompileGraph::release(std::uint32_t path_id) {
auto& unit = units.find(path_id)->second;
assert(unit.refcount > 0 && "released more interest than acquired");
unit.refcount -= 1;
if(unit.refcount == 0) {
unit.foreground = false;
}

// No interest left in an in-flight round. The drop is often transient —
// a stale round's edges being re-acquired by the retry that re-resolves
Expand Down Expand Up @@ -189,9 +192,16 @@ kota::task<> CompileGraph::unit_body(std::uint32_t path_id,

// Acquire edge references on all direct dependencies. This must stay
// synchronous: no suspension between refcount++ and guard registration.
// A foreground unit passes its class down: the user waits on the whole
// chain, not just the root — and a dep discovered here may already be
// running with a resolved closure of its own, so the mark must recurse.
auto foreground = unit.foreground;
for(auto dep_id: deps) {
acquire(dep_id);
guard.acquired.push_back(dep_id);
if(foreground) {
mark_foreground(dep_id);
}
Comment thread
16bit-ykiko marked this conversation as resolved.
}

if(!deps.empty()) {
Expand All @@ -208,12 +218,17 @@ kota::task<> CompileGraph::unit_body(std::uint32_t path_id,
}
}

bool ok = co_await dispatch(path_id);
// Re-look up the class: a foreground requester may have joined while
// the dependency waits above were suspended.
auto outcome = co_await dispatch(path_id, units.find(path_id)->second.foreground);

// Synchronous tail: nothing can interleave between dispatch resuming us
// and co_return, so the checks below are atomic.
if(!ok) {
guard.outcome = CompileUnit::Outcome::Failed;
if(outcome != CompileUnit::Outcome::Success) {
// Failed propagates to waiters; Stale (the scheduler preempted the
// build) makes them respawn the round, re-reading the interest
// class — a foreground joiner's retry dispatches at High.
guard.outcome = outcome;
co_return;
}

Expand Down Expand Up @@ -257,20 +272,44 @@ kota::task<bool> CompileGraph::await_unit(std::uint32_t path_id,
case CompileUnit::Outcome::Failed: co_return false;
// The round was cancelled and produced no result; we still hold
// interest, so drive a new round. Each retry consumes one
// staleness event — without further updates this terminates.
// staleness event (an update, or one scheduler preemption of
// the dispatch, whose retry queues for a pool slot rather than
// spinning) — this terminates once the events stop.
case CompileUnit::Outcome::Stale: break;
}
}
}

kota::task<bool> CompileGraph::compile(std::uint32_t path_id) {
void CompileGraph::mark_foreground(std::uint32_t path_id) {
auto& unit = units[path_id];
unit.path_id = path_id;
// Already-marked doubles as the cycle/shared-dep visit guard.
if(unit.foreground) {
return;
}
unit.foreground = true;
// A late join must upgrade the whole resolved closure, not just the
// root: a dependency already spawned at Low re-reads its class when a
// preempted round retries, and without the closure mark that retry
// would stay Low — cancellable by the very foreground waiting on it.
// Copy: recursion inserts units and may rehash the map.
auto deps = unit.dependencies;
for(auto dep_id: deps) {
mark_foreground(dep_id);
}
}

kota::task<bool> CompileGraph::compile(std::uint32_t path_id, bool foreground) {
// Request scope: one root reference, dropped when the requester exits or
// its frame is cancelled.
RefGuard scope(*this, {path_id});
if(foreground) {
mark_foreground(path_id);
}
co_return co_await await_unit(path_id, std::nullopt);
}

kota::task<bool> CompileGraph::compile_deps(std::uint32_t path_id) {
kota::task<bool> CompileGraph::compile_deps(std::uint32_t path_id, bool foreground) {
ensure_resolved(path_id);

// Copy deps — the map may rehash while this frame is suspended.
Expand All @@ -282,6 +321,11 @@ kota::task<bool> CompileGraph::compile_deps(std::uint32_t path_id) {
// Request scope: root references on each direct dependency (path_id
// itself is never dispatched here).
RefGuard scope(*this, deps);
if(foreground) {
for(auto dep_id: deps) {
mark_foreground(dep_id);
}
}

std::vector<kota::task<bool>> waits;
waits.reserve(deps.size());
Expand Down
39 changes: 31 additions & 8 deletions src/server/compiler/compile_graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ namespace clice {

struct CompileUnit {
/// Result of one compilation round, observed by waiters after the
/// round's completion event fires.
/// round's completion event fires. Also the dispatch callback's return
/// type: a dispatch reports Stale when the scheduler preempted its
/// build — no verdict on the unit, waiters retry.
enum class Outcome : std::uint8_t {
/// The round was cancelled (file update or loss of interest) and its
/// result discarded; waiters that still hold interest retry.
/// The round was cancelled (file update, loss of interest, or a
/// scheduler-preempted build) and its result discarded; waiters
/// that still hold interest retry.
Stale,
Success,
/// Dispatch failed or a dependency cycle was detected; waiters
Expand Down Expand Up @@ -51,6 +54,14 @@ struct CompileUnit {
/// compiling cancels this unit's round. Not a lifetime count.
std::uint32_t refcount = 0;

/// A foreground requester holds (or held) interest in this round: the
/// dispatch sends the build at High priority so a waiting user request
/// is not throttled behind background indexing. Sticky while any
/// interest remains — clearing with the foreground requester alone
/// would drop an in-flight retry back to Low mid-wait — and reset when
/// the interest count returns to zero.
bool foreground = false;

/// A zero-interest cancellation check is already queued for this unit.
bool zero_check_pending = false;

Expand Down Expand Up @@ -94,20 +105,28 @@ struct CompileUnit {
/// dependency cycle) propagates to waiters without retry.
class CompileGraph {
public:
/// Performs the actual compilation (e.g. produce PCM file).
using dispatch_fn = std::function<kota::task<bool>(std::uint32_t path_id)>;
/// Performs the actual compilation (e.g. produce PCM file); `foreground`
/// carries the unit's interest class into the build's priority. Stale
/// reports a scheduler-preempted build: the round ends without a
/// verdict and waiters respawn it — a foreground joiner's retry then
/// re-dispatches at High instead of surfacing the preemption as a
/// failure.
using dispatch_fn =
std::function<kota::task<CompileUnit::Outcome>(std::uint32_t path_id, bool foreground)>;

/// Returns the dependency path_ids for a given path_id (called lazily on first compile).
using resolve_fn = std::function<llvm::SmallVector<std::uint32_t>(std::uint32_t path_id)>;

CompileGraph(kota::event_loop& loop, dispatch_fn dispatch, resolve_fn resolve);

/// Compile a unit and all its transitive dependencies.
kota::task<bool> compile(std::uint32_t path_id);
/// Compile a unit and all its transitive dependencies. `foreground`
/// marks the chain's dispatches High-priority (a user request waits on
/// them); background callers leave it unset.
kota::task<bool> compile(std::uint32_t path_id, bool foreground = false);

/// Compile all transitive module dependencies of path_id, but NOT path_id itself.
/// Used for non-module files (plain .cpp) that import modules.
kota::task<bool> compile_deps(std::uint32_t path_id);
kota::task<bool> compile_deps(std::uint32_t path_id, bool foreground = false);

/// Mark path_id and all transitive dependents as dirty,
/// cancelling any in-progress compilations (their results are stale).
Expand Down Expand Up @@ -150,6 +169,10 @@ class CompileGraph {
/// Interest +1; creates the unit if needed.
void acquire(std::uint32_t path_id);

/// Mark a unit and its resolved dependency closure foreground; units
/// resolved later inherit through the edge propagation in unit_body.
void mark_foreground(std::uint32_t path_id);

/// Interest -1; schedules a zero-interest cancellation check when it
/// drops to zero mid-compile.
void release(std::uint32_t path_id);
Expand Down
33 changes: 23 additions & 10 deletions src/server/compiler/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,11 @@ void Compiler::init_compile_graph() {
};

// Dispatch: sends BuildPCM request to a stateless worker.
auto dispatch = [this](std::uint32_t path_id) -> kota::task<bool> {
using Outcome = CompileUnit::Outcome;
auto dispatch = [this](std::uint32_t path_id, bool foreground) -> kota::task<Outcome> {
auto mod_it = workspace.path_to_module.find(path_id);
if(mod_it == workspace.path_to_module.end())
co_return false;
co_return Outcome::Failed;

// Copy out of the map before any suspension below: while a PCM build
// is awaited, a concurrent didSave can insert into (or erase from)
Expand All @@ -282,13 +283,14 @@ void Compiler::init_compile_graph() {
auto file_path = std::string(workspace.path_pool.resolve(path_id));

worker::BuildParams bp;
bp.priority = foreground ? worker::Priority::High : worker::Priority::Low;
bp.kind = worker::BuildKind::BuildPCM;
bp.file = file_path;
contexts.resolve_command(file_path, bp.directory, bp.arguments);

if(!workspace.store) {
LOG_WARN("BuildPCM skipped for module {}: cache store is unavailable", module_name);
co_return false;
co_return Outcome::Failed;
}

// Deterministic content-addressed PCM key over the source path and
Expand All @@ -314,7 +316,7 @@ void Compiler::init_compile_graph() {
} else {
workspace.pcm_paths[path_id] = pcm_it->second.path;
LOG_PERF("cache", "ns=pcm event=hit key={} module={}", pcm_key, module_name);
co_return true;
co_return Outcome::Success;
}
}
LOG_PERF("cache",
Expand All @@ -335,7 +337,7 @@ void Compiler::init_compile_graph() {
LOG_WARN("PCM build for module {} refused: key {} keeps crashing workers",
module_name,
budget_key);
co_return false;
co_return Outcome::Failed;
}

bp.module_name = module_name;
Expand All @@ -353,6 +355,13 @@ void Compiler::init_compile_graph() {
[this, &budget_key](const kota::ipc::protocol::Error&) {
workspace.build_crashes.on_crash(budget_key);
});
// A scheduler preemption (foreground reclaim, memory pressure) is
// no verdict on the unit: report the round stale so waiters drive
// a retry instead of failing their whole chain.
if(!result.has_value() && result.error().code == worker::dispatch_errc::cancelled) {
LOG_INFO("BuildPCM preempted for module {}, will retry", module_name);
co_return Outcome::Stale;
}
if(!result.has_value() || !result.value().success) {
if(expected_build_failure(result)) {
LOG_WARN("BuildPCM failed for module {}: {}",
Expand All @@ -364,15 +373,15 @@ void Compiler::init_compile_graph() {
module_name,
build_failure_message(result));
}
co_return false;
co_return Outcome::Failed;
}

// Commit on the thread pool: it fsyncs the freshly written PCM.
auto committed =
co_await kota::queue([&] { return workspace.store->commit(std::move(pending)); });
if(!committed.has_value() || !committed.value().has_value()) {
LOG_WARN("Failed to commit PCM for module {}", module_name);
co_return false;
co_return Outcome::Failed;
}

workspace.build_crashes.on_land(budget_key);
Expand All @@ -392,7 +401,7 @@ void Compiler::init_compile_graph() {
if(on_indexing_needed)
on_indexing_needed();

co_return true;
co_return Outcome::Success;
};

workspace.compile_graph =
Expand Down Expand Up @@ -716,10 +725,14 @@ kota::task<bool> Compiler::ensure_deps(Session& session,
// scope unwinds the wait and releases this request's interest in the
// dependency graph, without touching the shared compilations themselves.
auto compile_deps = [&](std::uint32_t pid) -> kota::task<bool> {
// A user request waits on these builds: dispatch them High so the
// background budget cannot throttle its own foreground.
if(!scope) {
co_return co_await workspace.compile_graph->compile_deps(pid);
co_return co_await workspace.compile_graph->compile_deps(pid, /*foreground=*/true);
}
auto result = co_await kota::with_token(workspace.compile_graph->compile_deps(pid), *scope);
auto result = co_await kota::with_token(
workspace.compile_graph->compile_deps(pid, /*foreground=*/true),
*scope);
co_return result.has_value() && *result;
};

Expand Down
Loading
Loading