Checkpoint Lab — Configuration Elements, Variables, Properties, and Environment Control
The checkpoint makes environment control auditable: one JMX runs unchanged against LAB-A and LAB-B, external property files select the target/workload, a response-derived value stays thread-local, run-wide properties stay process-global, and one intentional variable/property mismatch is diagnosed without editing the plan into separate environment copies.
Learning objectives
- Run one unchanged JMX against two disposable loopback configurations.
- Prove resolved target, workload, label, and data-path inputs from external non-secret properties.
- Prove a response-derived variable remains isolated per thread.
- Prove a run-wide JMeter property is shared by threads in one JMeter process.
-
Introduce and diagnose one deliberate
${VAR}versus__P(property)mismatch. - Produce a property-resolution table, exact CLI record, JTL/log/server evidence, JMX hash, and validity note.
1. Checkpoint assumptions and hard ceilings
| Item | Baseline |
|---|---|
| JMeter | Apache JMeter 5.6.3. |
| Java | Java 17 JDK lab baseline; JMeter 5.6.3 requires Java 8+. |
| Plugins | None. |
| Targets |
Only 127.0.0.1:8000 (LAB-A) and
127.0.0.1:8001 (LAB-B).
|
| Profile A | 2 threads, 3-second lifetime. |
| Profile B | 1 thread, 4-second lifetime. |
| Sampler |
One /config request per loop plus an optional
bounded verification sampler.
|
| Property data | Synthetic, non-secret only. |
| Remote testing |
Not executed; -G discussed/validated
conceptually only.
|
2. Workspace
jmeter-ch06-checkpoint/
├── fixtures/
│ └── config_fixture.py
├── config/
│ ├── lab-a.properties
│ └── lab-b.properties
├── plans/
│ ├── config-debug.jmx
│ └── config-load.jmx
├── evidence/
│ ├── resolution-a.txt
│ ├── resolution-b.txt
│ ├── predictions.txt
│ ├── jmx-sha256.txt
│ └── validity.txt
└── results/
├── run-a/
├── run-b/
└── broken/
3. Start and preflight both fixtures
python fixtures/config_fixture.py --port 8000 --name LAB-A --log results/run-a/server-events.jsonl
python fixtures/config_fixture.py --port 8001 --name LAB-B --log results/run-b/server-events.jsonl
curl --fail --silent http://127.0.0.1:8000/health
curl --fail --silent http://127.0.0.1:8001/health
Record the returned server names so the configuration files can be verified independently of JMeter.
4. Property profiles
config/lab-a.properties:
# Non-secret local configuration A
lab.host=127.0.0.1
lab.port=8000
lab.name=LAB-A
load.threads=2
load.duration=3
run.label=properties-a
global.mode=project-file
lab.data.path=fixtures/data/config-a.csv
config/lab-b.properties:
# Non-secret local configuration B
lab.host=127.0.0.1
lab.port=8001
lab.name=LAB-B
load.threads=1
load.duration=4
run.label=properties-b
global.mode=project-file
lab.data.path=fixtures/data/config-b.csv
5. One JMX contract
The plan must use:
HOST = ${__P(lab.host,127.0.0.1)}
PORT = ${__P(lab.port,8000)}
DATA_PATH = ${__P(lab.data.path,fixtures/data/default.csv)}
Thread Group:
Number of Threads = ${__P(load.threads,1)}
Loop Count = 100
Lifetime enabled
Duration = ${__P(load.duration,3)}
Headers:
X-Run-Label = ${__P(run.label,default-run)}
X-Global-Mode = ${__P(global.mode,default-mode)}
Sampler:
/config?thread=${__threadNum}&data_path=${DATA_PATH}&run=${__P(run.label,default-run)}
Save the load JMX once, calculate its SHA-256, and keep the same file for A and B:
# Bash
sha256sum plans/config-load.jmx > evidence/jmx-sha256.txt
# PowerShell
Get-FileHash plans\config-load.jmx -Algorithm SHA256 |
Format-List | Out-File evidence\jmx-sha256.txt
6. Write predictions before any run
| Prediction | Run A | Run B |
|---|---|---|
| Target server | LAB-A / port 8000 | LAB-B / port 8001 |
| Configured users | 2 | 1 |
| Lifetime | 3 seconds | 4 seconds |
| Run label header/query | properties-a | properties-b |
| Global mode | project-file for every thread | project-file for every thread |
| Data-path string | config-a.csv path | config-b.csv path |
| JMX SHA-256 | Identical | Identical |
Add one more prediction: a response-derived
THREAD_ECHO variable will differ by JMeter thread,
while run.label remains the same for every thread
inside that run.
7. Authoring-only variable/property proof
Use the debug copy at 2 threads × 1 loop. Add the same extractor
from Lesson 2 to create THREAD_ECHO. Add a verification
sampler:
/verify?thread_var=${THREAD_ECHO}&thread_func=${__threadNum}&run=${__P(run.label,default-run)}
Use Debug Sampler/View Results Tree once to inspect thread variables
and selected properties. Expected: each thread's
THREAD_ECHO matches its own __threadNum,
while run.label is shared. Then disable diagnostics for
load runs.
8. Run LAB-A
jmeter -n -t plans/config-load.jmx -q config/lab-a.properties -Dlab.jvm.tag=JVM-A -l results/run-a/results.jtl -j results/run-a/jmeter.log
Verify server A receives events and server B does not receive this
run's requests. Preserve /stats from both endpoints.
9. Run LAB-B with the exact same JMX
jmeter -n -t plans/config-load.jmx -q config/lab-b.properties -Dlab.jvm.tag=JVM-B -l results/run-b/results.jtl -j results/run-b/jmeter.log
Recalculate the JMX SHA-256 and verify it has not changed. The resolved target/workload should change because the external properties changed—not because the JMX was edited.
10. Build the property-resolution evidence table
| Name | Source A | Resolved A | Source B | Resolved B | Store |
|---|---|---|---|---|---|
| lab.host | lab-a.properties | 127.0.0.1 | lab-b.properties | 127.0.0.1 | JMeter property |
| lab.port | lab-a.properties | 8000 | lab-b.properties | 8001 | JMeter property → PORT variable |
| load.threads | lab-a.properties | 2 | lab-b.properties | 1 | JMeter property |
| load.duration | lab-a.properties | 3 | lab-b.properties | 4 | JMeter property |
| run.label | lab-a.properties | properties-a | lab-b.properties | properties-b | JMeter property |
| lab.data.path | lab-a.properties | config-a.csv | lab-b.properties | config-b.csv | JMeter property → DATA_PATH variable |
| THREAD_ECHO | HTTP response extractor | per-thread | HTTP response extractor | per-thread | JMeter variable |
| lab.jvm.tag | -D |
JVM-A | -D |
JVM-B | Java system property |
11. Deliberately break the variable/property mapping
Create a copy only for the diagnostic failure,
plans/broken-port.jmx. Change HTTP Defaults port from
${PORT} to ${LAB_PORT}. Keep the launch
input -q config/lab-b.properties, which defines JMeter
property lab.port=8001 but does not define variable
LAB_PORT.
Prediction: ${LAB_PORT} remains
unresolved because it is a missing variable. Preserve the failing
JMX/JTL/log. Do not “fix” the property file by adding an unrelated
LAB_PORT variable concept.
12. Diagnose and restore the intended ownership
Read the JMX and input table:
- property exists:
lab.port=8001; -
startup variable exists:
PORT=${__P(lab.port,8000)}; - missing variable:
LAB_PORT.
The least destructive correction is to restore HTTP Defaults to
${PORT} (or use
${__P(lab.port,8000)} directly). Rerun LAB-B into a new
result directory; preserve the broken evidence.
13. One intentional -J override
Run A once more with the same profile and only this additional non-secret override:
-Jrun.label=checkpoint-cli-override
The target, users, duration, and data path remain from the A file while the run label changes. Record the resolved value and exact command.
14. Explain the distributed property boundary
If this plan later runs remotely, a controller-only
-Jrun.label=... is not a contract for remote engine
properties. The remote samplers need the relevant JMeter properties
provisioned/sent, commonly via -G. Data files, Java
-D system properties, plugins, and OS environment state
still require separate engine provisioning.
No remote engine is started in this checkpoint.
15. Verification checklist
- JMX SHA-256 is identical for A and B normal runs.
- LAB-A receives only the A normal run; LAB-B receives only B normal run.
- Resolved thread count/duration match the selected property file.
- Run label/global mode/data-path query/header values match the property resolution table.
- Bounded debug evidence shows response-derived variable isolation by thread.
-
run.labelis shared within one JMeter process as expected. -
The broken
${LAB_PORT}failure is preserved and correctly attributed to variable/property mismatch. - JTL and
jmeter.logare unique per run. - No real secrets, public targets, remote ports, or distribution-property edits were introduced.
16. Required evidence packet
| Artifact | Purpose |
|---|---|
| One normal JMX + SHA-256 | Proves plan logic stayed unchanged across environments. |
| A/B property files | Explicit non-secret environment/workload inputs. |
| Exact CLI commands |
Shows selected -q, -J,
-D inputs.
|
| Resolution A/B tables | Maps source → resolved value → store/scope. |
| Bounded Debug Sampler evidence | Variables, JMeter properties, system-property visibility, thread IDs. |
A/B JTL + jmeter.log |
Sample/runtime evidence. |
| A/B server JSONL + stats | Independent resolved-target/header/query evidence. |
| Broken JMX/JTL/log | First-failure proof for variable/property mismatch. |
| Validity note | Prevents configuration proof from becoming a capacity claim. |
17. Validity statement
-Dlab.jvm.tag remained a separate
JVM system-property plane. An intentional
${LAB_PORT} reference failed because the
launcher/property file defined lab.port, not a variable
named LAB_PORT; restoring the intended
PORT=${__P(lab.port,...)} mapping repaired the plan.
This demonstrates configuration ownership and resolution, not
production capacity or distributed-engine behavior.”
18. Cleanup
- Stop both local fixture processes.
- Keep A/B/broken artifacts until review is complete.
- Disable/remove Debug Sampler from load profiles.
- Do not copy property dumps into tickets if they might contain real secret values in future environments.
- No production target, real credential, remote engine, RMI port, database, container, plugin, or system-wide JVM/OS setting was changed.
19. What Chapter 06 adds to the operating model
The performance-test operating model now has a configuration provenance contract: committed JMX logic, selected non-secret profile, explicit CLI overrides, Java/JMeter property boundaries, thread-local runtime values, target allow-list, and the resolved run manifest are separately reviewable.
Chapter 07 builds on this configuration control to parameterize timers, think time, pacing, and throughput targets safely without confusing a timer property with achieved request rate or server latency.
Knowledge check
What proves the JMX really ran unchanged against LAB-A and LAB-B?
The same file/hash is used while external property profiles select different port/workload values, and server logs independently confirm the resolved target.
Why does ${LAB_PORT} fail when only -Jlab.port=8001 exists?
${LAB_PORT} is a variable lookup; -Jlab.port creates a JMeter property. They require an explicit __P mapping or direct property read.
What is shared across threads in one run: THREAD_ECHO or run.label?
run.label is a JMeter property shared by the process; THREAD_ECHO is a response-derived thread-local variable.
What does -Dlab.jvm.tag=JVM-A prove?
That Java system-property state is separate from JMeter property/variable state; it can be inspected as a JVM property but is not read by __P automatically.
Why is -G not exercised against live remote engines here?
Remote JMeter/RMI security and provisioning are a later isolated topic; this chapter needs only to establish that -G addresses remote-engine JMeter properties, not local thread variables or files.
Official references and version notes
-
Functions and Variables
— current variable syntax, undefined-variable behavior,
__P,__property,__setProperty, and thread-local versus global-property rules. - Elements of a Test Plan — User Defined Variables startup processing and variable-copy behavior per thread.
-
Getting Started
—
-J,-G,-D,-q, property-file loading, command-line processing order, GUI versus CLI guidance, and jmeter.log behavior. -
Properties Reference
— current guidance for setting JMeter properties through
user.propertiesrather than modifying distribution defaults. - Apache JMeter downloads — current production release and Java requirement.
Version-sensitive statements were rechecked against current Apache
JMeter documentation on 2026-09-04. The course baseline remains
Apache JMeter 5.6.3 with a Java 17 JDK for labs
and no third-party plugins; JMeter 5.6.3 itself requires Java 8+.
JMeter variables are thread-local after each thread receives its
startup copy; JMeter properties are shared within one JMeter
instance. An undefined ${VAR} reference is returned
unchanged rather than failing automatically.
__P(name,default) reads a JMeter property and
defaults to 1 when no explicit default is supplied.
-J defines a local JMeter property,
-G defines/sends properties to remote JMeter servers,
and -D defines a Java system property. The standard
user.properties layer is loaded after the base
property file and before later command-line additions such as
-q/-J; JMeter's Properties Reference
recommends setting ordinary JMeter properties in
user.properties rather than editing distribution
defaults. Mandatory labs remain single-process/local;
-G is demonstrated as a distributed-boundary concept
without starting remote engines.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.