skip to content

BeanShell Interpretation Cost

The legacy scripting family still ships and still works, but it is interpreted rather than compiled. Interviewers use it to see whether you know that a slow injector is a measurement problem.

on this pageshow

explore

questions

5

In JMeter, why does a BeanShell element re-pay its script cost on every call?

level: middleimportance: must knowfreq 62%

answer

  1. Think about what a sample pays twice
  2. Interpreter lifetime versus script lifetime
  3. Check what JMeter stores between two calls
  4. jmeter.properties has a note about interpreters

basics

~20 s

JMeter caches nothing for a BeanShell element. Every invocation hands the script straight to that element's bsh.Interpreter, calling eval() on the inline Script field or source() on a Script file, so the interpretation cost repeats per sample.

solid answer

~40 s

A BeanShell element wraps one `bsh.Interpreter`, built when JMeter clones the element for a thread. On each invocation `BeanShellTestElement` re-binds the script variables, sets `FileName`, `Parameters` and `bsh.args`, then calls `eval()` on the inline **Script** field or `source()` on the **Script file** if one is named. JMeter keeps no parsed or compiled form between calls, so the whole script is interpreted again for every sample that reaches the element. The interpreter itself is reused — `bin/jmeter.properties` states that BeanShell test elements do not share interpreters and that each element in each thread has its own, retained between samples — but that reuse preserves state, not work. Apache JMeter 6.0.0 still ships the family, pinned to BeanShell `2.0b6`.

code

xml · 6 lines
xml
<BeanShellPostProcessor guiclass="TestBeanGUI" testclass="BeanShellPostProcessor" testname="BeanShell PostProcessor" enabled="true">
  <stringProp name="filename"/>
  <stringProp name="parameters"/>
  <boolProp name="resetInterpreter">false</boolProp>
  <stringProp name="script">log.info(&quot;checked&quot;);</stringProp>
</BeanShellPostProcessor>

go deeper

for a junior

Recall that JMeter still ships six BeanShell elements and that they are interpreted, not compiled. Know that the text in the Script field runs again for every sample that reaches the element.

for a middle

Explain the per-call path: JMeter re-binds the script variables, then calls eval() on the inline script or source() on the script file. Nothing parsed or compiled survives between two calls.

for a senior

Show where the cost lands. A BeanShell Sampler times itself, while a PreProcessor, PostProcessor, Assertion, Timer or Listener burns injector CPU that no sample row in the results file records.

for a principal

Own the decision on whether an inherited plan's scripted elements are worth deleting, replacing with stock elements or converting, and be able to state what the team gives up by rewriting a plan that currently works.

