Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 78 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,14 @@ were the ones intended. A reading of the diff proves nothing; the DOM does.
- **A French option name stays accepted for ever**, silently, now that its English equivalent
has arrived. Scripts already deployed never break. Only `faits.jsonl` will break, in format
2.0, and that is assumed: it is three days old.
- **A produced file keeps one vocabulary, and `diagnostic.json`'s is French.** Its keys —
`executions`, `rapprochement`, `fichiersSansSource` — predate the switch to English and are
frozen like every other produced key. New keys there follow them (`empreinte`,
`fichiersEcrits`, `seuilAlerte`): a document half in one language and half in the other
reads worse than either, and the choice is between consistency inside one file and
consistency with the code around it. `faits.jsonl` went the other way and paid for it with
a format break at 2.0 — worth it there, because that file is *read by others*; the
diagnostic is read by us.
- **A commit explains why**, not what the diff already shows. The subject is a sentence, not a
label.
- **A test guards a decision.** The tests here do not check lines but choices: that the Maven
Expand Down Expand Up @@ -412,12 +420,24 @@ were the ones intended. A reading of the diff proves nothing; the DOM does.
out MSYS2's POSIX translation as the cause. The JVM escapes it because it opens few files
and keeps its handles.

**That sentence is about a *walk*, and it must not be read as "concurrency never helps".**
An archiver, an explorer, a backup agent goes through the files one after another and
nothing overlaps. When we hold the *producer*, on the other hand, two producers do overlap
— which is what makes the parallel rendering below worth its extra pass.

**The cost model was measured rather than assumed**, and the measurement is what settled
the rest. Injecting a fixed latency on every `open` under the output — an `LD_PRELOAD` that
changes nothing else — gives a total that tracks **one open per file written**: ~185
delayed opens for the 178 files of a run's two sites. So the time paid is the file count
times the filter's latency, and any change is worth exactly what it does to that product.

**`JACOCO_REPORTS` decides what is written of them**, and nothing else: `full` both sites
(the default, unchanged), `detailed` the complete one alone, `data` neither. The setting
touches neither the measurement nor the display — the coverage rendered comes from
`jacoco.xml`, written in every case, and `Coverage.parse` reads it directly. On the example
application, a run goes from 186 to 101 files in `detailed`, to 9 in `data`, with the same
coverage to the figure.
(the default, unchanged), `detailed` the complete one alone, `data` neither, `minimal`
not even the campaign's merged site — its XML and CSV are still written, so the figure
survives its rendering. The setting touches neither the measurement nor the display — the
coverage rendered comes from `jacoco.xml`, written in every case, and `Coverage.parse`
reads it directly. On the example application, a run goes from 186 to 101 files in
`detailed`, to 9 in `data`, with the same coverage to the figure.

Three decisions hold it, and they are the ones to know before touching it:

Expand All @@ -439,9 +459,61 @@ were the ones intended. A reading of the diff proves nothing; the DOM does.
copy goes through the zip filesystem, so the intermediate files never exist. Writing them in
order to delete them would have cost exactly what one is trying to avoid.

**Three things are now done without being asked, because they take nothing away.**

- **The two sites are rendered at once.** The focused report learns which classes ran from
`jacoco.xml`, so the data pass has to come first — and it used to carry the complete site
with it, which left the two renderings strictly one after the other. Split into three
passes, the data pass writes two files and the two renderings overlap. Measured **on the
rendering step**, which is the only thing this changes: the two `jacococli` calls alone
go from 1.69 s to 1.24 s at 5 ms per open (−27 %), 2.62 → 1.73 at 10 ms (−34 %), 4.44 →
2.68 at 20 ms (−40 %) — and *0.75 against 0.76* with no filter at all, the extra
start-up being exactly paid by the overlap. On the tool itself, read off the files'
timestamps, the phase goes **4.26 s → 2.45 s at 20 ms (−42 %)**.

