>_ Johannes Bechberger

experiments

Not everything needs to be production-ready. Some of these are conference demos, some are weekend hacks, some are proofs-of-concept that turned into something real. All of them were worth building.

eBPF

hello-ebpf experimental

Write eBPF programs in pure Java 22+ — tracepoints, kprobes, schedulers, network filters.

You want to write eBPF observability tools, schedulers, or network filters without leaving the JVM

Details

Scheduling

taskcontrol poc

eBPF Linux scheduler with REST API — stop, resume, and time-plan tasks and processes.

You want to stop and resume specific processes or JVM threads at the OS scheduler level from Java

Details
sound-of-scheduling demo

Linux eBPF scheduler that turns task scheduling into music, written in Java.

You want a fun demo of what you can build with hello-ebpf and eBPF schedulers

Details
concurrency-fuzz-scheduler poc

Fuzz concurrent Java apps with a custom eBPF scheduler — random stop/start at kernel level.

You want to reproduce concurrency bugs that only appear under unusual thread interleaving

Details

Misc

check-language-version experimental

Find what's blocking a --release downgrade — per-file minimum Java version from syntax analysis.

You want to lower your project's --release target and need to know what's blocking it

Details
local-android-ai experimental

Android app serving local LLM inference and device sensors over a REST API.

You want to run LLM inference or object detection on an Android device and access it from another app or script

Details
eBPF

hello-ebpf

experimental Docs GitHub

The Java library for eBPF. Write eBPF programs and the accompanying user-land code in pure Java 22+, without leaving the JVM. Covers tracepoints, kprobes, uprobes, XDP/TC network filters, and custom Linux schedulers via sched-ext.

When to use

  • You want to write eBPF observability tools, schedulers, or network filters without leaving the JVM
  • You are exploring Linux kernel internals from Java
  • You want to follow the accompanying blog series and build understanding step by step

When not to use

  • You need a production-ready, stable eBPF framework
  • You need broad kernel version support (requires kernel 6.17+ with BTF)
  • You are on macOS or Windows (macOS works via Lima VM — see docs)

Install

No stable release yet — check GitHub for build instructions.

Usage

@BPF(license = "GPL")
public abstract class HelloWorld extends BPFProgram implements SystemCallHooks {
    @Override
    public void enterOpenat2(int dfd, String filename, Ptr<open_how> how) {
        bpf_trace_printk("opening: %s", filename);
    }
    public static void main(String[] args) {
        try (HelloWorld p = BPFProgram.load(HelloWorld.class)) {
            p.autoAttachPrograms();
            p.tracePrintLoop(f -> f.task() + ": " + f.msg());
        }
    }
}

Find more information at https://parttimenerd.github.io/hello-ebpf/

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Set up prerequisites on Linux

Requires Linux kernel 6.17+, Java 22+, libbpf, and root privileges.

apt install libbpf-dev linux-tools-common linux-tools-$(uname -r)

Run the doctor script from the repo root to verify your setup:

./scripts/doctor.sh
Run a hello-world tracepoint program

The kernel-side method is compiled to BPF bytecode via the annotation processor at build time. Run with sudo and --enable-native-access (required for the Panama foreign-function API):

mvn package
sudo java --enable-native-access=ALL-UNNAMED \
     -cp target/bpf-samples.jar me.bechberger.ebpf.samples.HelloWorld

Expected output (one line per openat2 syscall from any process):

cat: opening: /etc/hosts
java: opening: /proc/self/status
Drop every third incoming packet with XDP

XDP hooks run directly in the kernel on every incoming packet — before the network stack:

@BPF(license = "GPL")
public abstract class XDPDropEveryThirdPacket extends BPFProgram implements XDPHook {
    final GlobalVariable<@Unsigned Integer> count = new GlobalVariable<>(0);

    @Override
    public xdp_action xdpHandlePacket(Ptr<xdp_md> ctx) {
        count.set(count.get() + 1);
        return count.get() % 3 == 1 ? xdp_action.XDP_DROP : xdp_action.XDP_PASS;
    }

    public static void main(String[] args) throws InterruptedException {
        try (var p = BPFProgram.load(XDPDropEveryThirdPacket.class)) {
            p.xdpAttach(XDPUtil.getNetworkInterfaceIndex());
            while (true) { System.out.println("Packets: " + p.count.get()); Thread.sleep(1000); }
        }
    }
}
Write a custom Linux CPU scheduler