## What JMeter actually calls on each invocation Every element in the BeanShell family — **BeanShell Sampler**, **BeanShell PreProcessor**, **BeanShell PostProcessor**, **BeanShell Assertion**, **BeanShell Listener** and **BeanShell Timer** — inherits from `BeanShellTestElement`. That class holds one `BeanShellInterpreter`, which wraps a single `bsh.Interpreter`. The per-invocation path is short and it is the whole story: 1. `getBeanShellInterpreter()` re-binds the standard script variables — `ctx`, `Label`, `prev`, `props` and `vars` — on the interpreter. 2. `processFileOrScript()` sets `FileName`, `Parameters` and `bsh.args`. 3. If the **Script file** field is empty it calls `eval(scriptText)`; otherwise it calls `source(fileName)`. There is no fourth step. JMeter holds no parsed or compiled form of the script between calls, and it keys nothing by hash or by element. Whatever the interpreter has to do to turn that text into behaviour, it does again for the next sample. A `source(fileName)` call is re-issued on the file every time too, so moving the script out of the `.jmx` and into a `.bsh` on disk changes where the text lives, not how often it is read. ## One interpreter per element, per thread `bin/jmeter.properties` states the ownership rule in the BeanShell block itself: *"Beanshell test elements do not share interpreters. Each element in each thread has its own interpreter. This is retained between samples."* The mechanism is `clone()`: JMeter clones the test tree for each thread, and `BeanShellTestElement.clone()` runs `init()`, which constructs a fresh `BeanShellInterpreter`. So a plan with three BeanShell elements running under 200 threads builds 600 interpreters at ramp-up, each one kept alive for that thread's whole run. That reuse is real, and it is why a variable you assign in the script survives to the next iteration. What it buys you is **state**, not **work** — the namespace persists, the interpretation does not become cheaper. ## Where the cost lands in your results This is the part that catches people out on an inherited plan. Only one of the six elements is timed by a sample at all. `JMeterThread` runs pre-processors, then serves the timers, then samples, then runs post-processors and assertions, then broadcasts to listeners — and listeners are notified inline on the sampling thread. | Element | When it runs | Where its time appears | |---|---|---| | BeanShell Sampler | as the sample itself | inside its own sample's elapsed time | | BeanShell PreProcessor | before the timers and the sampler | no sample records it | | BeanShell Timer | when JMeter collects the delay | added on top of the pause it returns | | BeanShell PostProcessor | after the sample has ended | no sample records it | | BeanShell Assertion | after the post-processors | no sample records it | | BeanShell Listener | when the result is broadcast | no sample records it | The `BeanShell Sampler` is honest about itself: `sample()` calls `sampleStart()`, evaluates the script, then `sampleEnd()`, so interpretation is inside the elapsed value you will later read. The other five are invisible in the results file while still consuming the injector's CPU and holding the thread. An inherited plan can therefore show perfectly ordinary per-sample numbers and still refuse to push more load, because the missing time is between the samples rather than in them. ## What this means for an inherited plan Some practical consequences worth stating plainly: - **Script size matters per sample, not per run.** A twenty-line script in a post-processor on a hot request is twenty lines interpreted for every one of those requests. - **A BeanShell Timer pays twice.** The script runs, JMeter reads its return value as a millisecond count, and only then sleeps. The time spent producing the number is additional to the pause. - **Ramp-up is not free either.** Each new thread's clone builds its own interpreter, and sources the init file if one is configured. - **Nothing warns you.** JMeter logs nothing about interpretation time; a script that quietly grew over three years looks exactly like one that did not. ## Checking it yourself Open `src/core/src/main/java/org/apache/jmeter/util/BeanShellTestElement.java` and follow `processFileOrScript`; then open `BeanShellInterpreter.java` and look at `eval` and `source`. Both are thin delegations to the `bsh.Interpreter` instance. Apache JMeter 6.0.0 still ships this family and pins `org.apache-extras.beanshell:bsh:2.0b6`; the components build file records the reason as *"commonly used in the jmx scripts"*, with a note that new scripts should not use it.

  • Does using the Script file field instead of the inline Script field avoid the per-call cost?
    No. `processFileOrScript` calls `source(fileName)` on each invocation instead of `eval(script)`, and JMeter keeps no parsed form of the file either. The Script file field changes where the text lives, and it stops JMeter interpolating `${...}` references into it — the manual warns those are passed verbatim to the interpreter and usually produce a syntax error.
  • Which of the six BeanShell elements records its own execution time as a sample?
    Only the BeanShell Sampler. Its `sample()` method calls `sampleStart()`, evaluates the script, then `sampleEnd()`, so interpretation is inside that sample's elapsed value. The PreProcessor, PostProcessor, Assertion, Listener and Timer produce no `SampleResult` of their own, so their time is spent between samples rather than inside one.
  • How many interpreters exist for one BeanShell PreProcessor under 200 threads?
    Two hundred. JMeter clones the test tree per thread, and `BeanShellTestElement.clone()` calls `init()`, which constructs a fresh `BeanShellInterpreter`. Each one is retained for that thread's lifetime, which is why a variable assigned in the script survives into the next iteration.

It is the difference between a translator who reads a letter aloud in a new language each time you ask, and one who writes the translation down once. The BeanShell element keeps the translator on staff between requests, but never keeps the translation.

saying these in an interview costs you the question

  • Claiming JMeter compiles the BeanShell script once at test start
  • Assuming every thread shares one interpreter for a BeanShell element
  • Believing a BeanShell PostProcessor's time lands inside the sample's elapsed value
  • Saying a script file is cheaper than an inline script because it is read once
  • Treating a short script as free because it is only a few lines
open as a page

What does JMeter's 'Reset bsh.Interpreter before each call' option cost you?

level: middleimportance: should knowfreq 38%

basics

~20 s

Ticking it throws away the element's interpreter and builds a new one on every invocation, re-sourcing the init file if one is configured. You pay construction per call, and the script loses any state it kept between calls.

open as a page

In JMeter, why can one ${__BeanShell(...)} call serialise every thread in the plan?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Each occurrence of a function call is one Function instance shared by all threads, and JMeter's BeanShell function declares execute() synchronized. Every thread that reaches that field therefore queues on one monitor and one interpreter.

open as a page

You inherit a JMeter plan whose BeanShell elements cap the injector. How do you plan the move off them?

level: principalimportance: should knowfreq 41%

basics

~20 s

Sort the scripts before rewriting any. Delete the dead ones, replace the ones duplicating a stock element, and convert only what genuinely encodes behaviour, ranked by how often it runs rather than by how long it looks.

open as a page

In JMeter, what changes in a run when you set beanshell.sampler.init?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

The named file is sourced into every BeanShell Sampler interpreter as it is built, so once per element per thread. Setting it also switches on the element's threadStarted, threadFinished, testStarted and testEnded script callbacks, which are otherwise never dispatched.

open as a page