[llvm] [llvm-advisor] Add tool foundation (PR #207625)

Miguel Cárdenas via llvm-commits llvm-commits at lists.llvm.org
Sun Jul 5 17:12:37 PDT 2026


https://github.com/miguelcsx created https://github.com/llvm/llvm-project/pull/207625

Split from llvm/llvm-project#147451.

This patch adds the llvm-advisor foundation: the tool target, shared types, core records, and common utilities.

>From e7fcea9fdd1409a362366ee1652bf825f3c3f3d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Miguel=20C=C3=A1rdenas?= <miguelecsx at gmail.com>
Date: Tue, 28 Apr 2026 22:53:17 -0500
Subject: [PATCH] [llvm-advisor] Add tool foundation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Miguel Cárdenas <miguelecsx at gmail.com>
---
 llvm/tools/llvm-advisor/CMakeLists.txt        |   8 +
 llvm/tools/llvm-advisor/src/AdvisorCommon.h   |  30 +++
 llvm/tools/llvm-advisor/src/CMakeLists.txt    |  19 ++
 .../llvm-advisor/src/Core/AdvisorTypes.cpp    | 253 ++++++++++++++++++
 .../llvm-advisor/src/Core/AdvisorTypes.h      |  92 +++++++
 .../llvm-advisor/src/Core/JobManager.cpp      | 104 +++++++
 llvm/tools/llvm-advisor/src/Core/JobManager.h |  59 ++++
 .../llvm-advisor/src/Core/SnapshotManager.h   |  35 +++
 .../llvm-advisor/src/Core/UnitIdentity.cpp    |  23 ++
 .../llvm-advisor/src/Core/UnitIdentity.h      |  26 ++
 llvm/tools/llvm-advisor/src/Utils/Hashing.cpp |  81 ++++++
 llvm/tools/llvm-advisor/src/Utils/Hashing.h   |  43 +++
 llvm/tools/llvm-advisor/src/Utils/JSON.cpp    | 100 +++++++
 llvm/tools/llvm-advisor/src/Utils/JSON.h      |  45 ++++
 llvm/tools/llvm-advisor/src/Utils/Logging.cpp |  36 +++
 llvm/tools/llvm-advisor/src/Utils/Logging.h   |  38 +++
 llvm/tools/llvm-advisor/src/Utils/Metrics.cpp |  35 +++
 llvm/tools/llvm-advisor/src/Utils/Metrics.h   |  38 +++
 .../llvm-advisor/src/Utils/Normalization.cpp  | 111 ++++++++
 .../llvm-advisor/src/Utils/Normalization.h    |  43 +++
 .../llvm-advisor/src/Utils/ProcessRunner.cpp  | 125 +++++++++
 .../llvm-advisor/src/Utils/ProcessRunner.h    |  64 +++++
 .../llvm-advisor/src/Utils/Redaction.cpp      |  58 ++++
 llvm/tools/llvm-advisor/src/Utils/Redaction.h |  26 ++
 llvm/tools/llvm-advisor/src/llvm-advisor.cpp  |  27 ++
 25 files changed, 1519 insertions(+)
 create mode 100644 llvm/tools/llvm-advisor/CMakeLists.txt
 create mode 100644 llvm/tools/llvm-advisor/src/AdvisorCommon.h
 create mode 100644 llvm/tools/llvm-advisor/src/CMakeLists.txt
 create mode 100644 llvm/tools/llvm-advisor/src/Core/AdvisorTypes.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Core/AdvisorTypes.h
 create mode 100644 llvm/tools/llvm-advisor/src/Core/JobManager.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Core/JobManager.h
 create mode 100644 llvm/tools/llvm-advisor/src/Core/SnapshotManager.h
 create mode 100644 llvm/tools/llvm-advisor/src/Core/UnitIdentity.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Core/UnitIdentity.h
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Hashing.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Hashing.h
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/JSON.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/JSON.h
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Logging.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Logging.h
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Metrics.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Metrics.h
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Normalization.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Normalization.h
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/ProcessRunner.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/ProcessRunner.h
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Redaction.cpp
 create mode 100644 llvm/tools/llvm-advisor/src/Utils/Redaction.h
 create mode 100644 llvm/tools/llvm-advisor/src/llvm-advisor.cpp

diff --git a/llvm/tools/llvm-advisor/CMakeLists.txt b/llvm/tools/llvm-advisor/CMakeLists.txt
new file mode 100644
index 0000000000000..b8016f723430f
--- /dev/null
+++ b/llvm/tools/llvm-advisor/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(LLVM_TOOL_LLVM_ADVISOR_BUILD_DEFAULT ON)
+set(LLVM_REQUIRE_EXE_NAMES llvm-advisor)
+
+add_subdirectory(src)
+
+install(TARGETS llvm-advisor
+  RUNTIME DESTINATION bin
+  COMPONENT llvm-advisor)
diff --git a/llvm/tools/llvm-advisor/src/AdvisorCommon.h b/llvm/tools/llvm-advisor/src/AdvisorCommon.h
new file mode 100644
index 0000000000000..6f1e478637bb0
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/AdvisorCommon.h
@@ -0,0 +1,30 @@
+//===------------------- AdvisorCommon.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Common LLVM headers used throughout llvm-advisor. Included by every advisor
+// header so individual files do not need to repeat the boilerplate.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/StringSet.h"
+#include "llvm/ADT/Twine.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Support/raw_ostream.h"
+
+#include <cstdint>
+#include <memory>
+#include <string>
diff --git a/llvm/tools/llvm-advisor/src/CMakeLists.txt b/llvm/tools/llvm-advisor/src/CMakeLists.txt
new file mode 100644
index 0000000000000..6902bdb0dafff
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/CMakeLists.txt
@@ -0,0 +1,19 @@
+set(LLVM_LINK_COMPONENTS
+  Support
+)
+
+file(GLOB_RECURSE LLVM_ADVISOR_SOURCES CONFIGURE_DEPENDS
+  ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
+)
+
+if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/Storage/StorageManager.h)
+  list(FILTER LLVM_ADVISOR_SOURCES EXCLUDE REGEX ".*/Core/JobManager\\.cpp$")
+endif()
+
+add_llvm_tool(llvm-advisor
+  ${LLVM_ADVISOR_SOURCES}
+)
+
+target_include_directories(llvm-advisor PRIVATE
+  ${CMAKE_CURRENT_SOURCE_DIR}
+)
diff --git a/llvm/tools/llvm-advisor/src/Core/AdvisorTypes.cpp b/llvm/tools/llvm-advisor/src/Core/AdvisorTypes.cpp
new file mode 100644
index 0000000000000..236859af07653
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Core/AdvisorTypes.cpp
@@ -0,0 +1,253 @@
+//===------------------- AdvisorTypes.cpp - LLVM Advisor
+//-------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Data types for compilation commands, snapshots, and unit records.
+// Used throughout the system for representing build entities.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Core/AdvisorTypes.h"
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+static Expected<std::string> readString(const json::Object &Object,
+                                        StringRef Key) {
+  std::optional<StringRef> Value = Object.getString(Key);
+  if (!Value)
+    return createStringError(inconvertibleErrorCode(), "missing '%s'",
+                             Key.str().c_str());
+  return Value->str();
+}
+
+static json::Array stringsToJSON(ArrayRef<std::string> Values) {
+  json::Array Out;
+  for (const std::string &Value : Values)
+    Out.push_back(Value);
+  return Out;
+}
+
+static SmallVector<std::string, 16> stringsFromJSON(const json::Object &Object,
+                                                    StringRef Key) {
+  SmallVector<std::string, 16> Out;
+  const json::Array *Array = Object.getArray(Key);
+  if (!Array)
+    return Out;
+  for (const json::Value &Value : *Array) {
+    if (std::optional<StringRef> String = Value.getAsString())
+      Out.push_back(String->str());
+    // Non-string elements are silently skipped (lenient parsing).
+  }
+  return Out;
+}
+
+static StringRef jobStateToString(JobRecord::State S) {
+  switch (S) {
+  case JobRecord::Queued:
+    return "queued";
+  case JobRecord::Running:
+    return "running";
+  case JobRecord::Succeeded:
+    return "succeeded";
+  case JobRecord::Failed:
+    return "failed";
+  case JobRecord::Cancelled:
+    return "cancelled";
+  }
+  return "queued"; // Unreachable.
+}
+
+static Expected<JobRecord::State> jobStateFromString(StringRef State) {
+  if (State == "queued")
+    return JobRecord::Queued;
+  if (State == "running")
+    return JobRecord::Running;
+  if (State == "succeeded")
+    return JobRecord::Succeeded;
+  if (State == "failed")
+    return JobRecord::Failed;
+  if (State == "cancelled")
+    return JobRecord::Cancelled;
+  return createStringError(inconvertibleErrorCode(), "unknown job state '%s'",
+                           State.str().c_str());
+}
+
+json::Value llvm::advisor::toJSON(const CompileCommand &Command) {
+  return json::Object{{"directory", Command.Directory},
+                      {"file", Command.File},
+                      {"arguments", stringsToJSON(Command.Arguments)}};
+}
+
+json::Value llvm::advisor::toJSON(const SnapshotRecord &Snapshot) {
+  return json::Object{
+      {"id", Snapshot.ID},
+      {"source_root", Snapshot.SourceRoot},
+      {"build_root", Snapshot.BuildRoot},
+      {"parent_id", Snapshot.ParentID},
+      {"created_unix", static_cast<int64_t>(Snapshot.CreatedUnix)}};
+}
+
+json::Value llvm::advisor::toJSON(const UnitRecord &Unit) {
+  return json::Object{{"id", Unit.ID},
+                      {"snapshot_id", Unit.SnapshotID},
+                      {"source_path", Unit.SourcePath},
+                      {"directory", Unit.Directory},
+                      {"object_path", Unit.ObjectPath},
+                      {"ir_path", Unit.IRPath},
+                      {"remarks_path", Unit.RemarksPath},
+                      {"language", Unit.Language},
+                      {"target_triple", Unit.TargetTriple},
+                      {"toolchain_version", Unit.ToolchainVersion},
+                      {"source_content_hash", Unit.SourceContentHash},
+                      {"command_fingerprint", Unit.CommandFingerprint},
+                      {"arguments", stringsToJSON(Unit.Arguments)}};
+}
+
+json::Value llvm::advisor::toJSON(const JobRecord &Job) {
+  return json::Object{{"id", Job.ID},
+                      {"state", jobStateToString(Job.Current)},
+                      {"message", Job.Message}};
+}
+
+json::Value llvm::advisor::toJSON(const EntityRecord &Entity) {
+  // Entities are polymorphic: the kind is stored both as `entity_kind`
+  // (canonical) and `kind` (for backward compatibility with older clients).
+  json::Object Object = Entity.Data;
+  Object["entity_kind"] = Entity.Kind;
+  if (!Object.get("kind"))
+    Object["kind"] = Entity.Kind;
+  Object["id"] = Entity.ID;
+  Object["snapshot_id"] = Entity.SnapshotID;
+  Object["unit_id"] = Entity.UnitID;
+  Object["owner_id"] = Entity.OwnerID;
+  return Object;
+}
+
+json::Value llvm::advisor::toJSON(const HealthStatus &Health) {
+  return json::Object{{"ok", Health.OK},
+                      {"store", Health.Store},
+                      {"snapshots", static_cast<int64_t>(Health.Snapshots)},
+                      {"units", static_cast<int64_t>(Health.Units)}};
+}
+
+Expected<SnapshotRecord>
+llvm::advisor::snapshotFromJSON(const json::Value &Value) {
+  const json::Object *Object = Value.getAsObject();
+  if (!Object)
+    return createStringError(inconvertibleErrorCode(),
+                             "snapshot is not an object");
+  SnapshotRecord Snapshot;
+  if (Expected<std::string> ID = readString(*Object, "id"))
+    Snapshot.ID = *ID;
+  else
+    return ID.takeError();
+  if (Expected<std::string> SourceRoot = readString(*Object, "source_root"))
+    Snapshot.SourceRoot = *SourceRoot;
+  else
+    return SourceRoot.takeError();
+  if (Expected<std::string> BuildRoot = readString(*Object, "build_root"))
+    Snapshot.BuildRoot = *BuildRoot;
+  else
+    return BuildRoot.takeError();
+  if (std::optional<StringRef> ParentID = Object->getString("parent_id"))
+    Snapshot.ParentID = ParentID->str();
+  if (std::optional<int64_t> Created = Object->getInteger("created_unix"))
+    Snapshot.CreatedUnix = static_cast<uint64_t>(*Created);
+  return Snapshot;
+}
+
+Expected<UnitRecord> llvm::advisor::unitFromJSON(const json::Value &Value) {
+  const json::Object *Object = Value.getAsObject();
+  if (!Object)
+    return createStringError(inconvertibleErrorCode(), "unit is not an object");
+  UnitRecord Unit;
+  if (Expected<std::string> ID = readString(*Object, "id"))
+    Unit.ID = *ID;
+  else
+    return ID.takeError();
+  if (Expected<std::string> SnapshotID = readString(*Object, "snapshot_id"))
+    Unit.SnapshotID = *SnapshotID;
+  else
+    return SnapshotID.takeError();
+  if (Expected<std::string> SourcePath = readString(*Object, "source_path"))
+    Unit.SourcePath = *SourcePath;
+  else
+    return SourcePath.takeError();
+  if (std::optional<StringRef> Directory = Object->getString("directory"))
+    Unit.Directory = Directory->str();
+  if (std::optional<StringRef> ObjectPath = Object->getString("object_path"))
+    Unit.ObjectPath = ObjectPath->str();
+  if (std::optional<StringRef> IRPath = Object->getString("ir_path"))
+    Unit.IRPath = IRPath->str();
+  if (std::optional<StringRef> RemarksPath = Object->getString("remarks_path"))
+    Unit.RemarksPath = RemarksPath->str();
+  if (std::optional<StringRef> Language = Object->getString("language"))
+    Unit.Language = Language->str();
+  if (std::optional<StringRef> TargetTriple =
+          Object->getString("target_triple"))
+    Unit.TargetTriple = TargetTriple->str();
+  if (std::optional<StringRef> ToolchainVersion =
+          Object->getString("toolchain_version"))
+    Unit.ToolchainVersion = ToolchainVersion->str();
+  if (std::optional<StringRef> SourceContentHash =
+          Object->getString("source_content_hash"))
+    Unit.SourceContentHash = SourceContentHash->str();
+  if (std::optional<StringRef> CommandFingerprint =
+          Object->getString("command_fingerprint"))
+    Unit.CommandFingerprint = CommandFingerprint->str();
+  Unit.Arguments = stringsFromJSON(*Object, "arguments");
+  return Unit;
+}
+
+Expected<JobRecord> llvm::advisor::jobFromJSON(const json::Value &Value) {
+  const json::Object *Object = Value.getAsObject();
+  if (!Object)
+    return createStringError(inconvertibleErrorCode(), "job is not an object");
+  JobRecord Job;
+  if (Expected<std::string> ID = readString(*Object, "id"))
+    Job.ID = *ID;
+  else
+    return ID.takeError();
+  if (std::optional<StringRef> Message = Object->getString("message"))
+    Job.Message = Message->str();
+  if (std::optional<StringRef> State = Object->getString("state")) {
+    if (Expected<JobRecord::State> S = jobStateFromString(*State))
+      Job.Current = *S;
+    else
+      return S.takeError();
+  }
+  return Job;
+}
+
+Expected<EntityRecord> llvm::advisor::entityFromJSON(const json::Value &Value) {
+  const json::Object *Object = Value.getAsObject();
+  if (!Object)
+    return createStringError(inconvertibleErrorCode(),
+                             "entity is not an object");
+
+  EntityRecord Entity;
+  if (std::optional<StringRef> Kind = Object->getString("entity_kind"))
+    Entity.Kind = Kind->str();
+  else if (Expected<std::string> Kind = readString(*Object, "kind"))
+    Entity.Kind = *Kind;
+  else
+    return Kind.takeError();
+  if (Expected<std::string> ID = readString(*Object, "id"))
+    Entity.ID = *ID;
+  else
+    return ID.takeError();
+  if (std::optional<StringRef> SnapshotID = Object->getString("snapshot_id"))
+    Entity.SnapshotID = SnapshotID->str();
+  if (std::optional<StringRef> UnitID = Object->getString("unit_id"))
+    Entity.UnitID = UnitID->str();
+  if (std::optional<StringRef> OwnerID = Object->getString("owner_id"))
+    Entity.OwnerID = OwnerID->str();
+  Entity.Data = *Object;
+  return Entity;
+}
diff --git a/llvm/tools/llvm-advisor/src/Core/AdvisorTypes.h b/llvm/tools/llvm-advisor/src/Core/AdvisorTypes.h
new file mode 100644
index 0000000000000..c9bd9797c71d3
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Core/AdvisorTypes.h
@@ -0,0 +1,92 @@
+//===------------------- AdvisorTypes.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Data types for compilation commands, snapshots, and unit records.
+// Used throughout the system for representing build entities.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+
+namespace llvm::advisor {
+
+struct CompileCommand {
+  std::string Directory;
+  std::string File;
+  SmallVector<std::string, 16> Arguments;
+};
+
+struct SnapshotRecord {
+  std::string ID;
+  std::string SourceRoot;
+  std::string BuildRoot;
+  std::string ParentID;
+  uint64_t CreatedUnix = 0;
+};
+
+struct UnitRecord {
+  std::string ID;
+  std::string SnapshotID;
+  std::string SourcePath;
+  std::string Directory;
+  std::string ObjectPath;
+  std::string IRPath;
+  std::string RemarksPath;
+  std::string Language;
+  std::string TargetTriple;
+  std::string ToolchainVersion;
+  std::string SourceContentHash;
+  std::string CommandFingerprint;
+  SmallVector<std::string, 16> Arguments;
+};
+
+struct CapabilityRequest {
+  std::string SnapshotID;
+  std::string UnitID;
+  SmallVector<std::string, 8> CapabilityIDs;
+};
+
+struct JobRecord {
+  enum State { Queued, Running, Succeeded, Failed, Cancelled };
+
+  std::string ID;
+  State Current = Queued;
+  std::string Message;
+};
+
+struct EntityRecord {
+  std::string Kind;
+  std::string ID;
+  std::string SnapshotID;
+  std::string UnitID;
+  std::string OwnerID;
+  json::Object Data;
+};
+
+struct HealthStatus {
+  bool OK = true;
+  std::string Store;
+  uint64_t Snapshots = 0;
+  uint64_t Units = 0;
+};
+
+json::Value toJSON(const CompileCommand &Command);
+json::Value toJSON(const SnapshotRecord &Snapshot);
+json::Value toJSON(const UnitRecord &Unit);
+json::Value toJSON(const JobRecord &Job);
+json::Value toJSON(const EntityRecord &Entity);
+json::Value toJSON(const HealthStatus &Health);
+
+Expected<SnapshotRecord> snapshotFromJSON(const json::Value &Value);
+Expected<UnitRecord> unitFromJSON(const json::Value &Value);
+Expected<JobRecord> jobFromJSON(const json::Value &Value);
+Expected<EntityRecord> entityFromJSON(const json::Value &Value);
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Core/JobManager.cpp b/llvm/tools/llvm-advisor/src/Core/JobManager.cpp
new file mode 100644
index 0000000000000..83d07b3c8820d
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Core/JobManager.cpp
@@ -0,0 +1,104 @@
+//===------------------- JobManager.cpp - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Manages job queue and executes capability nodes in parallel.
+// Provides worker pool and streaming result dispatch.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Core/JobManager.h"
+#include "Utils/Hashing.h"
+#include "Utils/JSON.h"
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+JobManager::JobManager(StorageManager &Storage, unsigned MaxInFlight)
+    : Storage(Storage), Pool(hardware_concurrency()), MaxInFlight(MaxInFlight) {
+}
+
+Expected<JobRecord>
+JobManager::submit(StringRef Type, json::Value Request,
+                   unique_function<Error(CancellationToken &)> Work) {
+  if (Queued.load() >= MaxInFlight)
+    return createStringError(inconvertibleErrorCode(), "job queue is full");
+
+  JobRecord Job;
+  unsigned Seq = NextJobID.fetch_add(1);
+  Job.ID =
+      "job_" +
+      hashString((Twine(Type) + stringifyJSON(Request) + Twine(Seq)).str());
+  Job.Current = JobRecord::Queued;
+  Job.Message = Type.str();
+  if (Error Err = Storage.metadata().putJob(Job))
+    return std::move(Err);
+
+  ++Queued;
+  std::shared_ptr<CancellationToken> Token =
+      std::make_shared<CancellationToken>();
+  Pool.async([this, Job, Token, Work = std::move(Work)]() mutable {
+    {
+      std::lock_guard<std::mutex> Guard(TokensLock);
+      Tokens[Job.ID] = Token;
+    }
+
+    if (Error Err = transition(Job.ID, JobRecord::Running, Job.Message)) {
+      consumeError(std::move(Err));
+      removeJobTracking(Job.ID);
+      return;
+    }
+
+    Error Err = Token->isCancelled() ? Error::success() : Work(*Token);
+    removeJobTracking(Job.ID);
+
+    if (Token->isCancelled()) {
+      consumeError(transition(Job.ID, JobRecord::Cancelled, "cancelled"));
+      return;
+    }
+
+    if (Err) {
+      std::string Message = toString(std::move(Err));
+      consumeError(transition(Job.ID, JobRecord::Failed, Message));
+      return;
+    }
+    consumeError(transition(Job.ID, JobRecord::Succeeded, Job.Message));
+  });
+
+  return Job;
+}
+
+void JobManager::removeJobTracking(StringRef JobID) {
+  --Queued;
+  std::lock_guard<std::mutex> Guard(TokensLock);
+  Tokens.erase(JobID);
+}
+
+Error JobManager::transition(StringRef JobID, JobRecord::State State,
+                             StringRef Message) {
+  Expected<JobRecord> Job = Storage.metadata().getJob(JobID);
+  if (!Job)
+    return Job.takeError();
+  Job->Current = State;
+  Job->Message = Message.str();
+  return Storage.metadata().putJob(*Job);
+}
+
+Error JobManager::cancel(StringRef JobID) {
+  {
+    std::lock_guard<std::mutex> Guard(TokensLock);
+    StringMap<std::shared_ptr<CancellationToken>>::iterator I =
+        Tokens.find(JobID);
+    if (I != Tokens.end())
+      I->second->cancel();
+  }
+  return transition(JobID, JobRecord::Cancelled, "cancelled");
+}
+
+SmallVector<JobRecord, 16> JobManager::list() const {
+  return Storage.metadata().listJobs();
+}
diff --git a/llvm/tools/llvm-advisor/src/Core/JobManager.h b/llvm/tools/llvm-advisor/src/Core/JobManager.h
new file mode 100644
index 0000000000000..27c35bd39ff7d
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Core/JobManager.h
@@ -0,0 +1,59 @@
+//===------------------- JobManager.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Manages job queue and executes capability nodes in parallel.
+// Provides worker pool and streaming result dispatch.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+#include "Core/AdvisorTypes.h"
+#include "Storage/StorageManager.h"
+#include "llvm/Support/ThreadPool.h"
+#include <atomic>
+#include <memory>
+#include <mutex>
+
+namespace llvm::advisor {
+
+class CancellationToken {
+public:
+  void cancel() { Cancelled.store(true); }
+  bool isCancelled() const { return Cancelled.load(); }
+
+private:
+  std::atomic<bool> Cancelled{false};
+};
+
+class JobManager {
+public:
+  /// JobManager must outlive all submitted jobs. Destroying JobManager while
+  /// jobs are active is undefined behavior.
+  explicit JobManager(StorageManager &Storage, unsigned MaxInFlight = 1000);
+
+  Expected<JobRecord> submit(StringRef Type, json::Value Request,
+                             unique_function<Error(CancellationToken &)> Work);
+  Error cancel(StringRef JobID);
+  SmallVector<JobRecord, 16> list() const;
+
+private:
+  Error transition(StringRef JobID, JobRecord::State State, StringRef Message);
+  void removeJobTracking(StringRef JobID);
+
+  StorageManager &Storage;
+  DefaultThreadPool Pool;
+  unsigned MaxInFlight;
+  std::atomic<unsigned> Queued{0};
+  std::atomic<unsigned> NextJobID{0};
+  mutable std::mutex TokensLock;
+  StringMap<std::shared_ptr<CancellationToken>> Tokens;
+};
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Core/SnapshotManager.h b/llvm/tools/llvm-advisor/src/Core/SnapshotManager.h
new file mode 100644
index 0000000000000..77a8cd77ea417
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Core/SnapshotManager.h
@@ -0,0 +1,35 @@
+//===------------------- SnapshotManager.h - LLVM Advisor
+//-------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Snapshot lifecycle: create, list, delete, and tier management.
+// Handles snapshot retention and cleanup policies.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+#include "Core/AdvisorTypes.h"
+#include "Storage/StorageManager.h"
+
+namespace llvm::advisor {
+
+class SnapshotManager {
+public:
+  explicit SnapshotManager(StorageManager &Storage) : Storage(Storage) {}
+
+  SmallVector<SnapshotRecord, 16> list() const {
+    return Storage.metadata().listSnapshots();
+  }
+
+private:
+  StorageManager &Storage;
+};
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Core/UnitIdentity.cpp b/llvm/tools/llvm-advisor/src/Core/UnitIdentity.cpp
new file mode 100644
index 0000000000000..82b0ba0273b71
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Core/UnitIdentity.cpp
@@ -0,0 +1,23 @@
+//===------------------- UnitIdentity.cpp - LLVM Advisor
+//-------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Computes UnitID and CapabilityRunKey for build units.
+// Uniquely identifies a compilation unit across builds.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Core/UnitIdentity.h"
+#include "Utils/Hashing.h"
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+std::string UnitIdentity::compute(const UnitRecord &Unit) const {
+  return computeUnitID(Unit);
+}
diff --git a/llvm/tools/llvm-advisor/src/Core/UnitIdentity.h b/llvm/tools/llvm-advisor/src/Core/UnitIdentity.h
new file mode 100644
index 0000000000000..0309add1b1565
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Core/UnitIdentity.h
@@ -0,0 +1,26 @@
+//===------------------- UnitIdentity.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Computes UnitID and CapabilityRunKey for build units.
+// Uniquely identifies a compilation unit across builds.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+#include "Core/AdvisorTypes.h"
+
+namespace llvm::advisor {
+
+class UnitIdentity {
+public:
+  std::string compute(const UnitRecord &Unit) const;
+};
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Utils/Hashing.cpp b/llvm/tools/llvm-advisor/src/Utils/Hashing.cpp
new file mode 100644
index 0000000000000..cfe75727f2b12
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Hashing.cpp
@@ -0,0 +1,81 @@
+//===------------------- Hashing.cpp - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Provides hashing functions for UnitID, CapabilityRunKey, and content hashing.
+// Uses BLAKE3 for fast, secure hashing.
+//
+//===----------------------------------------------------------------------===//
+#include "Utils/Hashing.h"
+#include "llvm/ADT/StringExtras.h"
+#include "llvm/Support/BLAKE3.h"
+#include "llvm/Support/MemoryBuffer.h"
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+std::string llvm::advisor::hashString(StringRef Data) {
+  BLAKE3 Hasher;
+  Hasher.update(Data);
+  BLAKE3Result<> Result = Hasher.final();
+  return toHex(Result, true);
+}
+
+Expected<std::string> llvm::advisor::hashFile(StringRef Path) {
+  ErrorOr<std::unique_ptr<MemoryBuffer>> Buffer = MemoryBuffer::getFile(Path);
+  if (!Buffer)
+    return createStringError(Buffer.getError(), "cannot read '%s'",
+                             Path.str().c_str());
+  return hashString((*Buffer)->getBuffer());
+}
+
+std::string llvm::advisor::hashJSON(const json::Value &Value) {
+  std::string Storage;
+  raw_string_ostream OS(Storage);
+  OS << Value;
+  return hashString(OS.str());
+}
+
+std::string llvm::advisor::computeUnitID(const UnitRecord &Unit) {
+  BLAKE3 Hasher;
+  Hasher.update(Unit.SourcePath);
+  Hasher.update("\0");
+  Hasher.update(Unit.Language);
+  Hasher.update("\0");
+  Hasher.update(Unit.TargetTriple);
+  for (const std::string &Arg : Unit.Arguments) {
+    Hasher.update("\0");
+    Hasher.update(Arg);
+  }
+  BLAKE3Result<> Result = Hasher.final();
+  return toHex(Result, true);
+}
+
+std::string llvm::advisor::computeSnapshotID(StringRef SourceRoot,
+                                             StringRef BuildRoot,
+                                             uint64_t CreatedUnix) {
+  std::string Data;
+  raw_string_ostream OS(Data);
+  OS << SourceRoot << '\0' << BuildRoot << '\0' << CreatedUnix;
+  return hashString(OS.str());
+}
+
+std::string llvm::advisor::computeCapabilityRunKey(const UnitRecord &Unit,
+                                                   StringRef CapabilityID,
+                                                   StringRef CapabilityVersion,
+                                                   StringRef InputDigest) {
+  BLAKE3 Hasher;
+  Hasher.update(Unit.ID);
+  Hasher.update("\0");
+  Hasher.update(CapabilityID);
+  Hasher.update("\0");
+  Hasher.update(CapabilityVersion);
+  Hasher.update("\0");
+  Hasher.update(InputDigest);
+  BLAKE3Result<> Result = Hasher.final();
+  return toHex(Result, true);
+}
diff --git a/llvm/tools/llvm-advisor/src/Utils/Hashing.h b/llvm/tools/llvm-advisor/src/Utils/Hashing.h
new file mode 100644
index 0000000000000..3a65dbd49e3dd
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Hashing.h
@@ -0,0 +1,43 @@
+//===------------------- Hashing.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Provides hashing functions for UnitID, CapabilityRunKey, and content hashing.
+// Uses BLAKE3 for fast, secure hashing.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+#include "Core/AdvisorTypes.h"
+
+namespace llvm::advisor {
+
+/// Return the BLAKE3 hash of a string as a hex string.
+std::string hashString(StringRef Data);
+
+/// Return the BLAKE3 hash of a file's contents as a hex string.
+Expected<std::string> hashFile(StringRef Path);
+
+/// Return the BLAKE3 hash of a JSON value as a hex string.
+std::string hashJSON(const json::Value &Value);
+
+/// Compute a deterministic hash-based ID for a compilation unit.
+std::string computeUnitID(const UnitRecord &Unit);
+
+/// Compute a deterministic snapshot ID from root paths and a timestamp.
+std::string computeSnapshotID(StringRef SourceRoot, StringRef BuildRoot,
+                              uint64_t CreatedUnix);
+
+/// Compute a deterministic run key from a unit, capability, and input digest.
+std::string computeCapabilityRunKey(const UnitRecord &Unit,
+                                    StringRef CapabilityID,
+                                    StringRef CapabilityVersion,
+                                    StringRef InputDigest);
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Utils/JSON.cpp b/llvm/tools/llvm-advisor/src/Utils/JSON.cpp
new file mode 100644
index 0000000000000..06e46dc7099c8
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/JSON.cpp
@@ -0,0 +1,100 @@
+//===------------------- JSON.cpp - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Typed JSON helpers on top of LLVM's JSON support.
+// Provides convenient wrappers for reading and writing JSON files.
+//
+//===----------------------------------------------------------------------===//
+#include "Utils/JSON.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/ToolOutputFile.h"
+
+#include <chrono>
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+Expected<json::Value> llvm::advisor::parseJSONFile(StringRef Path) {
+  ErrorOr<std::unique_ptr<MemoryBuffer>> Buffer = MemoryBuffer::getFile(Path);
+  if (!Buffer)
+    return createStringError(Buffer.getError(), "cannot read '%s'",
+                             Path.str().c_str());
+
+  Expected<json::Value> Parsed = json::parse((*Buffer)->getBuffer());
+  if (!Parsed)
+    return Parsed.takeError();
+  return std::move(*Parsed);
+}
+
+Error llvm::advisor::writeJSONFile(StringRef Path, const json::Value &Value) {
+  std::error_code EC;
+  ToolOutputFile Out(Path, EC, sys::fs::OF_Text);
+  if (EC)
+    return createStringError(EC, "cannot write '%s'", Path.str().c_str());
+  Out.os() << Value << '\n';
+  Out.keep();
+  return Error::success();
+}
+
+Expected<std::string> llvm::advisor::getString(const json::Object &Object,
+                                               StringRef Key) {
+  std::optional<StringRef> Value = Object.getString(Key);
+  if (!Value)
+    return createStringError(inconvertibleErrorCode(), "missing string '%s'",
+                             Key.str().c_str());
+  return Value->str();
+}
+
+SmallVector<std::string, 8>
+llvm::advisor::getStringArray(const json::Object &Object, StringRef Key) {
+  SmallVector<std::string, 8> Out;
+  const json::Array *Array = Object.getArray(Key);
+  if (!Array)
+    return Out;
+
+  for (const json::Value &Value : *Array) {
+    std::optional<StringRef> String = Value.getAsString();
+    if (String)
+      Out.push_back(String->str());
+  }
+  return Out;
+}
+
+std::string llvm::advisor::stringifyJSON(const json::Value &Value) {
+  std::string Storage;
+  raw_string_ostream OS(Storage);
+  OS << Value;
+  return OS.str();
+}
+
+static int64_t unixNow() {
+  return std::chrono::duration_cast<std::chrono::seconds>(
+             std::chrono::system_clock::now().time_since_epoch())
+      .count();
+}
+
+static std::string uniqueRequestID() {
+  static uint64_t Counter = 0;
+  int64_t Now = unixNow();
+  return ("req_" + Twine(Now) + "_" + Twine(Counter++)).str();
+}
+
+json::Value llvm::advisor::successEnvelope(json::Value Data) {
+  return json::Object{{"request_id", uniqueRequestID()},
+                      {"timestamp_unix", unixNow()},
+                      {"status", "success"},
+                      {"data", std::move(Data)}};
+}
+
+json::Value llvm::advisor::errorEnvelope(StringRef Code, StringRef Message) {
+  return json::Object{
+      {"request_id", uniqueRequestID()},
+      {"timestamp_unix", unixNow()},
+      {"status", "error"},
+      {"error", json::Object{{"code", Code}, {"message", Message}}}};
+}
diff --git a/llvm/tools/llvm-advisor/src/Utils/JSON.h b/llvm/tools/llvm-advisor/src/Utils/JSON.h
new file mode 100644
index 0000000000000..75ce8f5ce899f
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/JSON.h
@@ -0,0 +1,45 @@
+//===------------------- JSON.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Typed JSON helpers on top of LLVM's JSON support.
+// Provides convenient wrappers for reading and writing JSON files.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/ToolOutputFile.h"
+
+namespace llvm::advisor {
+
+/// Parse a JSON file from disk.
+Expected<json::Value> parseJSONFile(StringRef Path);
+
+/// Write a JSON value to disk atomically.
+Error writeJSONFile(StringRef Path, const json::Value &Value);
+
+/// Extract a string value from a JSON object.
+Expected<std::string> getString(const json::Object &Object, StringRef Key);
+
+/// Extract an array of strings from a JSON object.
+/// Non-string elements are silently skipped.
+SmallVector<std::string, 8> getStringArray(const json::Object &Object,
+                                           StringRef Key);
+
+/// Serialize a JSON value to a string.
+std::string stringifyJSON(const json::Value &Value);
+
+/// Wrap data in a standard success envelope with request metadata.
+json::Value successEnvelope(json::Value Data);
+
+/// Wrap an error in a standard error envelope with request metadata.
+json::Value errorEnvelope(StringRef Code, StringRef Message);
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Utils/Logging.cpp b/llvm/tools/llvm-advisor/src/Utils/Logging.cpp
new file mode 100644
index 0000000000000..d28a90aa7f8ea
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Logging.cpp
@@ -0,0 +1,36 @@
+//===------------------- Logging.cpp - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Structured logging with multiple severity levels.
+// Provides logging infrastructure for the entire advisor.
+//
+//===----------------------------------------------------------------------===//
+#include "Utils/Logging.h"
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+static StringRef levelLabel(LogLevel Level) {
+  switch (Level) {
+  case LogLevel::Error:
+    return "error";
+  case LogLevel::Warning:
+    return "warning";
+  case LogLevel::Info:
+    return "info";
+  case LogLevel::Debug:
+    return "debug";
+  }
+  llvm_unreachable("Unknown log level");
+}
+
+void Logger::log(LogLevel Level, StringRef Message) {
+  if (Level == LogLevel::Debug && !Verbose)
+    return;
+  OS << "llvm-advisor: " << levelLabel(Level) << ": " << Message << '\n';
+}
diff --git a/llvm/tools/llvm-advisor/src/Utils/Logging.h b/llvm/tools/llvm-advisor/src/Utils/Logging.h
new file mode 100644
index 0000000000000..8dce3495e7fe9
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Logging.h
@@ -0,0 +1,38 @@
+//===------------------- Logging.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Structured logging with multiple severity levels.
+// Provides logging infrastructure for the entire advisor.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+
+namespace llvm::advisor {
+
+enum class LogLevel { Error, Warning, Info, Debug };
+
+/// Simple logger that writes messages to a stream with severity labels.
+class Logger {
+public:
+  explicit Logger(raw_ostream &OS) : OS(OS) {}
+
+  /// Toggle verbose (debug) output.
+  void setVerbose(bool Value) { Verbose = Value; }
+
+  /// Write a message at the given severity level.
+  void log(LogLevel Level, StringRef Message);
+
+private:
+  raw_ostream &OS;
+  bool Verbose = false;
+};
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Utils/Metrics.cpp b/llvm/tools/llvm-advisor/src/Utils/Metrics.cpp
new file mode 100644
index 0000000000000..bd60c61dbe01b
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Metrics.cpp
@@ -0,0 +1,35 @@
+//===------------------- Metrics.cpp - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Prometheus-compatible metrics counters for observability.
+// Tracks performance and operational metrics.
+//
+//===----------------------------------------------------------------------===//
+#include "Utils/Metrics.h"
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+void Metrics::increment(StringRef Name, uint64_t Delta) {
+  Counters[Name] += Delta;
+}
+
+uint64_t Metrics::get(StringRef Name) const {
+  StringMap<uint64_t>::const_iterator I = Counters.find(Name);
+  if (I == Counters.end())
+    return 0;
+  return I->second;
+}
+
+std::string Metrics::toText() const {
+  std::string Storage;
+  raw_string_ostream OS(Storage);
+  for (const StringMapEntry<uint64_t> &Entry : Counters)
+    OS << Entry.first() << ' ' << Entry.second << '\n';
+  return OS.str();
+}
diff --git a/llvm/tools/llvm-advisor/src/Utils/Metrics.h b/llvm/tools/llvm-advisor/src/Utils/Metrics.h
new file mode 100644
index 0000000000000..2c91403433f1a
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Metrics.h
@@ -0,0 +1,38 @@
+//===------------------- Metrics.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Prometheus-compatible metrics counters for observability.
+// Tracks performance and operational metrics.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+
+namespace llvm::advisor {
+
+/// Simple counter-based metrics tracker.
+class Metrics {
+public:
+  /// Increment a named counter by the given delta.
+  void increment(StringRef Name, uint64_t Delta = 1);
+
+  /// Read the current value of a named counter.
+  uint64_t get(StringRef Name) const;
+
+  /// Export all counters as simple text lines.
+  ///
+  /// Each line is "name value".
+  std::string toText() const;
+
+private:
+  StringMap<uint64_t> Counters;
+};
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Utils/Normalization.cpp b/llvm/tools/llvm-advisor/src/Utils/Normalization.cpp
new file mode 100644
index 0000000000000..251db9a9a1137
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Normalization.cpp
@@ -0,0 +1,111 @@
+//===------------------- Normalization.cpp - LLVM Advisor
+//-------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Command line normalization and path canonicalization.
+// Normalizes paths and commands for consistent comparison.
+//
+//===----------------------------------------------------------------------===//
+#include "Utils/Normalization.h"
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+std::string llvm::advisor::normalizePath(StringRef Path, StringRef Base) {
+  SmallString<256> Result;
+  if (!sys::path::is_absolute(Path) && !Base.empty())
+    Result = Base;
+  sys::path::append(Result, Path);
+  sys::path::remove_dots(Result, true);
+  sys::path::native(Result);
+  return Result.str().str();
+}
+
+static bool isInside(StringRef Path, StringRef Root) {
+  if (Path == Root)
+    return true;
+  if (!Path.starts_with(Root))
+    return false;
+  return sys::path::is_separator(Path.drop_front(Root.size()).front());
+}
+
+Expected<std::string>
+llvm::advisor::canonicalizePath(StringRef Path,
+                                ArrayRef<StringRef> AllowedRoots) {
+  SmallString<256> RealPath;
+  if (std::error_code EC = sys::fs::real_path(Path, RealPath))
+    return createStringError(EC, Twine("path does not exist: ") + Path);
+  sys::path::remove_dots(RealPath, true);
+
+  for (StringRef Root : AllowedRoots) {
+    SmallString<256> RealRoot;
+    if (std::error_code EC = sys::fs::real_path(Root, RealRoot))
+      return createStringError(EC, Twine("path root does not exist: ") + Root);
+    sys::path::remove_dots(RealRoot, true);
+    if (isInside(RealPath, RealRoot))
+      return RealPath.str().str();
+  }
+
+  return createStringError(inconvertibleErrorCode(),
+                           Twine("path escapes allowed roots: ") + Path);
+}
+
+SmallVector<std::string, 16>
+llvm::advisor::normalizeCommand(ArrayRef<std::string> Arguments) {
+  SmallVector<std::string, 16> Out;
+  for (const std::string &Arg : Arguments) {
+    StringRef Ref(Arg);
+    if (Ref == "-ftime-trace-granularity=0")
+      continue;
+    if (Ref.starts_with("-fdebug-compilation-dir="))
+      continue;
+    Out.push_back(Arg);
+  }
+  return Out;
+}
+
+std::string llvm::advisor::inferLanguage(StringRef Path) {
+  StringRef Ext = sys::path::extension(Path);
+  if (Ext == ".c")
+    return "c";
+  if (Ext == ".m")
+    return "objective-c";
+  if (Ext == ".mm")
+    return "objective-c++";
+  if (Ext == ".s" || Ext == ".S")
+    return "assembly";
+  return "c++";
+}
+
+std::string llvm::advisor::inferTargetTriple(ArrayRef<std::string> Arguments) {
+  constexpr StringRef LongTargetPrefix = "--target=";
+  constexpr StringRef ShortTargetPrefix = "-target=";
+
+  for (size_t I = 0, E = Arguments.size(); I != E; ++I) {
+    StringRef Arg(Arguments[I]);
+    if (Arg == "-target" && I + 1 != E)
+      return Arguments[I + 1];
+    if (Arg.starts_with(LongTargetPrefix))
+      return Arg.drop_front(LongTargetPrefix.size()).str();
+    if (Arg.starts_with(ShortTargetPrefix))
+      return Arg.drop_front(ShortTargetPrefix.size()).str();
+  }
+  return {};
+}
+
+std::string llvm::advisor::resolveOutputPath(ArrayRef<std::string> Arguments,
+                                             StringRef BaseDir) {
+  for (size_t I = 0, E = Arguments.size(); I != E; ++I) {
+    StringRef Arg(Arguments[I]);
+    if (Arg == "-o" && I + 1 < E)
+      return normalizePath(Arguments[I + 1], BaseDir);
+    if (Arg.starts_with("-o") && Arg.size() > 2)
+      return normalizePath(Arg.drop_front(2), BaseDir);
+  }
+  return {};
+}
diff --git a/llvm/tools/llvm-advisor/src/Utils/Normalization.h b/llvm/tools/llvm-advisor/src/Utils/Normalization.h
new file mode 100644
index 0000000000000..95da8a81aa5eb
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Normalization.h
@@ -0,0 +1,43 @@
+//===------------------- Normalization.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Command line normalization and path canonicalization.
+// Normalizes paths and commands for consistent comparison.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/Path.h"
+
+namespace llvm::advisor {
+
+/// Resolve a path relative to Base and normalize separators/dots.
+std::string normalizePath(StringRef Path, StringRef Base = {});
+
+/// Resolve a path to its real path and verify it is within AllowedRoots.
+Expected<std::string> canonicalizePath(StringRef Path,
+                                       ArrayRef<StringRef> AllowedRoots);
+
+/// Remove non-deterministic flags from a command line.
+SmallVector<std::string, 16> normalizeCommand(ArrayRef<std::string> Arguments);
+
+/// Detect language from file extension.
+std::string inferLanguage(StringRef Path);
+
+/// Extract the target triple from compiler arguments.
+std::string inferTargetTriple(ArrayRef<std::string> Arguments);
+
+/// Parse compiler arguments to find the output path specified by -o.
+/// Returns an empty string if no output flag is present.
+std::string resolveOutputPath(ArrayRef<std::string> Arguments,
+                              StringRef BaseDir = {});
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Utils/ProcessRunner.cpp b/llvm/tools/llvm-advisor/src/Utils/ProcessRunner.cpp
new file mode 100644
index 0000000000000..43d36870cecc7
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/ProcessRunner.cpp
@@ -0,0 +1,125 @@
+//===------------------- ProcessRunner.cpp - LLVM Advisor
+//-------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Safe, allowlisted subprocess execution for external_fallback capabilities.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Utils/ProcessRunner.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Program.h"
+
+#include <chrono>
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+namespace {
+
+/// RAII guard that removes a temporary file on destruction.
+struct TempFileGuard {
+  StringRef Path;
+  explicit TempFileGuard(StringRef Path) : Path(Path) {}
+  ~TempFileGuard() { sys::fs::remove(Path); }
+};
+
+} // namespace
+
+void ProcessRunner::allow(StringRef Program) {
+  ToolPolicy &Policy = AllowList[Program];
+  Policy.AllowAll = true;
+  Policy.Flags.clear();
+}
+
+void ProcessRunner::allow(StringRef Program, ArrayRef<StringRef> Flags) {
+  ToolPolicy &Policy = AllowList[Program];
+  Policy.AllowAll = false;
+  Policy.Flags.clear();
+  for (StringRef Flag : Flags)
+    Policy.Flags.insert(Flag);
+}
+
+bool ProcessRunner::isAllowedArg(StringRef Program, StringRef Arg) const {
+  if (Arg.empty() || Arg.contains('\0') || Arg.contains('\n') ||
+      Arg.contains('\r'))
+    return false;
+  if (!Arg.starts_with("-"))
+    return true;
+
+  StringMap<ToolPolicy>::const_iterator I = AllowList.find(Program);
+  if (I == AllowList.end())
+    return false;
+  if (I->second.AllowAll)
+    return true;
+  if (I->second.Flags.empty())
+    return false;
+  if (I->second.Flags.contains(Arg))
+    return true;
+
+  StringRef Name = Arg.take_until([](char C) { return C == '='; });
+  return I->second.Flags.contains(Name);
+}
+
+Expected<ProcessResult> ProcessRunner::run(StringRef Program,
+                                           ArrayRef<std::string> Arguments,
+                                           unsigned TimeoutSeconds) const {
+  if (!AllowList.contains(Program))
+    return createStringError(inconvertibleErrorCode(),
+                             "program not allowlisted: %s",
+                             Program.str().c_str());
+
+  ErrorOr<std::string> Resolved = sys::findProgramByName(Program);
+  if (!Resolved)
+    return createStringError(Resolved.getError(), "cannot find program: %s",
+                             Program.str().c_str());
+
+  SmallVector<StringRef, 16> Args;
+  Args.push_back(*Resolved);
+  for (const auto &Arg : Arguments) {
+    if (!isAllowedArg(Program, Arg))
+      return createStringError(inconvertibleErrorCode(),
+                               "argument not allowlisted: %s", Arg.c_str());
+    Args.push_back(Arg);
+  }
+
+  SmallString<128> StdoutPath, StderrPath;
+  if (auto EC = sys::fs::createTemporaryFile("advisor-out", "tmp", StdoutPath))
+    return createStringError(EC, "failed to create stdout temp file");
+  TempFileGuard StdoutGuard(StdoutPath);
+
+  if (auto EC = sys::fs::createTemporaryFile("advisor-err", "tmp", StderrPath))
+    return createStringError(EC, "failed to create stderr temp file");
+  TempFileGuard StderrGuard(StderrPath);
+
+  std::optional<StringRef> Redirects[] = {
+      std::nullopt,          // stdin
+      StringRef(StdoutPath), // stdout
+      StringRef(StderrPath)  // stderr
+  };
+
+  auto StartTime = std::chrono::steady_clock::now();
+  int ExitCode = sys::ExecuteAndWait(*Resolved, Args, std::nullopt, Redirects,
+                                     TimeoutSeconds);
+  auto EndTime = std::chrono::steady_clock::now();
+
+  ProcessResult Result;
+  Result.ExitCode = ExitCode;
+  Result.WallTimeNs =
+      std::chrono::duration_cast<std::chrono::nanoseconds>(EndTime - StartTime)
+          .count();
+
+  if (auto Buf = MemoryBuffer::getFile(StdoutPath))
+    Result.Stdout = (*Buf)->getBuffer().str();
+  if (auto Buf = MemoryBuffer::getFile(StderrPath))
+    Result.Stderr = (*Buf)->getBuffer().str();
+
+  return Result;
+}
diff --git a/llvm/tools/llvm-advisor/src/Utils/ProcessRunner.h b/llvm/tools/llvm-advisor/src/Utils/ProcessRunner.h
new file mode 100644
index 0000000000000..eb684476d4266
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/ProcessRunner.h
@@ -0,0 +1,64 @@
+//===------------------- ProcessRunner.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Safe, allowlisted subprocess execution for external_fallback capabilities.
+// The only file in the entire codebase that may spawn child processes.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/ADT/StringSet.h"
+#include "llvm/Support/Error.h"
+
+#include <string>
+
+namespace llvm::advisor {
+
+/// Result of an external process invocation.
+struct ProcessResult {
+  int ExitCode = -1;
+  std::string Stdout;
+  std::string Stderr;
+  uint64_t WallTimeNs = 0;
+};
+
+/// Runs external tools with an explicit allowlist.
+///
+/// Programs and their arguments must be registered before execution.
+/// Any argument not in the allowlist causes the run to fail.
+class ProcessRunner {
+public:
+  /// Allow all arguments for a given program.
+  void allow(StringRef Program);
+
+  /// Allow only specific flags for a given program.
+  /// Overrides any previous allow-all setting.
+  void allow(StringRef Program, ArrayRef<StringRef> Flags);
+
+  /// Execute a program with the given arguments.
+  ///
+  /// Fails if the program or any argument is not allowlisted.
+  /// If TimeoutSeconds is zero, no timeout is applied.
+  Expected<ProcessResult> run(StringRef Program,
+                              ArrayRef<std::string> Arguments,
+                              unsigned TimeoutSeconds = 0) const;
+
+private:
+  struct ToolPolicy {
+    StringSet<> Flags;
+    bool AllowAll = false;
+  };
+
+  bool isAllowedArg(StringRef Program, StringRef Arg) const;
+  StringMap<ToolPolicy> AllowList;
+};
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/Utils/Redaction.cpp b/llvm/tools/llvm-advisor/src/Utils/Redaction.cpp
new file mode 100644
index 0000000000000..a5fcc304058b8
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Redaction.cpp
@@ -0,0 +1,58 @@
+//===------------------- Redaction.cpp - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Sensitive argument and path redaction for secure logging.
+// Removes secrets from stored command lines.
+//
+//===----------------------------------------------------------------------===//
+#include "Utils/Redaction.h"
+
+using namespace llvm;
+using namespace llvm::advisor;
+
+/// Return true if Haystack contains Needle ignoring ASCII case.
+static bool containsInsensitive(StringRef Haystack, StringRef Needle) {
+  if (Needle.size() > Haystack.size())
+    return false;
+  for (size_t I = 0, E = Haystack.size() - Needle.size() + 1; I != E; ++I) {
+    bool Match = true;
+    for (size_t J = 0; J != Needle.size(); ++J) {
+      if (std::tolower(static_cast<unsigned char>(Haystack[I + J])) !=
+          std::tolower(static_cast<unsigned char>(Needle[J]))) {
+        Match = false;
+        break;
+      }
+    }
+    if (Match)
+      return true;
+  }
+  return false;
+}
+
+static bool isSensitive(StringRef Value) {
+  static const StringRef Keywords[] = {"secret", "token", "password", "apikey",
+                                       "api_key"};
+  for (StringRef Keyword : Keywords)
+    if (containsInsensitive(Value, Keyword))
+      return true;
+  return false;
+}
+
+std::string llvm::advisor::redactString(StringRef Value) {
+  if (isSensitive(Value))
+    return "<redacted>";
+  return Value.str();
+}
+
+SmallVector<std::string, 16>
+llvm::advisor::redactCommand(ArrayRef<std::string> Arguments) {
+  SmallVector<std::string, 16> Out;
+  for (const std::string &Arg : Arguments)
+    Out.push_back(redactString(Arg));
+  return Out;
+}
diff --git a/llvm/tools/llvm-advisor/src/Utils/Redaction.h b/llvm/tools/llvm-advisor/src/Utils/Redaction.h
new file mode 100644
index 0000000000000..f496b1691b2e7
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/Utils/Redaction.h
@@ -0,0 +1,26 @@
+//===------------------- Redaction.h - LLVM Advisor -------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Sensitive argument and path redaction for secure logging.
+// Removes secrets from stored command lines.
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "AdvisorCommon.h"
+
+namespace llvm::advisor {
+
+/// Redact a single string if it contains sensitive keywords.
+std::string redactString(StringRef Value);
+
+/// Redact sensitive values from a command line argument list.
+SmallVector<std::string, 16> redactCommand(ArrayRef<std::string> Arguments);
+
+} // namespace llvm::advisor
diff --git a/llvm/tools/llvm-advisor/src/llvm-advisor.cpp b/llvm/tools/llvm-advisor/src/llvm-advisor.cpp
new file mode 100644
index 0000000000000..211c333590639
--- /dev/null
+++ b/llvm/tools/llvm-advisor/src/llvm-advisor.cpp
@@ -0,0 +1,27 @@
+//===--- llvm-advisor.cpp - LLVM Advisor ---------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/InitLLVM.h"
+#include "llvm/Support/raw_ostream.h"
+
+using namespace llvm;
+
+namespace {
+
+cl::OptionCategory AdvisorCategory("llvm-advisor options");
+
+} // namespace
+
+int main(int argc, char **argv) {
+  InitLLVM X(argc, argv);
+  cl::HideUnrelatedOptions(AdvisorCategory);
+  cl::ParseCommandLineOptions(argc, argv, "LLVM Advisor\n");
+  outs() << "llvm-advisor foundation is available.\n";
+  return 0;
+}



More information about the llvm-commits mailing list