sched_ext (kernel 6.14+) lets you replace the Linux CPU scheduler with BPF. The minimal scheduler needs just enqueue()SchedulerBase provides the shared dispatch queue, init(), dispatch(), and exit() defaults:

@BPF(license = "GPL")
@Property(name = "sched_name", value = "my_scheduler")
public abstract class MyScheduler extends SchedulerBase implements Scheduler {
    final DispatchQueue shared = DispatchQueue.attach(SHARED_DSQ_ID);

    @Override
    public void enqueue(Ptr<task_struct> p, long enq_flags) {
        shared.insertScaled(p, EnqFlags.passThrough(enq_flags));
    }

    public static void main(String[] args) throws Exception {
        try (var sched = BPFProgram.load(MyScheduler.class)) {
            sched.runSchedulerLoop(); // attach + block until Ctrl-C
        }
    }
}

Requires ls /sys/kernel/sched_ext to exist and systemctl stop scx if another scx scheduler is already running.

Use on macOS via Lima VM

hello-ebpf requires Linux. On macOS, use the bundled Lima VM config:

limactl start hello-ebpf.yaml --mount-writable
limactl shell hello-ebpf sudo bin/install.sh
limactl shell hello-ebpf
sudo -s PATH=$PATH   # root required for BPF program loading
Scheduling

taskcontrol

poc GitHub

eBPF-based Linux scheduler with a REST API for stopping, resuming, and time-planning individual tasks and processes at the OS level. Includes a Java client (ThreadControl) for per-thread control from within a JVM. Requires Linux 6.13+ and root.

When to use

  • You want to stop and resume specific processes or JVM threads at the OS scheduler level from Java
  • You need time-based plans (e.g. stop 10s, run 5s, repeat) for fine-grained workload control on Linux

When not to use

  • You are on macOS or Windows (Linux only, kernel 6.13+)
  • You want thread priority tuning without stop/resume control — use standard OS priority APIs instead

Install

No stable release yet — check GitHub for build instructions.

Usage

sudo ./scheduler.sh                                               # start (default port 8087)
curl "localhost:8087/taskGroup/$(pgrep -f MyApp)?stopping=true"  # stop a process
curl "localhost:8087/taskGroup/$(pgrep -f MyApp)?stopping=false" # resume it

Find more information at https://github.com/parttimenerd/taskcontrol

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Stop and resume a process via the REST API

Start the scheduler first (needs root), then use curl with the process PID:

sudo ./scheduler.sh        # starts on port 8087 by default
java samples/Ticker.java & # start a sample process
curl "localhost:8087/taskGroup/$(pgrep -f Ticker)?stopping=true"
# process stops being rescheduled
curl "localhost:8087/taskGroup/$(pgrep -f Ticker)?stopping=false"
# process resumes

taskGroup/{pid} targets all threads of a process. Use task/{tid} for a single thread. Check status with a plain GET: curl localhost:8087/taskGroup/$(pgrep -f Ticker) Returns running, stopping, or unknown.

Set a stop/run time plan for a process

The plan parameter is a comma-separated sequence of items: letter (s=stop, r=run) followed by a number of seconds. Stopping durations must be under 25s each:

# stop 10s, run 5s, stop 10s, run 10s
curl "localhost:8087/taskGroup/plan/$(pgrep -f Ticker)?plan=s10,r5,s10,r10"

Get the current plan:

curl "localhost:8087/taskGroup/plan/$(pgrep -f Ticker)"

Use task/plan/{tid} to target a single thread instead of a whole process.

Control JVM threads directly from Java

ThreadControl maps Java Thread objects to OS thread IDs and talks to the running scheduler. The scheduler must already be running before constructing ThreadControl:

ThreadControl tc = new ThreadControl();         // default port 8087
// ThreadControl tc = new ThreadControl(9000);  // custom port

Thread worker = new WorkerThread();
worker.start();

System.out.println("OS thread id: " + tc.osId(worker));
System.out.println("Status: " + tc.getThreadStatus(worker)); // RUNNING, STOPPED, or UNKNOWN

tc.stopThread(worker);
Thread.sleep(5000);
tc.resumeThread(worker);
Choose a scheduler and port
sudo ./scheduler.sh -s fifo -p 9000

Available schedulers: fifo (default), lottery (not yet fully implemented). Stopping a task for more than 30s will kill the scheduler; keep individual stop durations under 25s.

sound-of-scheduling