**Do not quote those percentages of a whole run.** The same run goes 7.84 s → 7.22 s
(−8 %), because it also pays for the application, the components and the assembly, none
of which this touches. The gain is on the rendering and grows with the number of
classes; everything else is fixed. The parallelism is of degree two and stays there:
each rendering parses the whole class set, so this phase's peak memory doubles.
- **The focused report is not written when every analysed class ran.** It would then list
exactly what the complete one lists. The test is strict and made on the coverage alone,
before staging: a single class never entered, or a hidden package — hidden on our side,
unknown to the CLI, hence present in the complete site — puts the two out of step and the
shortcut off.
- **The count is said.** Past `Footprint.NOTABLE` files the tool prints it at the end of a
campaign and names the directory to exclude; `diagnostic.json` carries it run by run.
Counting opens each *directory* and never a file, so it costs the tree's directories and
not its files — 0.63 s for one walk at 20 ms per open, against some four seconds for a
single pass over the files that walk describes. Cheap, not free: which is why the
diagnostic **sums the runs it has already counted** instead of walking them a second
time, and why the total is taken once, at the very end. Before this, the figure was
reachable only by counting by hand on the machine where counting is itself slow, which
is to say never: the setting existed, was documented, and nothing ever pointed at it.

**`ARCHIVE` gathers `runs/` into one `runs.zip`**, `keep` beside the tree or `replace`
instead of it. `replace` is the one that restricts — the page's links to the JaCoCo sites
stop resolving and `--report-only` has nothing left to rebuild from — so it is off by
default and says so where it is offered. Nothing is removed before the entries written are
counted against the files walked: this deletes measurements. And writing the archive does
read every file, so the gesture costs, once, exactly what it stops costing at every later
walk.

**A cache of rendered pages across runs is not possible, and the example application hides
it.** JaCoCo's source page carries the per-line colouring: the same class, with an `exec`
and without, gives 14 lines `fc` against 14 lines `nc`. But every run of `sample-app`
covers the same lines, so its 26 source pages come out identical from one run to the next —
a cache keyed on the code would pass every test here and be wrong exactly in a campaign
whose runs differ, which is the only reason to run a campaign. What *is* run-independent is
`jacoco-resources/`: 20 files per site, 22 % of a site on the example, ~2 % on an analysis
of 447 classes. It shrinks where the problem grows.

`runs/` remains, moreover, the right candidate for an antivirus exclusion: a single
directory, containing only generated artefacts, of which nothing is executed and everything
is reproducible.
is reproducible. It is the only measure that **removes** the cost rather than trimming it,
and it is the first thing the help's section on filtered machines says — that section lists
every setting that reduces the count **with what it gives up**, and `FileCostTest` holds it
to that: a value added to `JACOCO_REPORTS` and left out of the section fails the build.
- **Windows terminal**: the tool writes in UTF-8. A terminal in cp850 — the default on many
machines — renders the accents unreadable. Fix it on the terminal's side (mintty → Options →
Text → UTF-8), or launch with `-Dstdout.encoding=cp850`. Do not "fix" this in the code: when
Expand Down
50 changes: 48 additions & 2 deletions bin/acceptance-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ echo
echo "4 bis. What is written to disk is a setting, and does not touch the measurement"
pct(){ grep -o '"pct":[0-9.]*' "$1"/faits.jsonl | head -1; }
count(){ find "$1"/runs -type f | wc -l; }
for v in detailed data; do
for v in detailed data minimal; do
java -jar "$JAR" \
--java "java -jar $APP --iterations 200000" \
--level coverage --sources "$REPO_DIR/sample-app/src/main/java" \
Expand All @@ -145,11 +145,57 @@ done
step $? "in \"data\", no HTML site is written for the run"
grep -q 'toujours: true' "$REPO_DIR/orchestrator/src/main/resources/lab/xray/dashboard.html"
step $? "and the page still names the absent reports, with their command"
# The whole point of the settings: they give up a RENDERING, never a figure. The count of
# files written per run is in the diagnostic, so the trade is checkable after the fact.
grep -q '"fichiersEcrits"' rep-data/diagnostic.json
step $? "the diagnostic says, run by run, what was left on disk"
grep -q '"conseil":"Exclude' rep-data/diagnostic.json
step $? "and names the directory to exclude from an antivirus scan"
echo

# The only value that also gives up the campaign's merged rendering. It is the one that
# must not take the figure with it: an aggressive setting stays defensible only as long as
# the last possible reading survives it.
echo "4 ter. \"minimal\" gives up the merged SITE, never the merged figure"
java -jar "$JAR" \
--java "java -jar $APP --iterations 120000" \
--level coverage --sources "$REPO_DIR/sample-app/src/main/java" \
--classes "$APP" --jacoco-reports minimal --name "minimal bis" --out rep-minimal \
> rep-minimal-2.log 2>&1
step $? "a second run under \"minimal\" ends, so there is something to merge"
[ -f rep-minimal/jacoco-fusion/html/jacoco.xml ]
step $? "the merged XML is written"
[ ! -f rep-minimal/jacoco-fusion/html/index.html ]
step $? "and its site is not"
echo

