Getting Started

June 9, 2026 · View on GitHub

This guide will help you get up and running with Project Argus in minutes.

Prerequisites

Before you begin, ensure you have the following installed:

  • Java 11+ for CLI commands (JDK required)
  • Java 17+ for Agent + Dashboard (MXBean polling mode — GC, CPU, Memory)
  • Java 21+ for full features (JFR streaming, Virtual Threads, Flame Graph, Allocation Tracking)
  • Gradle 8.4+ (optional) - Only needed if building from source

Verify Java Version

java -version
# Should output: openjdk version "21.x.x" or higher

Installation

curl -fsSL https://raw.githubusercontent.com/rlaope/argus/master/install.sh | bash

This does three things:

  1. Downloads argus-agent.jar and argus-cli.jar to ~/.argus/
  2. Creates a wrapper script at ~/.argus/bin/argus (auto-finds Java 11+ and runs the CLI jar)
  3. Adds ~/.argus/bin to your PATH in ~/.zshrc (or ~/.bashrc)

After installation, restart your terminal (or run source ~/.zshrc), then:

# Just type argus — it works anywhere
argus ps
argus histo 12345
argus report 12345
# Install a specific version
curl -fsSL https://raw.githubusercontent.com/rlaope/argus/master/install.sh | bash -s -- v1.5.0

Option 2: Manual Download

# Download JARs from GitHub Releases
ARGUS_VERSION=1.5.0
curl -LO https://github.com/rlaope/argus/releases/download/v${ARGUS_VERSION}/argus-agent.jar
curl -LO https://github.com/rlaope/argus/releases/download/v${ARGUS_VERSION}/argus-cli-${ARGUS_VERSION}-all.jar

Option 3: Build from Source

git clone https://github.com/rlaope/argus.git
cd argus
./gradlew build
./gradlew :argus-cli:fatJar

# JARs:
# argus-agent/build/libs/argus-agent-1.5.0.jar
# argus-cli/build/libs/argus-cli-1.5.0-all.jar

Quick Start with Sample Project

The easiest way to see Argus in action is to run the included sample project.

Step 1: Build Everything

./gradlew build

Step 2: Run the Sample with Argus

# Basic virtual thread demo
./gradlew :samples:virtual-thread-demo:runWithArgus

# Metrics demo with GC/CPU activity (recommended for testing dashboard)
./gradlew :samples:virtual-thread-simulation:runMetricsDemo

Step 3: Open the Dashboard

Open your browser and navigate to: http://localhost:9202/

The dashboard displays two tabs:

  • Virtual Threads: Thread events, active threads, pinning analysis
  • JVM Overview: CPU, GC, heap, flame graph, profiling, contention, correlation

Step 4: Use the CLI

# First-time setup (choose language)
argus init

# Diagnose any running JVM (no agent required)
argus ps                       # List JVM processes
argus histo <pid>              # Heap object histogram
argus threads <pid>            # Thread dump summary
argus gc <pid>                 # GC statistics
argus gcutil <pid>             # GC generation utilization
argus heap <pid>               # Heap memory usage
argus sysprops <pid>           # System properties
argus vmflag <pid>             # VM flags (view/set)
argus nmt <pid>                # Native memory tracking
argus classloader <pid>        # Class loader hierarchy
argus jfr <pid> start          # Flight Recorder control
argus info <pid>               # JVM information

# Real-time monitoring (requires agent)
argus top

# Or run directly
java -jar argus-cli/build/libs/argus-cli-1.5.0-all.jar --help

The CLI auto-detects the best data source: Argus agent (HTTP) when available, JDK tools (jcmd) as fallback. Use --source=agent|jdk to override.

Step 5: Enable Flame Graph

# Run with profiling enabled for flame graph data
./gradlew :samples:virtual-thread-simulation:runMetricsDemoFull

The flame graph appears in the dashboard under JVM Overview tab. It shows which methods consume the most CPU time.

Step 6: View the Output

You'll see output like this:

   _____
  /  _  \_______  ____  __ __  ______
 /  /_\  \_  __ \/ ___\|  |  \/  ___/
