Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
64 changes: 55 additions & 9 deletions src/server/compiler/indexer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,12 @@ void Indexer::load() {
workspace.merged_indices[path_id] = std::move(shard);
expected_keys.insert(key);
} else {
// No shard survives, so there is nothing stale to keep
// serving; ContentChanged states the truth ("the index
// does not describe this file") without effect.
LOG_INFO("Discarding unreadable shard for {}",
workspace.path_pool.resolve(path_id));
enqueue(path_id);
enqueue(path_id, ReindexReason::ContentChanged);
}
}
} else {
Expand Down Expand Up @@ -332,7 +335,19 @@ bool Indexer::need_update(llvm::StringRef file_path) {
return merged_it->second.need_update();
}

void Indexer::enqueue(std::uint32_t server_path_id) {
void Indexer::enqueue(std::uint32_t server_path_id, ReindexReason reason) {
// Record (or refresh) why the file is pending. ContentChanged is
// absorbing: a deps-only cascade cannot downgrade a file whose own
// content already changed. The fresh ticket invalidates the clear of
// any index task already in flight for this file.
auto [it, inserted] = reindex_reasons.try_emplace(server_path_id, reason, ++reindex_ticket);
if(!inserted) {
if(reason == ReindexReason::ContentChanged) {
it->second.reason = ReindexReason::ContentChanged;
}
it->second.ticket = reindex_ticket;
Comment thread
16bit-ykiko marked this conversation as resolved.
Resolved
}

// Already queued and not yet consumed — a second entry would only be
// skipped by need_update later; drop it here.
if(!pending_ids.insert(server_path_id).second)
Expand Down Expand Up @@ -435,6 +450,26 @@ kota::task<> Indexer::index_one(std::uint32_t server_path_id,
}
}

kota::task<> Indexer::run_index_task(std::uint32_t server_path_id,
std::uint64_t ticket,
std::size_t index,
std::size_t total,
std::size_t& completed) {
co_await index_one(server_path_id, index, total);
// The pending window ends with the index attempt, success or not (a
// failed attempt is re-detected by the hash gate on the next round).
// A re-enqueue during the flight bumped the ticket: that newer pending
// state must survive this clear.
if(auto it = reindex_reasons.find(server_path_id);
it != reindex_reasons.end() && it->second.ticket == ticket) {
reindex_reasons.erase(it);
Comment thread
16bit-ykiko marked this conversation as resolved.
Resolved
}
Comment thread
16bit-ykiko marked this conversation as resolved.
Resolved
++completed;
progress_data.stage = Progress::Stage::Report;
progress_data.completed = completed;
on_progress_changed.emit();
}

kota::task<> Indexer::run_background_indexing() {
if(index_idle_timer) {
co_await index_idle_timer->wait();
Expand Down Expand Up @@ -478,18 +513,28 @@ kota::task<> Indexer::run_background_indexing() {
pending_ids.erase(server_path_id);
auto file_path = std::string(workspace.path_pool.resolve(server_path_id));
if(sessions.find(server_path_id) != nullptr || !need_update(file_path)) {
// Not pending anymore: an open session's buffer index is
// authoritative, and a hash-fresh shard already describes the
// current content.
reindex_reasons.erase(server_path_id);
Comment thread
16bit-ykiko marked this conversation as resolved.
Resolved
Comment thread
16bit-ykiko marked this conversation as resolved.
Resolved
++completed;
continue;
}

++dispatched;
workers.spawn([&, server_path_id, n = dispatched]() -> kota::task<> {
co_await index_one(server_path_id, n, total);
++completed;
progress_data.stage = Progress::Stage::Report;
progress_data.completed = completed;
on_progress_changed.emit();
}());
// Invariant: a queued file always has a pending entry (enqueue
// writes it before the queue push, and nothing erases it while it
// sits in the un-consumed tail). Ticket 0 is never issued, so if
// the invariant ever broke in a release build the task would run
// normally and simply leave no entry to clear.
auto pending_it = reindex_reasons.find(server_path_id);
assert(pending_it != reindex_reasons.end() && "queued file must have a pending reason");
Comment thread
16bit-ykiko marked this conversation as resolved.
Resolved
auto ticket = pending_it != reindex_reasons.end() ? pending_it->second.ticket : 0;
// A member coroutine, not an immediately-invoked capturing lambda:
// a lambda's captures live in the lambda object, which dies at the
// end of this statement — anything read after the first suspension
// would dangle. Coroutine parameters are copied into the frame.
workers.spawn(run_index_task(server_path_id, ticket, dispatched, total, completed));
}

LOG_DEBUG("Background indexing: all {} tasks spawned, waiting for completion", dispatched);
Expand All @@ -509,6 +554,7 @@ kota::task<> Indexer::run_background_indexing() {
// the next scheduled round.
if(index_queue_pos >= index_queue.size()) {
assert(pending_ids.empty() && "drained queue must have no pending ids");
assert(reindex_reasons.empty() && "drained queue must have no pending reasons");
Comment thread
16bit-ykiko marked this conversation as resolved.
Resolved
index_queue.clear();
index_queue_pos = 0;
}
Comment thread
16bit-ykiko marked this conversation as resolved.
Resolved
Expand Down
60 changes: 58 additions & 2 deletions src/server/compiler/indexer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <vector>

#include "server/state/workspace.h"
#include "support/signal.h"

#include "kota/async/async.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/StringRef.h"

Expand All @@ -18,6 +20,22 @@ class ContextResolver;
class WorkerPool;
struct SessionStore;

/// Why a file awaits re-indexing. The invalidation engine knows the cause
/// at enqueue time, so queries can decide in O(1) whether a pending file's
/// existing index rows are still trustworthy (see IndexQuery's freshness
/// contract).
enum class ReindexReason : std::uint8_t {
/// Enqueued by a dependency cascade (or a bulk sweep of unknown
/// staleness): the file's own content is not known to have changed, so
/// its index rows are positionally intact — at worst semantically
/// behind — and keep serving until the reindex lands.
DepsOnly,
/// The file's own content changed: its index rows describe text that
/// no longer exists, so queries skip this file's contribution until
/// the reindex lands.
ContentChanged,
};

/// Background indexing scheduler.
///
/// Indexer owns the indexing queue and drives disk files through
Expand Down Expand Up @@ -71,8 +89,22 @@ class Indexer {
return ScopedPause{*this};
}

/// Add a file to the background indexing queue.
void enqueue(std::uint32_t server_path_id);
/// Add a file to the background indexing queue. A file enqueued twice
/// keeps a single queue entry; its reason is upgraded to ContentChanged
/// if either enqueue says so (a file both cascaded onto and edited is
/// as stale as the edit makes it).
void enqueue(std::uint32_t server_path_id, ReindexReason reason);

/// Why the file awaits re-indexing (queued or currently being indexed),
/// or nullopt when its index is not pending an update. O(1), no I/O —
/// the query path calls this per candidate file.
std::optional<ReindexReason> pending_reason(std::uint32_t server_path_id) const {
auto it = reindex_reasons.find(server_path_id);
if(it == reindex_reasons.end()) {
return std::nullopt;
}
return it->second.reason;
}

/// Schedule background indexing (respects idle timeout and dedup).
void schedule();
Expand Down Expand Up @@ -147,6 +179,20 @@ class Indexer {
std::vector<std::uint32_t> index_queue;
llvm::DenseSet<std::uint32_t> pending_ids;
std::size_t index_queue_pos = 0;

/// The reason each file awaits re-indexing, kept from enqueue until the
/// file is skipped (open session, fresh shard) or its index task ends —
/// unlike pending_ids, which only covers the un-consumed queue tail.
/// The ticket guards the clear: a file re-enqueued while its previous
/// index task is still in flight bumps the ticket, so the older task's
/// completion must not erase the newer pending state.
struct PendingReindex {
ReindexReason reason;
std::uint64_t ticket;
};

llvm::DenseMap<std::uint32_t, PendingReindex> reindex_reasons;
std::uint64_t reindex_ticket = 0;
bool indexing_active = false;
bool indexing_scheduled = false;
std::shared_ptr<kota::timer> index_idle_timer;
Expand All @@ -160,6 +206,16 @@ class Indexer {

kota::task<> run_background_indexing();
kota::task<> index_one(std::uint32_t server_path_id, std::size_t index, std::size_t total);

/// One dispatched unit of a background round: index the file, then end
/// its pending window (ticket-guarded) and report progress. `completed`
/// refers into run_background_indexing's frame, which outlives every
/// spawned task (it joins them before returning).
kota::task<> run_index_task(std::uint32_t server_path_id,
std::uint64_t ticket,
std::size_t index,
std::size_t total,
std::size_t& completed);
};

} // namespace clice
35 changes: 32 additions & 3 deletions src/server/service/feature_router.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ static kota::ipc::Error item_not_resolved(llvm::StringRef kind) {
std::format("Failed to resolve {} item", kind)};
}

kota::task<> FeatureRouter::settle_cursor_file(std::shared_ptr<Session> session) {
if(session) {
co_await compiler.ensure_compiled(std::move(session));
Comment thread
16bit-ykiko marked this conversation as resolved.
Resolved
}
}

const std::vector<feature::DocumentLink>*
FeatureRouter::find_preamble_links(const Session& session) {
if(!session.pch_ref)
Expand Down Expand Up @@ -101,10 +107,13 @@ kota::task<kota::codec::RawValue, kota::ipc::Error>
FeatureRouter::definition(std::shared_ptr<Session> session,
llvm::StringRef path,
const protocol::Position& pos) {
co_await settle_cursor_file(session);

// Preamble include lines first: they have no symbol occurrence in
// the index and are invisible to the worker's AST. Dirty sessions
// skip this — the cached links may describe the pre-edit preamble —
// and retry below once the worker compile refreshed the PCH.
// the index and are invisible to the worker's AST. A session still
// dirty after the settle (failed or superseded compile) skips this —
// the cached links may describe the pre-edit preamble — and retries
// below once the worker compile refreshed the PCH.
if(session && !session->ast_dirty) {
if(auto directive = resolve_directive_definition(*session, pos); !directive.empty()) {
co_return to_raw(directive);
Expand Down Expand Up @@ -254,6 +263,8 @@ FeatureRouter::RawResult FeatureRouter::references(std::shared_ptr<Session> sess
llvm::StringRef path,
const protocol::Position& position,
bool include_declaration) {
co_await settle_cursor_file(session);

auto locations =
index_query.query_relations(path, position, RelationKind::Reference, session.get());

Expand All @@ -275,6 +286,8 @@ FeatureRouter::RawResult FeatureRouter::references(std::shared_ptr<Session> sess
FeatureRouter::RawResult FeatureRouter::declaration(std::shared_ptr<Session> session,
llvm::StringRef path,
const protocol::Position& position) {
co_await settle_cursor_file(session);

auto locations =
index_query.query_relations(path, position, RelationKind::Declaration, session.get());
auto defs =
Expand All @@ -288,6 +301,8 @@ FeatureRouter::RawResult FeatureRouter::declaration(std::shared_ptr<Session> ses
FeatureRouter::RawResult FeatureRouter::type_definition(std::shared_ptr<Session> session,
llvm::StringRef path,
const protocol::Position& position) {
co_await settle_cursor_file(session);

co_return to_raw(index_query.query_symbol_targets(path,
position,
RelationKind::TypeDefinition,
Expand All @@ -297,6 +312,8 @@ FeatureRouter::RawResult FeatureRouter::type_definition(std::shared_ptr<Session>
FeatureRouter::RawResult FeatureRouter::implementation(std::shared_ptr<Session> session,
llvm::StringRef path,
const protocol::Position& position) {
co_await settle_cursor_file(session);

co_return to_raw(index_query.query_symbol_targets(path,
position,
RelationKind::Implementation,
Expand All @@ -307,6 +324,8 @@ FeatureRouter::RawResult FeatureRouter::call_hierarchy_prepare(std::shared_ptr<S
const std::string& uri,
llvm::StringRef path,
const protocol::Position& position) {
co_await settle_cursor_file(session);

auto info = index_query.lookup_symbol(uri, path, position, session.get());
if(!info)
co_return serde_raw{"null"};
Expand All @@ -322,6 +341,8 @@ FeatureRouter::RawResult
FeatureRouter::call_hierarchy_incoming(std::shared_ptr<Session> session,
llvm::StringRef path,
const protocol::CallHierarchyItem& item) {
co_await settle_cursor_file(session);

auto info =
index_query.resolve_hierarchy_item(item.uri, path, item.range, item.data, session.get());
if(!info)
Expand All @@ -334,6 +355,8 @@ FeatureRouter::RawResult
FeatureRouter::call_hierarchy_outgoing(std::shared_ptr<Session> session,
llvm::StringRef path,
const protocol::CallHierarchyItem& item) {
co_await settle_cursor_file(session);

auto info =
index_query.resolve_hierarchy_item(item.uri, path, item.range, item.data, session.get());
if(!info)
Expand All @@ -346,6 +369,8 @@ FeatureRouter::RawResult FeatureRouter::type_hierarchy_prepare(std::shared_ptr<S
const std::string& uri,
llvm::StringRef path,
const protocol::Position& position) {
co_await settle_cursor_file(session);

auto info = index_query.lookup_symbol(uri, path, position, session.get());
if(!info)
co_return serde_raw{"null"};
Expand All @@ -362,6 +387,8 @@ FeatureRouter::RawResult
FeatureRouter::type_hierarchy_supertypes(std::shared_ptr<Session> session,
llvm::StringRef path,
const protocol::TypeHierarchyItem& item) {
co_await settle_cursor_file(session);

auto info =
index_query.resolve_hierarchy_item(item.uri, path, item.range, item.data, session.get());
if(!info)
Expand All @@ -374,6 +401,8 @@ FeatureRouter::RawResult
FeatureRouter::type_hierarchy_subtypes(std::shared_ptr<Session> session,
llvm::StringRef path,
const protocol::TypeHierarchyItem& item) {
co_await settle_cursor_file(session);

auto info =
index_query.resolve_hierarchy_item(item.uri, path, item.range, item.data, session.get());
if(!info)
Expand Down
8 changes: 8 additions & 0 deletions src/server/service/feature_router.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ class FeatureRouter {
RawResult workspace_symbol(llvm::StringRef query);

private:
/// Cursor-resolution freshness (IndexQuery's contract, clause 1): an
/// index query that resolves a cursor in an open file waits for the
/// file's compile first, exactly like every AST-backed request does —
/// same await, no timeout. A failed or superseded compile is not an
/// error here: the query layer falls back per its contract. No-op for
/// closed documents (null session).
kota::task<> settle_cursor_file(std::shared_ptr<Session> session);

/// The preamble include links of a session's active PCH, or nullptr.
const std::vector<feature::DocumentLink>* find_preamble_links(const Session& session);

Expand Down
Loading
Loading