quick_search/Cargo.toml

163 lines
8.8 KiB
TOML
Raw Normal View History

[workspace]
resolver = "2"
members = [
"crates/quicksearch-core",
"crates/quicksearch-gui",
]
# The `vendor/` crates are deliberately NOT members: they are third-party code
# carried here for a patch each, not part of this workspace's lints, tests or
# release profile. `[patch.crates-io]` below is what makes the dependency graph
# resolve to them.
exclude = ["vendor/pdf-extract", "vendor/rtf-parser"]
# pdf-extract 0.12.0, with the two unbounded recursions in it bounded.
#
# `get_inherited` follows `/Parent` and `process_stream`'s `Do` arm follows
# Form XObjects, neither with a depth counter or a visited set. A page whose
# `/Parent` is itself, or an XObject whose content stream draws itself, walks
# the stack until it hits the guard page — and a stack overflow is not a panic
# that `catch_unwind` can contain (`extract/pdf.rs` has one, for the parser's
# ordinary panics): Rust's handler calls `abort()`, so a ~600-byte file kills
# the process. It recurs on every run, because the row keeps
# `content_state = 0` and the feeder selects exactly those; and the live
# watcher re-extracts on-screen rows on the GUI thread, so such a file crashes
# the app when it merely appears in a result list.
#
# Vendored rather than forked-by-URL so the build stays offline, `--locked`
# keeps meaning what it means, and the cross-compile job needs no new host.
# The patch is marked LOCAL PATCH in the source and is upstreamable; the crate
# is MIT and the copy is recorded in `packaging/copyright`.
# rtf-parser 0.4.3, with its lexer taught where an RTF control word ends.
#
# The format's rule is that a control word runs `\` plus letters plus an
# optional numeric parameter, and ends at the first character that is neither —
# a space if there is one, which is swallowed as the delimiter, otherwise
# whatever that character is, which is *not* swallowed. The crate's lexer ends
# it at whitespace and nothing else. Two consequences, both of which lose
# indexed text silently rather than failing the file:
#
# * A `\uN` escape is followed by an ANSI fallback character for readers that
# predate Unicode, and the spec lets that be any character. `\u233?after`
# lexes as one unrecognised control word, so the character *and the rest of
# the word* vanish. LibreOffice writes `\uN\'3f` and dodges it; a literal
# `?` is just as legal and just as common.
# * After a `\'hh` escape the lexer re-tokenises the remainder and trims its
# leading spaces before classifying it. A remainder that is plain text
# keeps them; one that begins with another escape does not. So two adjacent
# words made entirely of escapes come back joined — `Καλημέρα κόσμε` as
# `Καλημέρακόσμε`, one FTS term where there were two. That reproduces on a
# file LibreOffice wrote, and it hits every script outside cp1252.
#
# Both were found by `tests/extraction_corpus.rs`, which is also what pins them
# fixed. A third patch replaces the two production `unwrap()`s in the parser:
# `String::from_utf16` on whatever `\uN` supplied panicked on a lone surrogate,
# and RTF is one of the two formats that also extract at *walk* time, where a
# panicking worker costs the root its whole content pass. That file now costs
# one replacement character instead of the whole document.
#
# One behaviour change is not a bug fix and is worth knowing about. Fixing the
# first bug leaves the ANSI fallback character sitting in the token stream as
# ordinary text, so the parser now counts fallbacks off against `\ucN` the way
# the specification says, rather than recognising only the `\'hh` spelling by
# guesswork. A document that writes `\u233 text` — space delimiter, no
# fallback, no `\uc0` — therefore loses the `t`, which is what Word does with
# that document too. It used to keep it.
#
# Vendored for the same reasons pdf-extract is, below: the build stays offline,
# `--locked` keeps meaning what it means, and the cross-compile job needs no
# new host. 0.4.3 is the latest release, so there is no upgrade to wait for.
# The patches are marked LOCAL PATCH in the source and are upstreamable; the
# crate is MIT and the copy is recorded in `packaging/copyright`.
[patch.crates-io]
pdf-extract = { path = "vendor/pdf-extract" }
rtf-parser = { path = "vendor/rtf-parser" }
[workspace.package]
version = "1.1.3"
edition = "2021"
license = "GPL-3.0-or-later"
authors = ["Jeremy <jeremy@karsttech.com>"]
repository = "https://code.karsttech.com/jeremy/quick_search.git"
# Fat LTO, one codegen unit. This reverses an earlier decision, so both
# measurements are kept: the old one was right about what it measured, and it
# measured the wrong axis on the wrong kind of build.
#
# WHAT WAS MEASURED BEFORE, against an *incremental* rebuild of the GUI after
# touching core (10.2 s at the defaults):
#
# lto=thin 67 s search cold 29.9 ms warm best 14.77 ms index cold 459 ms
# lto=thin,cgu=1 152 s (build cost alone ruled it out)
# lto=fat,cgu=1 200 s search cold 29.2 ms warm best 14.57 ms index cold 479 ms
# (defaults) 10 s search cold 30.0 ms warm best 14.92 ms index cold 478 ms
#
# It concluded that the runtime gain was at the noise floor against a 6.6-19.5x
# longer build. Two things were wrong with that as a decision:
#
# * It never weighed SIZE, which turns out to be where the effect is.
# * A 10 s incremental rebuild is the worst possible denominator for a
# link-time optimization, because the LTO link is nearly the whole cost and
# there is no compile phase to amortize it against. CI never builds that
# way; it builds clean.
#
# RE-MEASURED CLEAN, on 6 cores, stripped `target/*/quicksearch`:
#
# stripped vs def search cold warm best index cold
# (defaults) 28,248,272 - 28.2 ms 14.7 ms 445 ms
# cgu=1 25,909,616 -8.3% 28.6 ms 14.8 ms 452 ms
# lto=thin 28,233,296 -0.1% (not run: no size effect)
# lto=thin,cgu=1 25,991,536 -8.0% (not run: worse than cgu=1 alone)
# lto=fat,cgu=1 24,284,912 -14.0% 27.3 ms 14.1 ms 443 ms
#
# Search is best of three runs; the spreads overlap, so read fat as "3-4%
# faster or even", never as a regression. It is both the fastest and by far the
# smallest, which is why it wins outright.
#
# Two surprises worth keeping. `codegen-units = 1` ALONE is worth 2.3 MB - the
# old table never isolated it, because every cgu=1 row there also carried LTO.
# And `lto = "thin"` alone is worth nothing at all (0.1%), while thin+cgu=1 is
# slightly *worse* than cgu=1 by itself. Thin is not a cheaper fat here; it is
# a different, useless thing.
#
# BUILD COST, clean, 6 cores. The second column is what CI actually pays on top
# of the binaries, since `cargo test --release --workspace` links every test,
# example and bin target - `--release` is `--profile=release`, so this profile
# is theirs too:
#
# bins + all test targets total
# (defaults) 143 s 28 s 171 s
# cgu=1 158 s 41 s 199 s
# lto=fat,cgu=1 198 s 100 s 298 s
#
# So +127 s on a clean CI run, not the tens of minutes the incremental figure
# above implies. Test targets link cheaply because they are small; it is the
# 320-crate dependency compile that dominates, and cgu=1 spreads across cores.
#
# WHAT IS DELIBERATELY NOT SET:
#
# `strip` would save nothing shipped. The packaging scripts and ci.yml strip the
# staged *copy*, which keeps `target/release/` symbolised for `perf` and for the
# RUST_BACKTRACE=1 release-test backtraces CI prints, and keeps `--no-strip` on
# build-deb.sh and build-appimage.sh meaning something. Setting it here would
# move the same 6.5 MB saving to a place where it costs debuggability.
#
# `panic = "abort"` is the largest win left - it would delete `.gcc_except_table`
# (428 KB after LTO) and most of `.eh_frame` (1.33 MB) - and is unavailable for
# two independent reasons. `extract/pdf.rs` runs `pdf-extract` inside
# `catch_unwind`, so aborting would turn a malformed PDF into a killed process
# instead of one skipped file. And cargo forces every dependency to rebuild with
# unwind when building tests under an abort profile, so `cargo test --release`
# would compile the whole graph a second time.
#
# Non-PIE would remove most of `.rela.dyn` (1.37 MB) and is rejected on
# hardening grounds: this program parses arbitrary user PDFs with a crate known
# to panic on malformed input, which is the last place to give up ASLR.
#
# The vendored OpenSSL (~1.3 MB of `.text`, via SQLCipher) is unreachable by any
# profile knob - `openssl-src` pins its own `-O2` - and dropping it would add a
# runtime `libcrypto` dependency, which is exactly what the AppImage's
# bundles-no-libraries invariant forbids.
[profile.release]
lto = "fat"
codegen-units = 1