Skip to content

Split the REPL into a separate query-json-repl binary - #71

Closed
davesnx wants to merge 2 commits into
mainfrom
repl-split
Closed

davesnx wants to merge 2 commits into
mainfrom
repl-split

Conversation

@davesnx

@davesnx davesnx commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Every query-json invocation, even --version, paid about 3 ms and 8 MB of module initialisation for the REPL's terminal UI stack: mosaic.ui depends on cmarkit, whose Cmarkit_data builds its Unicode and HTML-entity tables eagerly at startup. On the small benchmark files that made jq 1.7 and jaq three to four times faster than query-json, purely on process startup.

This moves the REPL into its own executable, query-json-repl, and keeps query-json --repl working by launching the sibling binary (next to the executable first, then on PATH; a missing binary prints a hint and exits 127). The JSON-loading cases for file, inline and stdin input move unchanged into cli/repl.ml; the TUI module becomes Repl_ui.mlx so the entry point can own the Repl module name. Release zips, scripts/install.sh and the npm postinstall ship both binaries.

Startup, hyperfine -N --warmup 5 --min-runs 40, release builds, Linux x86-64, OCaml 5.5.0~beta1, base 0f5ca93 vs head dbbafcb:

Command mean ± σ
query-json -n 1 at 0f5ca93 4.4 ± 0.5 ms
query-json -n 1 at dbbafcb 1.6 ± 0.3 ms
jq -n 1 (1.7) 1.8 ± 0.3 ms
jaq -n 1 (3.1.1) 1.8 ± 0.2 ms

Allocation at exit (OCAMLRUNPARAM=v=0x400) drops from 1,063,863 to 20,074 words; cli.exe shrinks from 14.2 MB to 8.1 MB and nm finds no camlMosaic* or camlCmarkit* symbols in it. A hello world linked with cmarkit alone starts in 3.1 ms, which isolates the cost.

Checks at dbbafcb: dune build @runtest @fmt (797 runtime, 77 reader, CLI Cram incl. a new --repl case that runs through the shim) exit 0; release @all exit 0; git diff --check clean.

Review note: the two-binary release path (.github/workflows/ci.yml, scripts/install.sh, scripts/make-npm-release.js, scripts/release-postinstall.js) only runs on a tagged build and is not exercised locally.

Risk: two-way door. A merge revert restores the single-binary CLI and packaging; nothing outside the repository changes.

@davesnx davesnx left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review of dbbafcb against main (0f5ca93). Built with dune build -p query-json @install, ran @runtest @fmt (exit 0, 797 tests), exercised both binaries directly, and re-measured startup with hyperfine -N (20 runs: about 2x faster at -n 1, same direction and magnitude as the table above).

Findings (inline): three should-fix, one nit. None blocks the split itself; the sibling lookup and the install.sh -r <old-tag> regression are the two worth fixing before merge.

Checked and clean

  • cli/repl.ml matches the four positional cases of the old cli.ml arm by arm, including <inline>/<stdin> labels, error printing, exit codes, and the parse-then-reconnect TTY order.
  • Piped stdin reaches the REPL through the sh -c hop; Filename.quote_command handles inline JSON with spaces and commas.
  • dune: no module overlap between the two executables, Repl_ui.mlx builds under the mlx dialect, cli/test/dune depends on both binaries.
  • nm finds no camlMosaic*/camlCmarkit* symbols in cli.exe; size 14 MB to 8.1 MB as stated.
  • Pre-existing, not introduced here: the CI "Upload artifacts" step runs only on ubuntu-latest while the release step later expects macOS and Windows artifacts; Windows zips drop the .exe extension. Both are byte-identical on main.

Comment thread cli/cli.ml Outdated
3 ms of module initialization per process, which every plain query would
otherwise pay. Look next to this executable first, then on PATH. *)
let run_repl args =
let dir = Filename.dirname Sys.executable_name in

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix. Sys.executable_name resolves through symlinks (/proc/self/exe on Linux), so for any symlink-based install the sibling check looks in the wrong directory and only the PATH fallback saves it. Reproduced with dune's own install tree:

$ readlink -f _build/install/default/bin/query-json
.../_build/default/cli/cli.exe          # directory holds repl.exe, not query-json-repl
$ _build/install/default/bin/query-json --repl . cli/test/mock.json </dev/null
sh: 1: query-json-repl: not found
query-json-repl was not found. Install it next to query-json or on PATH.
exit 127

The shipped zip, install.sh and the npm postinstall place real files side by side, so they are fine; stow/nix/asdf-style layouts are not. Suggest also trying Filename.dirname Sys.argv.(0) (the path as invoked) before falling back to PATH.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 34c0986: the shim now resolves the binary itself, trying the directory of argv.(0) (when it has one), the directory of Sys.executable_name, then each PATH entry, and only then calls Sys.command with the absolute path. Verified: dune symlinked install tree with PATH= empty launches the REPL; a stray ./query-json-repl in the current directory is not picked up when query-json is found via PATH.

Comment thread cli/cli.ml Outdated
"query-json-repl"
in
let code = Sys.command (Filename.quote_command exe args) in
if code = 127 then

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix. Sys.command goes through cmd.exe on Windows, where a missing program exits with 9009, not 127, so the hint is never printed on the Windows build that CI ships. Check code = 127 || code = 9009, or branch on Sys.win32.

nit. On Unix the shell prints its own sh: 1: query-json-repl: not found before the friendly hint. Checking Sys.file_exists on the resolved candidate (or searching PATH yourself) before calling Sys.command would avoid the double message.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 34c0986: no shell is involved in the lookup any more, so the 127-vs-9009 difference and the extra sh: not found line are both gone. A missing binary prints only the hint and exits 127 on every platform; the extension is .exe only when Sys.win32.

Comment thread scripts/install.sh Outdated
unzip -q "$DOWNLOAD_DIR/$FILENAME.zip" -d "$DOWNLOAD_DIR"
mv "$DOWNLOAD_DIR/query-json" "$INSTALL_DIR/query-json"
chmod u+x "$INSTALL_DIR/query-json"
mv "$DOWNLOAD_DIR/query-json-repl" "$INSTALL_DIR/query-json-repl"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix. The script runs under set -e (line 3). Pinning an older release with -r <tag> downloads a single-binary zip, so this mv fails after query-json was already moved and before the chmod runs, leaving a half-installed, non-executable binary:

mv: cannot stat '.../query-json-repl': No such file or directory

Guard with [ -f "$DOWNLOAD_DIR/query-json-repl" ] && mv ..., or state in the script that -r only supports releases from this change onward.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 34c0986: query-json is moved and made executable first; query-json-repl is moved only if the zip contained it. Re-ran the sequence against a single-binary directory: exit 0, query-json installed and executable.

@davesnx davesnx closed this Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant