Path algebra: read a path's structure once instead of three times - #1081
Merged
Merged
Conversation
Follow-up to #1074 (jolt-2sp). That change consolidated three independent answers to "what is this path's root" onto java/io.ss. What was left is that every helper re-derived the structure from the STRING: path-root and path-segments each begin with their own path-root-end scan, so a single Path method could read the same path three times -- and endsWith against a rooted other, which normalizes both sides, read two paths seven times between them. path-parse does it once and hands back a value with the root and the segments as fields. The helpers that need both read the fields. The platform is deliberately NOT a field on that value. It reads like it should be one, but nothing would ever read it back: every helper already takes `windows?` as a parameter, which is what lets a Linux runner pin the Windows rows. A field no caller reads is weight on every parse. Two adjacent things the parse made visible: * path-segments called path-root-end twice, once for each of two loop variables initialized to the same value. * path-rebuild appended per segment, and each append copies the answer built so far -- quadratic in segment count, on the function every getter ends in. It now allocates once. Not every helper parses. resolve and getRoot take only the root, and the branch almost every resolve caller takes concatenates without ever needing segments; splitting eagerly there would COST work. jolt-path- normalize is untouched on purpose -- it runs per entry on every directory listing, allocates nothing for an already-normal path, and never asks for segments. On the measurement, honestly: this does not make anything reliably faster. getRoot goes through none of the changed code and moved 7-9% between builds, which sets the noise floor, and every real effect here is at or under it. The hypothesis the bead was filed on -- that the repeated SCANNING was the cost -- is wrong: scanning a root is ~6ms of a ~33ms operation, and the rest is allocation, the segment list and the string being built. That is recorded on the bead so this is not re-opened expecting speed. What this buys is that the structure is derived once and named, and the quadratic rebuild is gone. Behaviour-preserving by construction: every -for signature is unchanged, so the existing gate is the regression suite. 24 rows were added pinning the new invariant directly rather than inferring it from callers passing -- path-parse agrees with path-root and path-segments across all six root shapes on both platforms, including roots no caller here builds. win-platform-test.ss 178 -> 238 checks, and the 23 File-join and as-relative-path cases still match the JVM exactly. npath-rooted-for? goes: its two callers now read the root they already have, and the deadhost gate caught it, which is what that gate is for.
yogthos
changed the base branch from
fix/windows-spit-pathsep-spawn
to
main
September 21, 2026 18:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1074 (bead jolt-2sp). That change consolidated three
independent answers to "what is this path's root" onto
java/io.ss.What was left is that every helper re-derived the structure from the
string:
path-rootandpath-segmentseach begin with their ownpath-root-endscan, so a single Path method could read the same paththree times — and
endsWithagainst a rooted other, which normalizesboth sides, read two paths seven times between them.
path-parsedoes it once and hands back a value with the root and thesegments as fields.
On the measurement — this does not make anything reliably faster
That is the headline, because the bead was filed as a performance issue
and it should not be re-opened expecting speed.
getRootgoes through none of the changed code and moved 7–9% betweenbuilds, which sets the noise floor. Every effect measured here is at or
under it, in both directions. Run-to-run within one binary is tight
(±2%); it is build-to-build that is loose, so more repetitions do not
help — resolving anything smaller needs an A/B inside one binary.
The hypothesis the bead rests on — that the repeated scanning was the
cost — is wrong. Measured on a 9-segment path, 20k iterations:
getRootstartsWithgetParentSo a root scan is ~6ms of a ~33ms operation and the other ~27ms is
allocation — the segment list and the string being built. Parsing
once removes redundant scans, a fraction of the 6ms. This is recorded on
the bead with the remaining ideas (compare segment-wise without
materializing lists; index pairs instead of substrings).
What it does buy
path-segmentscalledpath-root-endtwice, once for each of twoloop variables initialized to the same value
path-rebuildappended per segment, and each append copies the answerbuilt so far — quadratic in segment count, on the function every getter
ends in. It allocates once now. Checked at 20k segments.
What deliberately does not parse
resolveandgetRoottake only the root, and the branch almost everyresolvecaller takes concatenates without ever needing segments —splitting eagerly there would cost work.
jolt-path-normalizeisuntouched: it runs per entry on every directory listing, allocates
nothing for an already-normal path, and never asks for segments.
The platform is not a field on the parsed value. It reads like it
should be one, but nothing would read it back — every helper already
takes
windows?, which is what lets a Linux runner pin the Windows rows.Verification
Behaviour-preserving by construction: every
-forsignature isunchanged, so the existing gate is the regression suite. 24 rows were
added pinning the new invariant directly rather than inferring it
from callers passing —
path-parseagrees withpath-rootandpath-segmentsacross all six root shapes on both platforms, includingroots no caller here builds.
win-platform-test.ss178 → 238 checks. The 23File(parent, child)andas-relative-pathcases still match the JVM exactly. Full POSIX setgreen: unit, smoke, depsunit, deadhost, portcheck, grenadine, winpath.
deadhostcaughtnpath-rooted-for?going dead when its two callersswitched to the root they already had; it is removed here.
Stacked on #1077 — review that first.