/    |    \  | \/ /_/  >  |  /\___ \
\____|__  /__|  \___  /|____//____  >
        \/     /_____/            \/

JVM Observability Platform vdev
[Argus] Initializing JFR streaming engine...
[Argus] Agent initialized successfully
[Argus] Ring buffer size: 65536
[Argus] JFR streaming started
============================================================
  Virtual Thread Demo - Argus Monitoring Example
============================================================

[Demo 1] Basic Virtual Threads
----------------------------------------
  Task 0 started on VirtualThread[#31]/runnable@ForkJoinPool-1-worker-1
  Task 1 started on VirtualThread[#33]/runnable@ForkJoinPool-1-worker-7
  ...
  All basic tasks completed.

[Demo 3] Thread Pinning (synchronized blocks)
----------------------------------------
  WARNING: This demonstrates thread pinning - avoid in production!
  Task 0 acquired lock (PINNED to carrier thread)
  ...
[Argus] PINNED: thread=61, carrier=-1, duration=204333917ns
[Argus] PINNED: thread=63, carrier=-1, duration=201776666ns
[Argus] PINNED: thread=62, carrier=-1, duration=204995125ns
  ...

[Argus] Shutting down...
[Argus] JFR streaming stopped. Total events processed: 29

Understanding the Output

Event Types

Virtual Thread Events

EventDescriptionWhen it Occurs
VIRTUAL_THREAD_STARTThread createdThread.startVirtualThread() or executor submit
VIRTUAL_THREAD_ENDThread terminatedVirtual thread completes or is interrupted
VIRTUAL_THREAD_PINNEDThread pinned to carrierCritical! synchronized block or native call
VIRTUAL_THREAD_SUBMIT_FAILEDSubmission failedExecutor rejected task

GC Events

EventDescription
GC_PAUSEGarbage collection occurred
GC_HEAP_SUMMARYHeap usage snapshot

CPU Events

EventDescription
CPU_LOADJVM and system CPU utilization

Reading PINNED Events

[Argus] PINNED: thread=61, carrier=-1, duration=204333917ns
         │             │           │              │
         │             │           │              └─ Duration pinned (~204ms)
         │             │           └─ Carrier thread ID
         │             └─ Virtual thread ID
         └─ Event type

Why Pinning Matters:

Pinning occurs when a virtual thread cannot be unmounted from its carrier thread. This defeats the purpose of virtual threads and can cause performance issues.

Common causes:

  • synchronized blocks (use ReentrantLock instead)
  • Native method calls
  • JNI operations

Using Argus with Your Application

Basic Usage

java -javaagent:~/.argus/argus-agent.jar \
     -jar your-application.jar

# Or using the helper command
java -javaagent:$(argus-agent --path) \
     -jar your-application.jar

With Full Profiling

java -javaagent:~/.argus/argus-agent.jar \
     -Dargus.profiling.enabled=true \
     -Dargus.contention.enabled=true \
     -jar your-application.jar

With OTLP Export

java -javaagent:~/.argus/argus-agent.jar \
     -Dargus.otlp.enabled=true \
     -Dargus.otlp.endpoint=http://localhost:4318/v1/metrics \
     -jar your-application.jar

Example: Spring Boot Application

java -javaagent:~/.argus/argus-agent.jar \
     -Dargus.profiling.enabled=true \
     -jar myapp.jar \
     --spring.threads.virtual.enabled=true

Sample Project Details

The sample project (samples/virtual-thread-demo) demonstrates four scenarios:

Demo 1: Basic Virtual Threads

// Creates 5 virtual threads that sleep for 100ms
for (int i = 0; i < 5; i++) {
    Thread.startVirtualThread(() -> {
        Thread.sleep(100);
    });
}

What to observe: VIRTUAL_THREAD_START and VIRTUAL_THREAD_END events.

Demo 2: Concurrent HTTP Requests

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // Each HTTP request runs on its own virtual thread
    executor.submit(() -> httpClient.send(request, ...));
}

What to observe: Multiple virtual threads handling I/O concurrently.

Demo 3: Thread Pinning

synchronized (lock) {
    Thread.sleep(200);  // Pinning occurs here!
}

What to observe: VIRTUAL_THREAD_PINNED events with duration.

Warning: Avoid synchronized with virtual threads. Use ReentrantLock instead:

lock.lock();
try {
    Thread.sleep(200);  // No pinning!
} finally {
    lock.unlock();
}

Demo 4: High Throughput

// Creates 1000 virtual threads rapidly
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 1000).forEach(i ->
        executor.submit(() -> computeTask())
    );
}