# Gathering the run directories into one file, and — only if asked — putting it in their
# place. Nothing is removed before the archive has been counted against what it replaces.
echo "4 quater. --archive gathers the files, and \"replace\" takes their place"
java -jar "$JAR" \
--java "java -jar $APP --iterations 120000" \
--level coverage --sources "$REPO_DIR/sample-app/src/main/java" \
--classes "$APP" --archive --name "archive kept" --out arch-keep > arch-keep.log 2>&1
step $? "a measurement with --archive ends"
[ -f arch-keep/runs.zip ] && [ -d arch-keep/runs ]
step $? " the archive is written, and the tree stays: \"keep\" takes nothing away"
[ "$(unzip -l arch-keep/runs.zip | tail -1 | awk '{print $2}')" = "$(find arch-keep/runs -type f | wc -l)" ]
step $? " and it holds exactly as many entries as there are files"
java -jar "$JAR" \
--java "java -jar $APP --iterations 120000" \
--level coverage --sources "$REPO_DIR/sample-app/src/main/java" \
--classes "$APP" --archive replace --name "archive replaced" --out arch-rep \
> arch-rep.log 2>&1
step $? "a measurement with --archive replace ends"
[ -f arch-rep/runs.zip ] && [ ! -d arch-rep/runs ]
step $? " the tree is gone, the archive is there"
[ -f arch-rep/index.html ] && [ -f arch-rep/diagnostic.json ] && [ -f arch-rep/faits.jsonl ]
step $? " and what is read without the measurements still stands"
echo

# A report is often reassembled without passing the launch options again. What was given to
# the measurement must not get lost on the way — and above all, must not be denied.
echo "4 ter. Reassembling without --sources finds the annotated code again"
echo "4 quinquies. Reassembling without --sources finds the annotated code again"
rm -rf reassemble && cp -r out reassemble
java -jar "$JAR" --report-only --out reassemble > reassemble.log 2>&1
step $? "reassembling without --sources ends"
Expand Down
69 changes: 57 additions & 12 deletions docs/outil/mode-emploi.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ the first go — **[Reducing the footprint on a large codebase](empreinte.md)**
| `LEVEL` / `--level` | `full` | How far to observe: `coverage` (JaCoCo alone), `tree` (+ sampling), `full` (+ values). The first knob to turn down when the measurement costs too much |
| `COVER_INCLUDES` / `--cover` | everything | Classes JaCoCo instruments, e.g. `com.example.*`. **Without it, every class loaded is instrumented**, dependencies included: it is the main cost centre |
| `SAMPLE_INTERVAL_MS` / `--interval` | 1 ms | Stack sampling interval. At 10 ms, ten times fewer samples |
| `JACOCO_REPORTS` / `--jacoco-reports` | `full` | How much of JaCoCo's own rendering to write per run — see [When the number of files costs](#when-the-number-of-files-costs) |
| `JACOCO_REPORTS` / `--jacoco-reports` | `full` | How much of JaCoCo's own rendering to write per run. **Every value but the default gives a rendering up** — see [When the number of files costs](#when-the-number-of-files-costs) |
| `ARCHIVE` / `--archive` | — | Gather `runs/` into `runs.zip` once the report is built: `keep` beside the tree, `replace` **instead of it** — same section |
| `FOLLOW_PORT` / `--follow` | — | Port of the follow page. Without it nothing is served — but `progression.jsonl` is written all the same |
| `EXPORT` / `--export` | — | Rewrites the measurements for other tools: `perf`, `cpuprofile`, `lcov`, `values`, or `all` — see [the exports](exports.md) |

Expand Down Expand Up @@ -257,7 +258,8 @@ runtime-xray-out/
├── config.json ← annotations for THIS run (optional, takes priority)
├── execution.log ← the application's output
├── jacoco/html/ ← the detailed coverage, all the analysed code
├── jacoco-focused/html/ ← the same, restricted to the classes that ran
├── jacoco-focused/html/ ← the same, restricted to the classes that ran (absent when
│ every analysed class ran: it would repeat the other)
├── classes-executees.jar ← the bytecode kept for that second report
├── async-profiler/ ← the folded stacks, plus the profile rendered by the tool
│ itself (flamegraph.html and its inverse)
Expand All @@ -277,22 +279,65 @@ size of the analysed code, not with the measurement. On a machine where every fi
crosses a stack of filters — antivirus, EDR, DLP — that count is what a campaign pays for:
at write time, at every later walk, and when archiving it to pass it on.

