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
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# AGENTS.md

Guidance for coding agents working in this repository. The design itself (write and read
paths, buffer ownership, map keys, the parser's accessor contract) is in `docs/DESIGN.md`;
read it before changing the generator, parser, writer or reader.

## Project

A Jackson 3 dataformat for MessagePack, `org.komamitsu:jackson-dataformat-msgpack`, with its
own MessagePack encoder and decoder. msgpack-core is a test dependency only, used as the
reference implementation in tests.

## Commands

```bash
./gradlew clean build # Compile, checkstyle, all tests (Java 17 toolchain)
./gradlew build -PtestJavaVersion=21 # Run the tests on another JDK, as CI does for 17, 21 and 24
./gradlew soakTest -Psoak.seconds=120 # Memory-growth soak tests, excluded from the normal build
./gradlew :jmh:jmhJar # Build the benchmark jar
```

Tests run with `-Dfile.encoding=windows-1252`, so a conversion that forgets to name a charset
fails in the tests.

## Rules

- **Round-trip safety comes first.** Whatever this library writes, it must be able to read
back. A type that cannot round-trip is refused on write rather than written in a form that
fails on read (see `UnreadableKeyGuard` and DESIGN.md 2.6).
- **Compatibility with the Jackson 2 module (`msgpack-jackson` in msgpack-java) is not a
goal.** Do not keep or add behaviour only because that module had it.
- **Output after a serialization failure is the caller's to discard.** Do not add rollback or
cleanup machinery for callers that catch the exception and keep using the generator.
- **Bug fixes come with a test that fails without the fix.** Assert exact values and bytes,
not only types or non-null.
- **Benchmark changes to a hot path before pushing.** Build the change and `main` and run them
back to back:
`java -jar jmh/build/libs/jmh-jmh.jar <Benchmark> -f 5 -wi 5 -w 1 -i 10 -r 2`, adding
`--add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED`
through `-jvmArgs`. Measure with a reused mapper; a benchmark that creates a mapper per
operation measures nothing users see. Numbers from different days are not comparable on
this machine, and a short run is not enough to call a change free.
- **Record meaningful results in `jmh/results/`**, one file per change, with the command, the
compared commits and the JSON control.
- **Reviews** go through GitHub Copilot, requested with
`gh api -X POST repos/komamitsu/jackson-dataformat-msgpack/pulls/<n>/requested_reviewers -f "reviewers[]=copilot-pull-request-reviewer[bot]"`.
Check each finding against the code, with a test where it makes a claim about behaviour,
before fixing or declining it.
26 changes: 26 additions & 0 deletions jmh/results/2026-09-22-write-wrap-helper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# One wrap helper for keys and container headers

Measured on the same machine as the earlier files, run back to back:

- Before: `c2d1248` (Write scalar values through one verify-and-wrap helper)
- After: `1af13b5`. Two commits on top of the before state: `f8695a4` (the change below) and
a read-side change that builds repeated parser errors in one place, which no write benchmark
runs

The JMH command of this run was not recorded, so it is not given here.

## Change

`f8695a4`: `writeName`, `openContainer` and `closeContainer` each wrapped their writer call in
their own try/catch that turned an `IOException` into a Jackson exception. The try/catch now
lives in one `pack()` helper, which makes those three hot methods small enough to inline
better.

## Result

| Benchmark | Before | After | Change |
|---|---|---|---|
| writePojoJson | 1099800 ± 14428 | 1088509 ± 13948 | control |
| writePojoMsgpack | 1170317 ± 9411 | **1273380 ± 12473** | **+8.8%** |

The error bars do not overlap and the control is flat.
26 changes: 26 additions & 0 deletions jmh/results/2026-09-23-integer-without-boxing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Reading an integer without boxing it

Same machine as the earlier files, with `-prof gc` for the allocation figures, run back to
back:

- Before: `d2d774e` (Name the context methods for what they do)
- After: `1037af9` below

The rest of the JMH command of this run was not recorded, so it is not given here.

## Change

`1037af9`: the parser boxed every integer it read into an `Integer` or `Long`. It now keeps the
value in a primitive field.

## Result

| Benchmark | Before | After |
|---|---|---|
| readPojoMsgpack allocation | 2056 B/op | **1912 B/op** |
| readPojoJson allocation (control) | 2096 B/op | 2096 B/op |

144 bytes per operation are gone. MessagePack read now allocates about 9% less than Jackson's JSON reader on the same POJO.

Throughput did not resolve on this machine (694079 ± 11508 before, 680121 ± 7908 after, with
the JSON control moving from 674k to 677k), so the allocation figure is the result here.
35 changes: 35 additions & 0 deletions jmh/results/2026-09-27-pr9-vs-main.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# PR #9 against main after the map-key and UUID changes

A check that the changes merged after the Jackson 3 rewrite (#9) did not cost performance:
#10 (string tokens read as binary), #16 (MessagePackKeySerializer removed, key guard added) and
#17 (UUID written as a string).

Same machine as the earlier files. Both commits were built and run back to back with
`java -jar jmh/build/libs/jmh-jmh.jar "MsgpackWriteBenchmark|MsgpackReadBenchmark|WriteUTF8StringBenchmark|WriteStringBenchmark" -f 5 -wi 5 -w 1 -i 10 -r 2 -bm thrpt -tu s`
(50 samples per benchmark), PR #9 (`bb4158f`) first, then main (`cff8bfc`).

The numbers are higher than the ones #9 recorded in `2026-09-21-single-pass-short-strings.md`
for the same code (writePojoMsgpack 1242839 here, 1092242 there), so only runs made back to
back are compared.

## Result

| Benchmark | PR #9 | main | Change |
|---|---|---|---|
| readPojoJson | 686406 ± 11655 | 682182 ± 10299 | control |
| readPojoMsgpack | 686441 ± 7242 | 689769 ± 5651 | +0.5%, within error |
| writePojoJson | 1091002 ± 5201 | 1072677 ± 11429 | control, -1.7% |
| writePojoMsgpack | 1242839 ± 9565 | 1237674 ± 8973 | -0.4%, within error |
| WriteStringBenchmark.shortAscii | 128397 ± 2511 | 128497 ± 2274 | within error |
| WriteStringBenchmark.shortNonAscii | 85536 ± 3778 | 89744 ± 2307 | within error |
| WriteStringBenchmark.shortNonAsciiShifted | 48127 ± 1499 | 46294 ± 1721 | within error |
| WriteStringBenchmark.longAscii | 81484 ± 1344 | 80275 ± 265 | within error |
| WriteStringBenchmark.longNonAscii | 55262 ± 518 | 55197 ± 187 | within error |
| WriteUTF8StringBenchmark.writeUTF8StringAscii | 118398 ± 1288 | 117607 ± 667 | within error |
| WriteUTF8StringBenchmark.writeUTF8StringNonAscii | 115120 ± 1381 | 112199 ± 1320 | -2.5% |

POJO read and write are on par with #9. The only MessagePack row whose error bars do not
overlap, writeUTF8StringNonAscii at -2.5%, moved while the JSON write control, whose code is
the same in both builds, dropped 1.7%, so the machine was slower during the second run. That
row is inconclusive rather than a confirmed regression; running the string benchmarks again
with main first would tell drift from a real cost.
Loading