What to observe: Argus handles high event rates efficiently.

Interpreting Results

Healthy Application

[Argus] JFR streaming stopped. Total events processed: 2000
  • Many START/END events
  • Zero or few PINNED events
  • No SUBMIT_FAILED events

Application with Issues

[Argus] PINNED: thread=100, carrier=5, duration=500000000ns
[Argus] PINNED: thread=101, carrier=5, duration=500000000ns
[Argus] PINNED: thread=102, carrier=5, duration=500000000ns
  • Frequent PINNED events indicate synchronization issues
  • Long durations (>100ms) suggest blocking operations in synchronized blocks
  • Same carrier thread repeatedly pinned suggests a contention hotspot

Actions to Take

ObservationAction
Frequent PINNED eventsReplace synchronized with ReentrantLock
Long PINNED durationsMove blocking operations outside synchronized blocks
SUBMIT_FAILED eventsIncrease executor capacity or check for resource exhaustion

Next Steps

Java Version Compatibility

FeatureJava 11+Java 17+Java 21+
CLI (71 commands)
Dashboard & Web UI
GC AnalysisCLI only✅ MXBean✅ JFR
Virtual Thread Monitoring✅ JFR
Flame Graph✅ JFR
Micrometer Metrics
Spring Boot Starter

Native Binary (GraalVM)

Argus ships a GraalVM native-image build of the CLI. Its sweet spot is offline analysis — point it at a GC log or a heap dump and it runs as a single self-contained executable, with instant startup and no JVM (not even a JDK) needed on the host:

argus gclog gc.log           # GC log analysis + tuning advice
argus gcscore gc.log         # A–F GC health score
argus heapanalyze app.hprof  # heap-dump histogram + insights

This is also where the fast startup pays off most — in scripts or watch loops the JVM's warm-up cost would otherwise dominate.

When a release succeeds, pre-built binaries are attached to the release:

  • argus-linux-amd64 — Linux x86_64
  • argus-macos-aarch64 — macOS Apple Silicon

The native-image workflow runs after a release is published. If those two assets are missing from a given tag, the native build failed for that tag — use the argus-cli-*-all.jar instead.

Build from source with GraalVM 21+ installed (the bundled META-INF/native-image/ reachability metadata is applied automatically):

./gradlew :argus-cli:nativeImage
./argus-cli/build/native/argus --help

Live-target commands

The native binary can also inspect a running JVM, but with caveats — reach for the JAR if you need the full command set against a live process:

  • Works: commands that shell out to the JDK tools (jcmd, jstack, jstat, jps) — gc, heap, threads, threaddump, and friends. Note these still require those JDK tools to be installed on the host, so the "no JDK needed" benefit applies to the offline analyzers above, not here.
  • JAR only: commands that attach into the target JVM via the Attach API (com.sun.tools.attach.VirtualMachine). SubstrateVM ships no libattach, so they fail with UnsatisfiedLinkError: No attach in java.library.path (the native binary now reports this cleanly instead of crashing). Run them through the JAR: java -jar argus-cli-*-all.jar <command>.
CommandsMechanismNativeJAR
gc gcutil gccause gcnew gcrun heap histo metaspace threaddump threads deadlock classloader classstat compiler compilerqueue info ps env sysprops vmflag buffers finalizer perfcounter dynlibs stringtable symboltable sc nmtjcmd / jstack / jstat subprocess
gclog gclogdiff gcscore gcwhy heapanalyze and other file analyzersfile parsing (no target attach)
mbean spring pool g1 zgc threads --topJMX over the Attach API
instrumentdynamic Java agent (ByteBuddy)