`JACOCO_REPORTS` (or `--jacoco-reports`) decides how much of it is written:

| Value | What is written | What is lost |
**The tool says the figure itself.** Past ten thousand files it prints the count at the end
of a campaign, names the directory to exclude, and points at `--help`; `diagnostic.json`
carries the count run by run, and the count of `runs/`. Before that, knowing what a campaign
had cost meant going and counting on the machine where counting is itself slow — so nobody
did, and a setting that reduces it stayed documented and unused.

**Two things are done without being asked, because they take nothing away.** The two sites
of a run are **rendered at the same time** rather than one after the other: the cost is one
file open apiece, paid serially, so two writers overlap where one could not. Measured on the
example application, with a fixed latency injected on every file open, **on the rendering
step** — which is the only thing this changes: **−27 % at 5 ms per open, −34 % at 10 ms,
−42 % at 20 ms**, and *nothing at all* — 0.75 s against 0.76 s — with no filter, which is
what makes it free. Read as a share of a whole run the figure is much smaller, since a run
also pays for the application, the components and the assembly. And the focused report is
**not written when every analysed class ran**: it would then list exactly what the complete
one lists.

### First, and it takes nothing away: exclude the directory

`runs/` is the right candidate for an antivirus exclusion: a single directory, holding only
generated artefacts, of which nothing is executed and everything is reproducible. It is the
only measure that **removes** the cost. Everything below merely reduces it, and everything
below has a price.

### Then, if that is refused — and each of these gives something up

> **These settings restrict the report.** None of them is on by default, none of them
> changes the coverage the page displays — read from `jacoco.xml`, written in every case —
> and an absent report is never silent: the page keeps naming it, greyed out, and clicking
> gives the command that produces it. But each takes away something a reader might have
> opened. That is the trade being offered, and it is offered here rather than left to be
> discovered.

`JACOCO_REPORTS` (or `--jacoco-reports`) decides how much of JaCoCo's own rendering is
written:

| Value | What is written | What is given up |
|---|---|---|
| `full` *(default)* | both sites | nothing |
| `detailed` | the complete site alone | a framing: the focused report holds **no datum** the complete one lacks |
| `data` | `jacoco.xml` and `jacoco.csv` only | JaCoCo's rendering **per run**; the campaign's (`jacoco-fusion/`) stays |
| `minimal` | the same, and no merged site either | the rendering of the figure one hands on; the merged XML and CSV are still written |

This setting touches **neither the measurement nor what the page shows**: the coverage
rendered line by line comes from `jacoco.xml`, written in every case. And an absent report is
never silent — the page keeps naming it, greyed out, and clicking gives the command that
produces it.
`ARCHIVE` (or `--archive`) gathers `runs/` into one `runs.zip` once the report is built:

`runs/` is, moreover, the right candidate for an antivirus exclusion: a single directory,
holding only generated artefacts, of which nothing is executed and everything is
reproducible.
| Value | What it does | What is given up |
|---|---|---|
| `keep` *(what a bare `--archive` means)* | writes the archive, leaves the tree | nothing — and it reduces nothing either: one object to hand on, to back up, to have scanned once |
| `replace` | writes it, checks it, **then removes `runs/`** | the page's links to the JaCoCo sites stop resolving, and `--report-only` and `--serve` have nothing left to rebuild from. The page, the diagnostic, the facts and the Markdown still read |

Nothing is removed before the archive has been verified — the entries written are counted
against the files walked, and the tree only goes if they agree. Writing the archive does read
every file: the gesture costs, once and deliberately, exactly what it stops costing at every
later walk.

Two other settings reduce the count and are documented elsewhere because they do more than
that: `--cover` restricts what JaCoCo instruments — by far the biggest lever, and the only
one that **changes the measurement**, since what it leaves out is no longer covered at all —
and `--level coverage` gives up the call tree and the captured values.

### `rapport.md` — for a forge

Expand Down
Loading
Loading