Chapter 02Lesson 05~160 minutes

Checkpoint Lab — JMeter Architecture, Java Setup, Installation, and First Test Plan

Prove that a JMeter project can be reconstructed from explicit inputs rather than workstation memory. Build the project from an empty directory, verify the official tool artifact, create one tiny local plan, execute it from a clean shell, deliberately break the tool path, diagnose and restore it, and retain the evidence.

CheckpointToolchain provenanceJMXCLIRecovery

Learning objectives

  • Record exact Java/JMeter versions, installation path, release integrity result, and project layout.
  • Create and save a tiny loopback-only JMX with a known sample-count ceiling.
  • Execute the saved plan from a clean shell in CLI mode.
  • Predict and verify target/result/generator changes.
  • Introduce and diagnose one reversible JMeter path mismatch without modifying system-wide configuration.
  • Produce a reviewable evidence packet and bridge to Chapter 03 tree/scope semantics.

1. Checkpoint scenario and hard safety boundary

You are onboarding a new performance-test workstation. Starting from an empty jmeter-ch02-checkpoint directory, you must prove which Java and JMeter binaries will run, verify the Apache binary ZIP, create a minimal plan against the loopback fixture, execute it in CLI mode, and preserve enough evidence for another engineer to review the run.

Traffic ceiling: exactly one thread × three loops × one HTTP sampler = at most three samples. Target remains 127.0.0.1:8000. Abort if the host changes, the fixture is not healthy, unexpected errors appear, or the workstation becomes unstable. Do not increase the workload.

2. Create the project layout

jmeter-ch02-checkpoint/
├── fixtures/
│   └── fixture_server.py
├── plans/
│   └── first-plan.jmx
├── evidence/
│   ├── toolchain.txt
│   ├── checksum.txt
│   └── run-command.txt
└── results/

Create the directories only. Keep installation files outside the project so source and toolchain remain distinguishable.

3. Record toolchain identity

Capture path plus version before doing anything else.

Windows PowerShell:

New-Item -ItemType Directory -Force evidence, fixtures, plans, results | Out-Null

"=== Java commands ===" | Set-Content evidence\toolchain.txt
Get-Command java -All | Format-List * | Out-String |
    Add-Content evidence\toolchain.txt
java -version 2>&1 | Out-String |
    Add-Content evidence\toolchain.txt

"=== JMeter command ===" | Add-Content evidence\toolchain.txt
Get-Command jmeter.bat -All | Format-List * | Out-String |
    Add-Content evidence\toolchain.txt
jmeter.bat -v 2>&1 | Out-String |
    Add-Content evidence\toolchain.txt

Linux/macOS:

mkdir -p evidence fixtures plans results
{
  echo "=== Java ==="
  command -v java
  java -version
  echo "=== JMeter ==="
  command -v jmeter
  jmeter -v
} > evidence/toolchain.txt 2>&1

4. Verify the pinned JMeter archive

The checkpoint baseline is the official apache-jmeter-5.6.3.zip. If it is already present from Lesson 2, do not redownload merely for ritual; verify the exact archive you plan to use. If it is absent, download it from the current official mirror page and the checksum from the Apache distribution service.

Expected SHA-512:

387fadca903ee0aa30e3f2115fdfedb3898b102e6b9fe7cc3942703094bd2e65b235df2b0c6d0d3248e74c9a7950a36e42625fd74425368342c12e40b0163076

Save the verification result into evidence/checksum.txt.

PowerShell:

$Expected = "387fadca903ee0aa30e3f2115fdfedb3898b102e6b9fe7cc3942703094bd2e65b235df2b0c6d0d3248e74c9a7950a36e42625fd74425368342c12e40b0163076"
$Actual = (Get-FileHash .\apache-jmeter-5.6.3.zip -Algorithm SHA512).Hash.ToLowerInvariant()
"expected=$Expected" | Set-Content evidence\checksum.txt
"actual=$Actual" | Add-Content evidence\checksum.txt
if ($Actual -ne $Expected) { throw "Checksum mismatch" }
"status=OK" | Add-Content evidence\checksum.txt

Linux/macOS:

EXPECTED="387fadca903ee0aa30e3f2115fdfedb3898b102e6b9fe7cc3942703094bd2e65b235df2b0c6d0d3248e74c9a7950a36e42625fd74425368342c12e40b0163076"
ACTUAL="$(shasum -a 512 apache-jmeter-5.6.3.zip | awk '{print $1}')"
printf 'expected=%s\nactual=%s\n' "$EXPECTED" "$ACTUAL" > evidence/checksum.txt
test "$EXPECTED" = "$ACTUAL"
printf 'status=OK\n' >> evidence/checksum.txt

5. Start and preflight the disposable target

Save the same Chapter 02 fixture as fixtures/fixture_server.py:

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs
import json
import threading
import time

HOST = "127.0.0.1"
PORT = 8000
lock = threading.Lock()
total = 0
active = 0