demo GitHub

A Linux eBPF scheduler written in Java (via hello-ebpf) that turns task scheduling events into music — mapping CPU dispatches and runtime to notes on a musical scale in real time.

When to use

  • You want a fun demo of what you can build with hello-ebpf and eBPF schedulers
  • You are curious what your Linux scheduler sounds like

When not to use

  • You need a production scheduler or performance tool — this is a demo

Install

No stable release yet — check GitHub for build instructions.

Usage

./scheduler.sh
# focus on firefox, faster tempo, scale based on task count:
./scheduler.sh --bpm=200 --scale-slice --filter firefox

Find more information at https://github.com/parttimenerd/sound-of-scheduling

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Tune the sound and focus on specific processes

Control tempo, musical scale, and which processes to include:

# 200 BPM, blues scale, only processes with "java" in their name
./scheduler.sh --bpm=200 --scale=BLUES --filter java

# scale the time slice based on number of active tasks
./scheduler.sh --scale-slice

# choose instruments for dispatches vs runtime
./scheduler.sh --dispatches-instrument=PIANO --runtime-instrument=STRINGS

Available scales: MAJOR_PENTATONIC, MINOR_PENTATONIC, BLUES, WHOLE_TONE, HARMONIC_MINOR, MELODIC_MINOR, HARMONIC_MAJOR.

Choose a scheduler type

Three scheduler types are available:

./scheduler.sh -t FIFO      # FIFO scheduler (default)
./scheduler.sh -t LOTTERY   # lottery scheduler
./scheduler.sh -t VTIME     # virtual time scheduler

Use --cores to limit how many CPU cores the scheduler uses:

./scheduler.sh -t VTIME --cores=4

Requires PulseAudio running at /run/user/1000/pulse/native.

concurrency-fuzz-scheduler

poc GitHub

eBPF-based Linux scheduler written in Java (via hello-ebpf) that fuzzes concurrent applications by randomly stopping and starting threads at the kernel level — triggering scheduling edge cases that are impossible to reproduce with user-space signals. FOSDEM'25 talk companion.

When to use

  • You want to reproduce concurrency bugs that only appear under unusual thread interleaving
  • You are testing a concurrent Java application and normal stress testing doesn't trigger the failure

When not to use

  • You are on macOS or Windows (Linux only, kernel 6.13+)
  • You need a production-grade fuzzer — this is a proof of concept

Install

No stable release yet — check GitHub for build instructions.

Usage

./scheduler.sh <script-or-command>
# focus on Java threads, log state changes:
./scheduler.sh samples/run_queue.sh --java --log

Find more information at https://github.com/parttimenerd/concurrency-fuzz-scheduler

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Fuzz a concurrent program until it fails

Pass the script or command to run as the positional argument. The scheduler randomly stops and resumes threads, restarting the program each iteration until it exits with a non-zero exit code:

./scheduler.sh samples/run_queue.sh --java --log

--java focuses the random scheduling on Java application threads (skips JVM internals). --log prints each stop/run state change with timestamps so you can see what triggered the crash.

Default ranges: sleep 10ms–2000ms, run 1ms–100ms. Use --max-iterations to cap the run:

./scheduler.sh my_test.sh --java --max-iterations=50
Tune the run/sleep ranges

Control how aggressively threads are interrupted with --run and --sleep ranges:

# run threads for 5–50ms, sleep them for 100–500ms
./scheduler.sh my_program --run=5ms,50ms --sleep=100ms,500ms

Set a per-iteration timeout — treated as a failure if the program doesn’t finish in time:

./scheduler.sh my_program --timeout=60
Use a custom error detection script

By default any non-zero exit code is treated as a failure. Supply --error-command to run a custom detection script at each check interval — the script signals failure by exiting with code 0 (success means “error found”):

./scheduler.sh my_program --error-command="grep -q 'ERROR' app.log"
# grep exits 0 when it finds the pattern — that triggers failure detection

Control how often the check runs (default 10s):

./scheduler.sh my_program --error-command="./check.sh" --error-check-interval=1s
Misc

check-language-version

experimental GitHub

Static analyser that scans your Java source tree and reports the minimum language level each file actually requires, so you know exactly what is preventing you from lowering --release.

Features

  • Per-file minimum Java version detection from Java 1.0 through 21+
  • map[Detects syntax features:lambdas, records, sealed classes, switch expressions, text blocks, var, pattern matching]
  • Summary table across all files
  • JSON output for scripting and aggregation across multiple runs
  • visualize.py for chart generation from JSON output

