Metadata-Version: 2.4
Name: lysiro
Version: 0.1.0
Summary: Performance test reports that run on your machine. JMeter and k6, exact percentiles, nothing uploaded.
Author: Lysiro
License-Expression: LicenseRef-Lysiro-Proprietary
Project-URL: Homepage, http://lyski.net/Lysiro/
Project-URL: Documentation, http://lyski.net/Lysiro/
Keywords: jmeter,k6,load-testing,performance-testing,jtl,percentiles,report,pdf,regression,benchmark
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Software Development :: Testing :: Traffic Generation
Classifier: Topic :: System :: Benchmark
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Provides-Extra: build
Requires-Dist: pyinstaller>=6.0; extra == "build"
Requires-Dist: build>=1.0; extra == "build"
Requires-Dist: twine>=5.0; extra == "build"
Dynamic: license-file

# Lysiro

Performance test reports that run on your machine.

Point it at a results file and get per-transaction percentiles, errors grouped
by cause, and a regression diff against a previous run. Nothing is uploaded.
There is no account.

Reads **JMeter CSV**, **JMeter XML** and **k6 JSON**, decided by looking at the
file rather than its extension.

```bash
pip install lysiro
```

```bash
lysiro report results/run-0918.jtl
lysiro report results/run-0918.jtl --baseline results/run-0911.jtl --pdf
lysiro report results/run.jtl --pdf out.pdf --title "Checkout load test" --org "Platform Engineering"

lysiro report results/run.xml            # JMeter XML
lysiro report results/out-k6.json        # k6 --out json=
lysiro convert results/huge.jtl          # strip a bloated file down first
```

**No runtime dependencies.** Not a boast — a design constraint, because this
runs inside someone else's network and every package in it is a line in their
security review. After installing, `pip list` shows `lysiro` and nothing else.
See `docs/DEPENDENCIES.md`.

## Why it exists

Every performance engineer rebuilds the same summary by hand after every run.
The numbers are always the same numbers; only the values change.

## What it does today

- Streams JMeter CSV (with or without a header row), JMeter XML, and k6 JSON
- Columns matched by **name**, not position, so a reordered file is read
  correctly rather than misread into plausible-but-wrong numbers
- Exact percentiles (p50/p90/p95/p99) — no sampling, no t-digest, no estimate
- Errors grouped by cause with first and last occurrence
- Regression diff against a baseline, with a noise threshold and a minimum
  sample count so small transactions don't cry wolf
- Exit codes for CI
- A three-page PDF: headline numbers and the response-time distribution;
  throughput, latency and errors over the run; the comparison against baseline
- `lysiro convert` to strip a bloated results file down to what a report needs


## Input formats

| Format | Produced by | Notes |
|---|---|---|
| JMeter CSV | the default `.jtl` | Survives saved response bodies; see below |
| JMeter XML | `output_format=xml` | Streamed with `iterparse`; nested sub-samples counted and reported |
| k6 JSON | `k6 run --out json=out.json` | Only `http_req_duration` points; the other metrics would multiply the sample count |

Format is decided by reading the file. A `.jtl` containing XML is ordinary, and
k6 output gets saved under every extension there is.

A k6 **summary export** is rejected with an explanation rather than a stack
trace: it holds pre-aggregated metrics and no samples, so exact percentiles are
impossible from it.

## `lysiro convert`

JMeter can be configured to save request and response bodies and headers into
the results file. That is how a run that should produce 400 MB produces 40 GB.

```bash
lysiro convert results/huge.jtl          # -> results/huge-lean.jtl
```

Measured on a synthetic 40,000-sample file with bodies and headers saved:

| | Size | Parse time |
|---|---:|---:|
| Source | 134.7 MB | 1.19 s |
| Converted | 1.8 MB | 0.13 s |

**76x smaller, 9x faster to report on.** Every sample is preserved, in order,
with its original timestamp; only the heavy columns are dropped.

The second reason matters more than the size. Saved bodies and headers are what
make a results file impossible to share -- session cookies, auth headers, and
whatever the application returned, which against a staging environment is often
real-looking customer data. The converted file has none of it, so it can go in
a ticket without a conversation first.

Output is ordinary JMeter CSV, so anything else that reads JTLs still can.

## Memory

The trick is that a JTL is mostly text the report doesn't need — URLs, thread
names, failure messages. Only the `elapsed` integer is required for
percentiles, so that is all that is retained.

Measured on 2026-09-22, Python 3.14:

| Samples | File | Peak memory | Parse time |
|---:|---:|---:|---:|
| 1,500,000 | 176 MB | 16.2 MB | 26 s |

That is about 11 bytes per sample, and roughly 11× smaller than the file.
Memory scales with **sample count**, not file size — a 2 GB JTL lands near
190 MB and takes about five minutes. It is not constant memory, and the docs
should never claim it is.

Parsing is currently bound by Python's `csv` module. If throughput becomes a
complaint, that is the thing to attack.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | Fine |
| 1 | A p95 regression exceeded the threshold |
| 2 | Bad input — file missing, or not a readable JTL |
| 3 | `--fail-on-error` was set and the run contained failures |

## Development

```bash
git clone https://github.com/evalysiro/lysiro-report
cd lysiro-report
pip install -e ".[dev]"
pytest
```

Fixtures are generated, never real: load test output carries URLs and sometimes
credentials from whoever produced it, so none is committed here.

```bash
python tests/make_fixture.py tests/fixtures/sample.jtl --seed 1
python tests/make_fixture.py tests/fixtures/baseline.jtl --seed 2 --faster
```

## Not done yet

- Gatling `simulation.log` input.
- The written summary paragraph — deterministic code does all the arithmetic,
  and a model only ever writes prose from the computed aggregates. It never
  sees a row, and it never produces a number.
- Licensing and packaged binaries.

## Layout

```
src/lysiro_report/   the deliverable
  parse.py           readers for JMeter CSV, JMeter XML and k6 JSON
  convert.py         rewrite any input as a lean JMeter CSV
  stats.py           exact percentiles, histogram, comparison
  pdf.py             minimal PDF writer, no dependencies
  render.py          the report layout
  cli.py             terminal output, exit codes
site/                the marketing page (liftable to its own repo)
scripts/             deploy
tests/
```
