Conversation
Zipping a 5,003-file tree spent 245.6 ms of an 831 ms TmpZip.new inside
Pathname#relative_path_from, which splits both paths into components and walks
them. Every entry is by construction a descendant of the glob root, so the
relative path is just the entry with the base prefix removed -- 1.7 ms for the
same 5,003 entries.
#entries also did more work than it needed to:
* it globbed the tree twice into Pathname objects, including the directories
it was about to discard;
* push(*array) splats the second glob across the stack;
* Array#sort over Pathnames re-runs Pathname#<=>, which translates "/" in
both operands on every comparison. Building that key once per entry
(sort_by) gives the identical ordering for 1/3 the cost.
Entry order decides the archive bytes and therefore its SHA1, which is what the
dirty check compares, so the ordering is preserved exactly rather than switched
to a plain String sort -- those differ ("a/b" sorts before "a-b" under
Pathname, after it under String). Added specs cover that ordering and the
existing "dotfiles yes, dot-directories no" glob behaviour.
Verified byte-identical: same archive SHA1 and same entry ordering across a
5,003-file cookbook tree, a tree with unicode/spaced/25-level-deep paths, and a
tree with hidden directories.
TmpZip.new over 5,003 files, median of 7 (Ruby 4.0.6, macOS arm64):
main 865.5 ms
branch 603.7 ms ~30% faster
Signed-off-by: Tim Smith <tsmith84@proton.me>
|
Two things a reviewer will reasonably want to know, since this touches path handling on a Windows-targeting gem: Separators. Prefix-stripping assumes entries share a literal prefix with The one shape that would break prefix-stripping is a CI. The |
Summary
Zipping a directory is the expensive half of a folder upload, and a fifth of it was going into
Pathname#relative_path_from.Profiling
TmpZip.newover a 5,003-file cookbook tree (Ruby 4.0.6, macOS arm64):relative_path_fromsplits both paths into components and walks them looking for a common root. But every entry is by construction a descendant of the glob root, so the relative path is just the entry with the base prefix stripped.#entriesalso did more work than needed:Pathnameobjects, including the directories it was about todelete_ifpush(*array)splats the entire second glob across the stackArray#sortover Pathnames re-runsPathname#<=>, whichtr-translates"/"in both operands on every comparison —sort_bybuilds that key once per entry insteadOrdering is preserved exactly — this is the risky part
Entry order determines the archive bytes, and therefore its SHA1, which is precisely the value the dirty check compares. So this deliberately does not switch to a plain
Stringsort, which is not the same ordering:I also left the two-glob (
**/.*then**/*) structure alone. Collapsing it to a singleFile::FNM_DOTMATCHglob is tempting and faster, but it is not equivalent —**/descends into hidden directories underFNM_DOTMATCHand does not without it:That would change what gets uploaded, so it is out of scope here. (Whether the current behaviour of silently skipping files under
.chef/,.delivery/etc. is desirable is a separate question I'm happy to open an issue for.)Verification
Built archives with
mainand this branch and compared the archive SHA1 and the entry-name ordering, across three trees:Checked both with
Zip.unicode_namesat its default and set totrueasFileTransporter#initializesets it during a real upload.Also confirmed prefix-stripping matches
relative_path_fromfor base directories given asdir,dir/,dir//,dir///, a nested subdirectory,.and./— hence thesub(%r{/*\z}, "/")normalisation rather than a plainchomp("/"), which getsdir//wrong.New specs lock in the ordering rule and the dot-directory behaviour. Both pass against unmodified
maintoo — they describe existing behaviour rather than the new implementation — and the ordering spec fails if the sort is naively switched toString#sort.bundle exec rspec spec/unit— 5 examples, 0 failuresbundle exec cookstyle --chefstyle -c .rubocop.yml— 16 files, no offensesResults
TmpZip.newover 5,003 files, median of 7 after 3 warmup iterations, repeated 3 times:The branch timings are notably steadier than
main's, which is consistent with dropping ~5,000 short-lived Pathname/String allocations per archive.