When to use

  • You want to lower your project's --release target and need to know what's blocking it
  • You are auditing a multi-module project for language-level inconsistencies
  • You want a quick per-file breakdown before a Java version migration

Install

No stable release yet — check GitHub for build instructions.

Usage

java -jar check-language-version.jar src/main/java/
java -jar check-language-version.jar src/main/java/ --summary --verbose

Find more information at https://github.com/parttimenerd/check-language-version

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Find what's preventing a --release downgrade

Run against your source tree, then use --summary to see the highest version required across all files and which features are responsible:

java -jar check-language-version.jar src/main/java/ --summary

Drill into individual files with --verbose to see exactly which syntax triggered the version:

java -jar check-language-version.jar src/main/java/MyClass.java --verbose
Export to JSON and visualize across multiple modules

Capture per-file results as JSON for scripting or cross-module aggregation:

java -jar check-language-version.jar src/ --json > module-a.json
java -jar check-language-version.jar other/src/ --json > module-b.json

Aggregate into a single summary table:

java -jar check-language-version.jar summary module-a.json module-b.json

Generate charts with the bundled script:

python3 visualize.py module-a.json module-b.json --output-dir charts --open

Syntax-only — does not detect API-level features like virtual threads (used via method calls, not syntax).

local-android-ai

experimental GitHub

Android app that serves local AI models and device sensors over a REST API on port 8005. Supports on-device LLM inference (Gemma, Llama, DeepSeek, TinyLlama via MediaPipe), object detection, camera capture, and compass orientation — designed for integration with Android terminals or K3s nodes.

When to use

  • You want to run LLM inference or object detection on an Android device and access it from another app or script
  • You are running Android devices as K3s cluster nodes and need AI/sensor APIs for cluster applications

When not to use

  • You need cloud-based AI inference — this runs fully on-device
  • Your device has less than 3 GB RAM (required for AI features)

Install

No stable release yet — check GitHub for build instructions.

Usage

# LLM inference (runs on-device, port 8005):
curl -X POST http://<phone-ip>:8005/ai/text \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello!", "model": "GEMMA_3_1B_IT"}'

Find more information at https://github.com/parttimenerd/local-android-ai

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Run LLM inference

Use the enum name or display name as the model field. Available models:

Model nameIdentifierVision
Gemma 3n E2B ITGEMMA_3_1B_ITyes
DeepSeek-R1 Distill Qwen 1.5BDEEPSEEK_R1_DISTILL_QWEN_1_5Bno
Llama 3.2 1B InstructLLAMA_3_2_1B_INSTRUCTno
Llama 3.2 3B InstructLLAMA_3_2_3B_INSTRUCTno
TinyLlama 1.1B ChatTINYLLAMA_1_1B_CHATno

Download a model first via the app UI or API, then query it:

# Download a model
curl -X POST http://<phone-ip>:8005/ai/models/download \
  -H "Content-Type: application/json" \
  -d '{"modelName": "GEMMA_3_1B_IT"}'

# Run inference
curl -X POST http://<phone-ip>:8005/ai/text \
  -H "Content-Type: application/json" \
  -d '{"text": "Explain eBPF in one sentence", "model": "GEMMA_3_1B_IT", "maxTokens": 150}'

Gemma 3n E2B IT supports image input — pass a base64-encoded image in the image field, or use captureConfig to capture directly from the camera.

Run object detection

Uses MediaPipe EfficientDet Lite 2. The app must be visible for camera capture (Android OS privacy restriction):

curl -X POST http://<phone-ip>:8005/ai/object_detection \
  -H "Content-Type: application/json" \
  -d '{"side": "rear", "threshold": 0.6, "maxResults": 5, "returnImage": false}'

Set returnImage: true to include a base64-encoded JPEG of the captured frame in the response.

Read device sensors

Get compass orientation (azimuth, pitch, roll) and camera snapshots:

# Compass orientation
curl http://<phone-ip>:8005/orientation

# Camera snapshot (app must be in foreground)
curl "http://<phone-ip>:8005/capture?side=rear&zoom=2.0"

# Server status, permissions, memory usage
curl http://<phone-ip>:8005/status

# Full API docs
curl http://<phone-ip>:8005/help

Highly experimental. Camera capture requires the app to be visible due to Android OS privacy restrictions.