class Handler(BaseHTTPRequestHandler):
    def _json(self, status, payload):
        body = json.dumps(payload).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        global total, active
        parsed = urlparse(self.path)

        if parsed.path == "/health":
            self._json(200, {"status": "ok"})
            return

        if parsed.path == "/stats":
            with lock:
                snapshot = {"active": active, "total": total}
            self._json(200, snapshot)
            return

        if parsed.path != "/work":
            self._json(404, {"error": "not_found"})
            return

        values = parse_qs(parsed.query)
        try:
            delay_ms = int(values.get("delay_ms", ["30"])[0])
        except ValueError:
            delay_ms = 30
        delay_ms = min(max(delay_ms, 0), 250)

        with lock:
            total += 1
            active += 1
            request_number = total
            active_now = active

        try:
            time.sleep(delay_ms / 1000.0)
            self._json(
                200,
                {
                    "status": "ok",
                    "request_number": request_number,
                    "active_at_start": active_now,
                    "delay_ms": delay_ms,
                },
            )
        finally:
            with lock:
                active -= 1

    def log_message(self, format, *args):
        return

if __name__ == "__main__":
    print(f"fixture=http://{HOST}:{PORT}")
    ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()

Start it in a separate terminal and verify:

python fixtures/fixture_server.py
# In another terminal:
curl --fail --silent http://127.0.0.1:8000/health
curl --fail --silent http://127.0.0.1:8000/stats

Before the run, total should be 0 from a fresh fixture.

6. Create and save the first plan

Use the GUI to construct the plan exactly as Lesson 2 describes, then save plans/first-plan.jmx. If you need a text comparison target, its essential saved structure is:

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan"
              testname="Chapter 02 First Plan" enabled="true">
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables"
                   elementType="Arguments"
                   guiclass="ArgumentsPanel"
                   testclass="Arguments"
                   testname="User Defined Variables">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup"
                   testname="One User — Three Iterations" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller"
                     elementType="LoopController"
                     guiclass="LoopControlPanel"
                     testclass="LoopController"
                     testname="Loop Controller">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">3</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui"
                          testclass="HTTPSamplerProxy"
                          testname="GET /work" enabled="true">
          <elementProp name="HTTPsampler.Arguments"
                       elementType="Arguments"
                       guiclass="HTTPArgumentsPanel"
                       testclass="Arguments"
                       testname="User Defined Variables">
            <collectionProp name="Arguments.arguments"/>
          </elementProp>
          <stringProp name="HTTPSampler.domain">127.0.0.1</stringProp>
          <stringProp name="HTTPSampler.port">8000</stringProp>
          <stringProp name="HTTPSampler.protocol">http</stringProp>
          <stringProp name="HTTPSampler.path">/work?delay_ms=30</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <stringProp name="HTTPSampler.connect_timeout">1000</stringProp>
          <stringProp name="HTTPSampler.response_timeout">2000</stringProp>
        </HTTPSamplerProxy>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

Do not perform the checkpoint evidence run in the GUI. If you used a GUI debug run while authoring, restart the fixture so its counter returns to a known baseline.

7. Predict changes before execution

  1. Thread/result prediction: one thread executes three loops, so the JTL should contain three sampler results.
  2. Target prediction: a fresh fixture should move from total=0 to total=3.
  3. Artifact prediction: the run directory should gain results.jtl, jmeter.log, and a report with index.html.
  4. Generator prediction: Java/JMeter will consume some CPU/memory/disk briefly, but this tiny run should not create sustained pressure.

Write these predictions into the runbook or evidence notes before executing.

8. Execute from a clean shell with explicit paths

Open a fresh terminal, set only the intended lab tool path, and verify versions again. Then execute:

Windows PowerShell:

New-Item -ItemType Directory -Force results\run-001 | Out-Null

$Command = @'
jmeter.bat -n -t plans\first-plan.jmx -l results\run-001\results.jtl -j results\run-001\jmeter.log -e -o results\run-001\report
'@
$Command | Set-Content evidence\run-command.txt

jmeter.bat -n `
  -t plans\first-plan.jmx `
  -l results\run-001\results.jtl `
  -j results\run-001\jmeter.log `
  -e -o results\run-001\report

Linux/macOS:

mkdir -p results/run-001
printf '%s
'   'jmeter -n -t plans/first-plan.jmx -l results/run-001/results.jtl -j results/run-001/jmeter.log -e -o results/run-001/report'   > evidence/run-command.txt

jmeter -n   -t plans/first-plan.jmx   -l results/run-001/results.jtl   -j results/run-001/jmeter.log   -e -o results/run-001/report

9. Verify independent observations

  • JTL exists and has three sample rows after its CSV header.
  • jmeter.log exists and contains no unexpected startup/runtime errors.
  • results/run-001/report/index.html exists.
  • Fixture /stats reports total 3 from the fresh process.
  • Host resource monitoring shows no unsafe generator pressure.
  • The run command, JMX, Java/JMeter versions, and checksum result are all preserved.

Do not infer production capacity from these observations. The checkpoint proves the toolchain and evidence path, not a capacity boundary.

10. Introduce one reversible path mismatch

Now create a controlled failure in the shell only. Do not rename/delete the real installation.

PowerShell:

$GoodJMeterHome = $env:JMETER_HOME
$env:JMETER_HOME = Join-Path $PWD "missing-jmeter-home"

"broken_jmeter_home=$env:JMETER_HOME" |
    Set-Content evidence\broken-path.txt

try {
    & "$env:JMETER_HOME\bin\jmeter.bat" -v
}
catch {
    $_ | Out-String | Add-Content evidence\broken-path.txt
}
finally {
    $env:JMETER_HOME = $GoodJMeterHome
}

& "$env:JMETER_HOME\bin\jmeter.bat" -v

Linux/macOS:

GOOD_JMETER_HOME="$JMETER_HOME"
export JMETER_HOME="$PWD/missing-jmeter-home"
printf 'broken_jmeter_home=%s
' "$JMETER_HOME" > evidence/broken-path.txt
"$JMETER_HOME/bin/jmeter" -v >> evidence/broken-path.txt 2>&1 || true
export JMETER_HOME="$GOOD_JMETER_HOME"
"$JMETER_HOME/bin/jmeter" -v

Diagnosis: the failure occurs before a JMeter engine exists because the referenced launcher path does not exist. JTL/SUT analysis is therefore irrelevant. The least destructive repair is restoring the correct installation path and re-verifying the tool identity.

11. Restore and rerun without overwriting evidence

Use a new run-002 directory after restoring the correct path. Restart the fixture first so its total counter returns to 0. Repeat the same CLI command with only output paths changed to run-002. Verify three samples and fixture total 3 again.

The first successful run, broken-path evidence, and restored run together demonstrate reproducibility and diagnosis more clearly than a single green screenshot.

12. Required evidence packet

Evidence Required content Review question
evidence/toolchain.txt Java/JMeter paths and versions Did the intended binaries actually run?
evidence/checksum.txt Expected/actual SHA-512 and OK result Was the pinned archive verified?
Installation path note Exact JMETER_HOME used Can another runner reconstruct the tool path?
plans/first-plan.jmx Saved JMeter tree What exact workload/target was configured?
evidence/run-command.txt Exact CLI invocation Can execution be repeated without GUI memory?
results/run-001/results.jtl Raw sample data Were three samples recorded and successful?
results/run-001/jmeter.log Engine diagnostics Did JMeter report runtime problems?
Dashboard report Derived presentation Can a reviewer inspect summarized samples without losing raw data?
evidence/broken-path.txt Controlled failure evidence Was the failure correctly localized to path/tool identity?
Restored run New run-002 artifacts Did the smallest correction restore the original behavior?

13. Cleanup and rollback

  1. Stop the loopback fixture with Ctrl+C.
  2. Restore any shell-local environment variables changed for the lab.
  3. Keep evidence and run artifacts until review is complete.
  4. Do not remove the verified JMeter installation if it will be used for subsequent chapters.
  5. No production targets, credentials, plugins, recorder certificates, databases, remote engines, containers, or system-wide OS/JVM settings were modified.

14. What Chapter 02 adds to the operating model

Chapter 01 gave you an experiment contract. Chapter 02 adds toolchain provenance: a specific Java runtime, a verified JMeter release, an explicit installation path, version-controlled JMX, a reproducible CLI command, and distinct raw/diagnostic/report artifacts. This makes a performance run transferable beyond one GUI session or workstation.

Chapter 03 now goes inside the JMX tree and explains scope, execution order, and component semantics so that a reproducible plan is also a correctly structured plan.

Knowledge check

Why is the path-mismatch exercise diagnosed before looking at JTL?

What two independent observations verify the expected sample count?

Why is the SHA-512 record part of performance-test evidence rather than merely installation trivia?

Why must the restored run use run-002 instead of overwriting run-001?

What does this checkpoint prove—and what does it not prove?

Next chapter

Test Plan Tree, Scope, Execution Order, and Component Semantics

You can now reproduce the JMeter runtime and artifacts. Chapter 03 explains how tree position, scope, execution order, and component type determine what each thread actually does.

Official references and version notes

Version and compatibility note

Version-sensitive statements were rechecked against current Apache JMeter primary sources on 2026-09-04. The current production release is Apache JMeter 5.6.3, whose download page states Java 8+; the 5.6.x change notes recommend Java 17 or later. The current development repository/next major line requires Java 17, so those development requirements are not retroactively applied to the 5.6.3 release. Mandatory labs use Java 17, the official 5.6.3 binary archive, no third-party plugins, the loopback target 127.0.0.1:8000, and CLI mode for the actual load run. The published SHA-512 for apache-jmeter-5.6.3.zip is 387fadca903ee0aa30e3f2115fdfedb3898b102e6b9fe7cc3942703094bd2e65b235df2b0c6d0d3248e74c9a7950a36e42625fd74425368342c12e40b0163076.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.